mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
594 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1695ab8c2 | ||
|
|
f27979e729 | ||
|
|
6e61e8172a | ||
|
|
21890894ae | ||
|
|
1feecd559d | ||
|
|
6ea4afe75b | ||
|
|
fd96f6367d | ||
|
|
8ce183c4c9 | ||
|
|
407a1db101 | ||
|
|
622d096e31 | ||
|
|
bf27fb1f89 | ||
|
|
860b9055c5 | ||
|
|
b3ae3a4148 | ||
|
|
0abd7d5762 | ||
|
|
0aac2bc55b | ||
|
|
fa08dc5cfb | ||
|
|
8d18970b4c | ||
|
|
173f5ce979 | ||
|
|
2e7088310a | ||
|
|
c75d99030c | ||
|
|
e4ba1d9bae | ||
|
|
e2886c342a | ||
|
|
e600d8a56c | ||
|
|
caeb305475 | ||
|
|
3d3a9d9bad | ||
|
|
5499ad5b94 | ||
|
|
0e29cd0cee | ||
|
|
24fb1b1a8f | ||
|
|
03fb4cbf0c | ||
|
|
e909280d5b | ||
|
|
d41ddf60b4 | ||
|
|
1e01a74986 | ||
|
|
96a007aace | ||
|
|
9f23bb6129 | ||
|
|
b7e8559773 | ||
|
|
5b247575c8 | ||
|
|
eb0e5b478f | ||
|
|
08906ae1da | ||
|
|
395a5efecd | ||
|
|
0452dd14f6 | ||
|
|
3906df314d | ||
|
|
ce49aa2ebd | ||
|
|
f2cb062b1e | ||
|
|
f25819a940 | ||
|
|
3075a9f9f4 | ||
|
|
52f1a672c8 | ||
|
|
45785fde1c | ||
|
|
6a7649e1d7 | ||
|
|
8b3831eb2b | ||
|
|
73e7f4669c | ||
|
|
c9f7bbbd25 | ||
|
|
ee67238133 | ||
|
|
b425da8d0f | ||
|
|
754a7f70bc | ||
|
|
a857ffa987 | ||
|
|
391ce2ebba | ||
|
|
086723adf4 | ||
|
|
e993279c35 | ||
|
|
aa64310875 | ||
|
|
795baf2c01 | ||
|
|
68ac453100 | ||
|
|
79d1f512e5 | ||
|
|
e739211314 | ||
|
|
d3f6374bb4 | ||
|
|
5add632cbc | ||
|
|
d210d1bcce | ||
|
|
6d6422cd63 | ||
|
|
66e8b7702b | ||
|
|
b75502ad87 | ||
|
|
3fba96d11f | ||
|
|
3571905705 | ||
|
|
acf0e2a1cb | ||
|
|
9e7dcb0d7c | ||
|
|
3c261e3cf7 | ||
|
|
b6f324f399 | ||
|
|
f2459489fa | ||
|
|
f8691cf285 | ||
|
|
2e631d3c42 | ||
|
|
1044dca936 | ||
|
|
56c3f495d5 | ||
|
|
0f3036dd9c | ||
|
|
af445ef157 | ||
|
|
25eb09f11c | ||
|
|
16f255aeca | ||
|
|
d0b340837d | ||
|
|
893828ac44 | ||
|
|
a3ca03636a | ||
|
|
582ffe3f23 | ||
|
|
3caad2cceb | ||
|
|
618dcf014d | ||
|
|
d88e20bb56 | ||
|
|
8a8b2b027e | ||
|
|
41cb223099 | ||
|
|
b93ea5e662 | ||
|
|
31c35939fd | ||
|
|
20a35f4221 | ||
|
|
bc6aeb2e93 | ||
|
|
a561299809 | ||
|
|
76efb938ab | ||
|
|
56abf73eaf | ||
|
|
2bc068d65a | ||
|
|
f191259f37 | ||
|
|
1384c6661d | ||
|
|
c047621548 | ||
|
|
017aaf2e54 | ||
|
|
e4b2b930af | ||
|
|
2646c0f0ee | ||
|
|
4afadb779c | ||
|
|
ad6e4daa22 | ||
|
|
d5cd07fe76 | ||
|
|
364065ed51 | ||
|
|
b4f05d4da7 | ||
|
|
3d12a0e8e9 | ||
|
|
e29f3194f3 | ||
|
|
6bac86638b | ||
|
|
d6ee1cdf6e | ||
|
|
e9d0efaf93 | ||
|
|
85467fa15b | ||
|
|
84bb47aa53 | ||
|
|
75f245c7dc | ||
|
|
6c5be88432 | ||
|
|
590b22de69 | ||
|
|
4770269f6f | ||
|
|
823f591aa8 | ||
|
|
c88c489633 | ||
|
|
8afb58a044 | ||
|
|
428fa8a61b | ||
|
|
3ba5e1ee79 | ||
|
|
d46d0cb384 | ||
|
|
f841884557 | ||
|
|
d381ab5316 | ||
|
|
a3e8e7ae77 | ||
|
|
a405753827 | ||
|
|
8df74d2c4b | ||
|
|
cbe0ffe52a | ||
|
|
a5631a0476 | ||
|
|
e4bd704a53 | ||
|
|
81a53440bc | ||
|
|
3466de1990 | ||
|
|
253b053c2c | ||
|
|
928d012046 | ||
|
|
19367406c6 | ||
|
|
f4e3717dd3 | ||
|
|
c01e1d91c5 | ||
|
|
6983f9f0b6 | ||
|
|
c13cf1a86c | ||
|
|
2a3dafe07f | ||
|
|
0104d02442 | ||
|
|
8cef4fde82 | ||
|
|
3dc02516ea | ||
|
|
3614529fda | ||
|
|
80d4f06e56 | ||
|
|
9ff4f395b5 | ||
|
|
61b8fa116a | ||
|
|
22a3541b7b | ||
|
|
c57515fed5 | ||
|
|
eec6199413 | ||
|
|
fe48955b94 | ||
|
|
03047ae6dd | ||
|
|
856d4867b4 | ||
|
|
b1b1aa0e13 | ||
|
|
ab68472698 | ||
|
|
5ae4ee1411 | ||
|
|
f1989bac21 | ||
|
|
1b20188b98 | ||
|
|
140a79d18c | ||
|
|
a9610ced0e | ||
|
|
17faee7719 | ||
|
|
120ca1c02c | ||
|
|
ef2d1ebe4f | ||
|
|
b9c931c97f | ||
|
|
60dd963d7d | ||
|
|
c414fac815 | ||
|
|
95041d6a0c | ||
|
|
1fab9c3279 | ||
|
|
3b8661249e | ||
|
|
2fba831851 | ||
|
|
fd4ac14ab3 | ||
|
|
1d9b9e9bfa | ||
|
|
14b2277313 | ||
|
|
0f57bf5235 | ||
|
|
4bb7fae1e3 | ||
|
|
ec0b8ebc92 | ||
|
|
1727633f5d | ||
|
|
668a77cb5a | ||
|
|
e87ee5e091 | ||
|
|
b7acc0e6da | ||
|
|
109f34680f | ||
|
|
fd57ccc1a7 | ||
|
|
e3dfae246a | ||
|
|
7186bbbe6c | ||
|
|
c7b2ce37b1 | ||
|
|
5b3a602911 | ||
|
|
6d51edd94d | ||
|
|
dcb68a6538 | ||
|
|
d549b86c81 | ||
|
|
ac415f1384 | ||
|
|
80ed5800a0 | ||
|
|
291ed44621 | ||
|
|
a7e8915ea0 | ||
|
|
a027c76af3 | ||
|
|
ec375bd068 | ||
|
|
f5a28c83f0 | ||
|
|
813240e233 | ||
|
|
051ac0e669 | ||
|
|
5c798c4569 | ||
|
|
cc2146e397 | ||
|
|
8356514bf8 | ||
|
|
464bc10860 | ||
|
|
5436c8bed1 | ||
|
|
1b163b161b | ||
|
|
f0ee5fd946 | ||
|
|
066a9f871c | ||
|
|
e23841f5bb | ||
|
|
571e66fae5 | ||
|
|
402aa280e0 | ||
|
|
65de5df319 | ||
|
|
63168afc85 | ||
|
|
1c61f54df6 | ||
|
|
50b5ad1799 | ||
|
|
6457c02a35 | ||
|
|
7b242c9733 | ||
|
|
e0ab6137e7 | ||
|
|
49b23a33e7 | ||
|
|
c5e9d14199 | ||
|
|
f7e8662bdf | ||
|
|
7d356d1e34 | ||
|
|
f9bb70f395 | ||
|
|
4335809468 | ||
|
|
2a1b1d28bd | ||
|
|
38d69428d8 | ||
|
|
0c22a8135d | ||
|
|
2b50665f9e | ||
|
|
59607efa62 | ||
|
|
cd5cfbe009 | ||
|
|
84e6d96ce0 | ||
|
|
8f00b53fab | ||
|
|
e2164a1a9c | ||
|
|
6e5dcaf08d | ||
|
|
18a2aae66a | ||
|
|
4c0aab15fa | ||
|
|
75a7d4188b | ||
|
|
ff0da57aeb | ||
|
|
d716cba46b | ||
|
|
5d41e44141 | ||
|
|
281936ecd0 | ||
|
|
da23321c0d | ||
|
|
110e2b7580 | ||
|
|
546d4353e2 | ||
|
|
15b5d36cd6 | ||
|
|
6a935a49ea | ||
|
|
f7f515481f | ||
|
|
3aab37c0c0 | ||
|
|
123e626df6 | ||
|
|
758985675d | ||
|
|
631ebc657b | ||
|
|
d0ca714482 | ||
|
|
1fe3787186 | ||
|
|
c8a952616f | ||
|
|
efcbecc218 | ||
|
|
487bafa5cf | ||
|
|
93aed33e2a | ||
|
|
a7765cb635 | ||
|
|
e6a5be6c66 | ||
|
|
5443532266 | ||
|
|
990aca2e1a | ||
|
|
49297adf97 | ||
|
|
b4d26fd35a | ||
|
|
b31f580760 | ||
|
|
7dc33f1956 | ||
|
|
e3470dd68b | ||
|
|
410c653654 | ||
|
|
bef3cf4c93 | ||
|
|
d8961d2acb | ||
|
|
17873ef7aa | ||
|
|
1d6e7eabc1 | ||
|
|
ba956abb1c | ||
|
|
7a2c2df89e | ||
|
|
bd44e659a8 | ||
|
|
8a1a27f5a1 | ||
|
|
7825071f61 | ||
|
|
7047bcbb1e | ||
|
|
193ef74e63 | ||
|
|
8a193d4dcd | ||
|
|
35cc662b26 | ||
|
|
9173bcc742 | ||
|
|
267f4273ee | ||
|
|
f1e675d662 | ||
|
|
c228534218 | ||
|
|
181fa5f32a | ||
|
|
7d5c37ec33 | ||
|
|
2eb9736d23 | ||
|
|
9f89ad2a08 | ||
|
|
490db3a026 | ||
|
|
2428f073bc | ||
|
|
e1231585ce | ||
|
|
839adaf559 | ||
|
|
839e004c08 | ||
|
|
ecf98828ff | ||
|
|
4dbc3c2353 | ||
|
|
c953a337fe | ||
|
|
6f026e6043 | ||
|
|
a775e29aef | ||
|
|
cb7b37fca4 | ||
|
|
cc79a67a9f | ||
|
|
72e7e3a3c4 | ||
|
|
4ffbeebd85 | ||
|
|
2e4b4e4736 | ||
|
|
a0acb0b658 | ||
|
|
feeaea4d64 | ||
|
|
0b7eb96e48 | ||
|
|
f0c4b628bf | ||
|
|
c66fc102ee | ||
|
|
9947a23343 | ||
|
|
abc204b313 | ||
|
|
425a0812bd | ||
|
|
030d466b11 | ||
|
|
27e9cd04a9 | ||
|
|
e42aea9444 | ||
|
|
3e1ee720f1 | ||
|
|
cb9f12482e | ||
|
|
bcf273a435 | ||
|
|
6230ada2cc | ||
|
|
db4de65683 | ||
|
|
931f7f07ca | ||
|
|
0f764c9400 | ||
|
|
89c98ab257 | ||
|
|
a49255c471 | ||
|
|
39d5a4908f | ||
|
|
fbaeaddff7 | ||
|
|
bc97db982e | ||
|
|
9a2f260bc5 | ||
|
|
954f1e3c7f | ||
|
|
0626f8d678 | ||
|
|
aa8c9676fe | ||
|
|
bb2ab768a8 | ||
|
|
8192c68f38 | ||
|
|
454561c2a1 | ||
|
|
deabcc9605 | ||
|
|
c3bdb9dd16 | ||
|
|
86d77c908f | ||
|
|
c5cbba5971 | ||
|
|
aea5493180 | ||
|
|
431832e50e | ||
|
|
daf239c93c | ||
|
|
2c669deae5 | ||
|
|
81969de76e | ||
|
|
5a29e7a83e | ||
|
|
9ed5aef40f | ||
|
|
9e025b4329 | ||
|
|
543da4fe24 | ||
|
|
e82e50a57a | ||
|
|
288d62d666 | ||
|
|
8ed224eeab | ||
|
|
22bafff943 | ||
|
|
02403cf566 | ||
|
|
eabbc6f037 | ||
|
|
27e60edaf4 | ||
|
|
f7d0b64dea | ||
|
|
b874484101 | ||
|
|
0ee72db2e9 | ||
|
|
4f07bac9ec | ||
|
|
80abaf70f2 | ||
|
|
d6acbc2fb1 | ||
|
|
caf93c019a | ||
|
|
7044efa7c3 | ||
|
|
5b4d8f971f | ||
|
|
75e8feb9b8 | ||
|
|
5d59419a06 | ||
|
|
ff2e47bf70 | ||
|
|
4721ed7c15 | ||
|
|
d9da328088 | ||
|
|
2ee506ac06 | ||
|
|
16c4e4dc5b | ||
|
|
28b8fb1b97 | ||
|
|
c8bae9fba6 | ||
|
|
920764ad33 | ||
|
|
a45d114527 | ||
|
|
62cca09045 | ||
|
|
ea126b90e2 | ||
|
|
777a20182e | ||
|
|
df62a9d32b | ||
|
|
47e211bf3c | ||
|
|
650ca3844e | ||
|
|
8de49fa027 | ||
|
|
35b7633f4d | ||
|
|
2c96a80280 | ||
|
|
c7000c8ac6 | ||
|
|
1dca2b4387 | ||
|
|
ccbe9ad21e | ||
|
|
1fe6f11ef4 | ||
|
|
9d4ab60fc4 | ||
|
|
ea8c5ad6e1 | ||
|
|
63a0f5f91c | ||
|
|
e8bacbdb6f | ||
|
|
bcc8226ccc | ||
|
|
ddec91a0c5 | ||
|
|
2c262bb29d | ||
|
|
ae0afb7b53 | ||
|
|
84e7fe5b60 | ||
|
|
ccb32b045a | ||
|
|
b2e29ac4bd | ||
|
|
7df6309a68 | ||
|
|
a203ad9f64 | ||
|
|
44d6e09337 | ||
|
|
7c59ec019a | ||
|
|
b457f64ec8 | ||
|
|
e152e59a61 | ||
|
|
2c620ad69a | ||
|
|
c08b405fc2 | ||
|
|
585e39c591 | ||
|
|
60e857dba9 | ||
|
|
9a4aa2b08f | ||
|
|
2900b08b70 | ||
|
|
b0936563a2 | ||
|
|
3d327b245a | ||
|
|
9a14de3448 | ||
|
|
59f83545cf | ||
|
|
d9da1ef085 | ||
|
|
279a27f740 | ||
|
|
6ba822ee48 | ||
|
|
e4445949c8 | ||
|
|
8178ff583d | ||
|
|
0e0e19a802 | ||
|
|
1e2e7d841f | ||
|
|
263c2f00eb | ||
|
|
39c0b57652 | ||
|
|
677baf549c | ||
|
|
848478c28b | ||
|
|
98ad33a589 | ||
|
|
3f33733f40 | ||
|
|
cdc4ae4fb3 | ||
|
|
3edfa8a6ce | ||
|
|
6e99b667af | ||
|
|
23c8735568 | ||
|
|
f39065044f | ||
|
|
7d2ae47b0f | ||
|
|
7247de6908 | ||
|
|
4c2aef8504 | ||
|
|
ac8717cf1f | ||
|
|
7761928064 | ||
|
|
198047406b | ||
|
|
1d57309db9 | ||
|
|
07af9f36b2 | ||
|
|
d157fea9be | ||
|
|
7692caf622 | ||
|
|
dc65a8823f | ||
|
|
2210932fdf | ||
|
|
0cb1bf4b7f | ||
|
|
44d175dd40 | ||
|
|
097feb6437 | ||
|
|
250c7e5f20 | ||
|
|
83740f77bb | ||
|
|
8267e13d22 | ||
|
|
a7e9396f35 | ||
|
|
0f6c8601d2 | ||
|
|
66e68092d5 | ||
|
|
09616a594c | ||
|
|
7ae58ea7ea | ||
|
|
9f1f85a3c4 | ||
|
|
9992cdfce0 | ||
|
|
f5fa08ce94 | ||
|
|
67f70f7f85 | ||
|
|
840bd56a3d | ||
|
|
c1e2d31cfd | ||
|
|
c35322d44e | ||
|
|
9867e46c3f | ||
|
|
946a120038 | ||
|
|
2cd1a17aa2 | ||
|
|
6e29fe2932 | ||
|
|
e6c6bf6613 | ||
|
|
72c53169df | ||
|
|
b78cce7b4f | ||
|
|
c40499ec31 | ||
|
|
ee158e517e | ||
|
|
70aebcf9aa | ||
|
|
b15870d441 | ||
|
|
fe7074736a | ||
|
|
69c523ffad | ||
|
|
6aeefdf43d | ||
|
|
5f4c8be3d3 | ||
|
|
3092e4c557 | ||
|
|
ea119068da | ||
|
|
14371ecf94 | ||
|
|
45b0fcfe15 | ||
|
|
00aa5f5586 | ||
|
|
79fd53fd4c | ||
|
|
357ee3daf0 | ||
|
|
c2f7897f10 | ||
|
|
75455c0c48 | ||
|
|
a394bb9f8e | ||
|
|
2f5a846493 | ||
|
|
90376b361d | ||
|
|
229cf6bf5f | ||
|
|
bc46737353 | ||
|
|
1db2243a2e | ||
|
|
7cf836b3cb | ||
|
|
f7bbd295d6 | ||
|
|
62dbd95b48 | ||
|
|
df07e9c719 | ||
|
|
9aaf72726e | ||
|
|
5289927619 | ||
|
|
fa8c686f75 | ||
|
|
df5f28b7c4 | ||
|
|
280d8368fa | ||
|
|
80dbff1f21 | ||
|
|
7ee68a8481 | ||
|
|
ac8dd42c45 | ||
|
|
eed2148b2a | ||
|
|
dc5345b6d3 | ||
|
|
221af0b5bb | ||
|
|
a10ccc1eb0 | ||
|
|
b72d858480 | ||
|
|
60412cc90b | ||
|
|
512ac87dc9 | ||
|
|
d97020d1c5 | ||
|
|
f82cb617ba | ||
|
|
f79281a1a0 | ||
|
|
7be1dbb78b | ||
|
|
f875787799 | ||
|
|
f82c208556 | ||
|
|
f2150e3d85 | ||
|
|
ecc198e8a0 | ||
|
|
949bc58a80 | ||
|
|
7d79fff16f | ||
|
|
3aeb678292 | ||
|
|
160ac41bed | ||
|
|
9dfbcbe89c | ||
|
|
30da529348 | ||
|
|
dd8d114711 | ||
|
|
2191a4a848 | ||
|
|
8b9ea8d988 | ||
|
|
46dda8567d | ||
|
|
6953da4d9a | ||
|
|
e987d6647e | ||
|
|
359d85727e | ||
|
|
a994bf9dd8 | ||
|
|
14c4e095f6 | ||
|
|
59dce0e56f | ||
|
|
1f54a893a7 | ||
|
|
9489f1ee3d | ||
|
|
dc3d720e8d | ||
|
|
efe30b598b | ||
|
|
42515bfb2d | ||
|
|
acb3657d95 | ||
|
|
e7159c9d36 | ||
|
|
f3fdca368b | ||
|
|
4ea3e08a45 | ||
|
|
1eebc72b21 | ||
|
|
9a91be7e36 | ||
|
|
48476f9b88 | ||
|
|
68c991eb9b | ||
|
|
b51c77b01b | ||
|
|
57105db554 | ||
|
|
4bd3bdaddf | ||
|
|
9cf59b8d73 | ||
|
|
a793b0bb42 | ||
|
|
4d9e2e1471 | ||
|
|
1719887e55 | ||
|
|
2be00aca9d | ||
|
|
6fd05c5ad7 | ||
|
|
65e404374f | ||
|
|
0b78731bb3 | ||
|
|
65d318ed88 | ||
|
|
1bfcea55a9 | ||
|
|
6780eb004d | ||
|
|
0e12b2d021 | ||
|
|
1b356f87ec | ||
|
|
bf24d0bae1 | ||
|
|
97c831d4bb | ||
|
|
e3e48ffa6d | ||
|
|
b678416122 | ||
|
|
df722925fa | ||
|
|
4cbc0d9806 | ||
|
|
b0f3711b4e | ||
|
|
14ec6579b2 | ||
|
|
30bf039473 | ||
|
|
33bea317b0 | ||
|
|
c6814dc05e | ||
|
|
16808069ec | ||
|
|
e813974e29 | ||
|
|
7e0010b536 | ||
|
|
93171adcb3 | ||
|
|
9501cc76a4 | ||
|
|
5d7db046ac | ||
|
|
46c259bc3e | ||
|
|
3bacff91d3 | ||
|
|
bd79d483d2 | ||
|
|
16e31f4881 | ||
|
|
afa23c393d | ||
|
|
54d23cb781 | ||
|
|
c757e410e9 | ||
|
|
39dd25567d | ||
|
|
b3c0494618 |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -20,7 +20,7 @@ Steps to reproduce the behavior:
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
** Keepass Database **
|
||||
**KeePass Database**
|
||||
- Created with: [e.g Windows KeePass 2.42]
|
||||
- Version: [e.g. 2]
|
||||
- Location: [e.g. Remote file retrieved with GDrive app]
|
||||
|
||||
68
CHANGELOG
68
CHANGELOG
@@ -1,3 +1,71 @@
|
||||
KeePassDX(2.9.3)
|
||||
* Unlock database by device credentials (PIN/Password/Pattern) #779 #102
|
||||
* Advanced unlock with timeout #102 #437 #566
|
||||
* Remove default database parameter when the file is no longer accessible #803
|
||||
* Move OTP button to the first view level in Magikeyboard #587
|
||||
* Tooltips for Magikeyboard #586
|
||||
* Fix small bugs #805
|
||||
|
||||
KeePassDX(2.9.2)
|
||||
* Managing OTP links from QR applications #556
|
||||
* Prevent manual creation of existing field name #718
|
||||
* Harmonization of field names #789
|
||||
* Different channels for each type of notification #688
|
||||
* Fix OTP #780 #781
|
||||
* Fix same save shared info #783
|
||||
* Fix switch back to previous keyboard #782
|
||||
* Fix read only #792
|
||||
* Better UI #719 #534 #617 #793
|
||||
|
||||
KeePassDX(2.9.1)
|
||||
* Copy password from generator #697
|
||||
* Fix Magikeyboard not fully visible #772
|
||||
* Fix change font size #770
|
||||
* Fix crash #774
|
||||
* Small fixes #771
|
||||
|
||||
KeePassDX(2.9)
|
||||
* Upgrade to Android API 30 #723
|
||||
* Save new credentials with Autofill #524
|
||||
* Setting to close database after Autofill selection #755
|
||||
* Setting to switch keyboard when database is locked #625
|
||||
* Fix biometric issues #724 #740 #731
|
||||
* Fix autofill #725 #551
|
||||
* Fix subdomain search #728
|
||||
* Fix backup search #759
|
||||
* Small fixes and translations #732 #736 #737 #738 #742 #767
|
||||
|
||||
KeePassDX(2.8.7)
|
||||
* Downgrade to Android API 29 (crash on startup with API 30 on some devices)
|
||||
Sorry for the inconvenience
|
||||
|
||||
KeePassDX(2.8.6)
|
||||
* Fix Autofill recognition #712
|
||||
* Keep value after renaming custom field #709
|
||||
* Prevent random binary bug #713
|
||||
* Fix dialog background #717
|
||||
* Better domain recognition for autofill #702
|
||||
* Write custom data #651
|
||||
* Fix autolink #720
|
||||
|
||||
KeePassDX(2.8.5)
|
||||
* Fix Base 64 #708
|
||||
|
||||
KeePassDX(2.8.4)
|
||||
* Fix incomplete attachment deletion #684
|
||||
* Fix opening database v1 without backup folder #692
|
||||
* Fix ANR during first entry education #685
|
||||
* Entry edition as fragment and manual views to fix focus #686
|
||||
* Fix opening database with corrupted attachment #691
|
||||
* Manage empty keyfile #679
|
||||
|
||||
KeePassDX(2.8.3)
|
||||
* Upload attachments
|
||||
* Visibility button for each hidden field
|
||||
* Fix read header file
|
||||
* Fix deletion in KDB database
|
||||
* Fix minor issues
|
||||
|
||||
KeePassDX(2.8.2)
|
||||
* Fix themes / new UI
|
||||
* Fix multiples notifications
|
||||
|
||||
202
LICENSES/LICENSE_APACHE
Normal file
202
LICENSES/LICENSE_APACHE
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
7
LICENSES/LICENSE_BOUNCY_CASTLE
Normal file
7
LICENSES/LICENSE_BOUNCY_CASTLE
Normal file
@@ -0,0 +1,7 @@
|
||||
Copyright (c) 2000 - 2011 The Legion Of The Bouncy Castle (http://www.bouncycastle.org)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
111
LICENSES/LICENSE_ICON_PACK_CLASSIC
Normal file
111
LICENSES/LICENSE_ICON_PACK_CLASSIC
Normal file
@@ -0,0 +1,111 @@
|
||||
Files ic00.png to ic61.png under res/drawable, res/drawable-hdpi and res/drawable-ldpi
|
||||
|
||||
TITLE: NUVOLA ICON THEME for KDE 3.x
|
||||
AUTHOR: David Vignoni | ICON KING
|
||||
SITE: http://www.icon-king.com
|
||||
MAILING LIST: http://mail.icon-king.com/mailman/listinfo/nuvola_icon-king.com
|
||||
|
||||
Copyright (c) 2003-2004 David Vignoni.
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation,
|
||||
version 2.1 of the License.
|
||||
This library 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
|
||||
Lesser General Public License for more details.
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library (see the the license.txt file); if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#######**** NOTE THIS ADD-ON ****#######
|
||||
The GNU Lesser General Public License or LGPL is written for software libraries
|
||||
in the first place. The LGPL has to be considered valid for this artwork
|
||||
library too.
|
||||
Nuvola icon theme for KDE 3.x is a special kind of software library, it is an
|
||||
artwork library, it's elements can be used in a Graphical User Interface, or
|
||||
GUI.
|
||||
Source code, for this library means:
|
||||
- raster png image* .
|
||||
The LGPL in some sections obliges you to make the files carry
|
||||
notices. With images this is in some cases impossible or hardly usefull.
|
||||
With this library a notice is placed at a prominent place in the directory
|
||||
containing the elements. You may follow this practice.
|
||||
The exception in section 6 of the GNU Lesser General Public License covers
|
||||
the use of elements of this art library in a GUI.
|
||||
dave [at] icon-king.com
|
||||
|
||||
Date: 15 october 2004
|
||||
Version: 1.0
|
||||
|
||||
DESCRIPTION:
|
||||
|
||||
Icon theme for KDE 3.x.
|
||||
Icons where designed using Adobe Illustrator, and then exported to PNG format.
|
||||
Icons shadows and minor corrections were done using Adobe Photoshop.
|
||||
Kiconedit was used to correct some 16x16 and 22x22 icons.
|
||||
|
||||
LICENSE
|
||||
|
||||
Released under GNU Lesser General Public License (LGPL)
|
||||
Look at the license.txt file.
|
||||
|
||||
CONTACT
|
||||
|
||||
David Vignoni
|
||||
e-mail : david [at] icon-king.com
|
||||
ICQ : 117761009
|
||||
http: http://www.icon-king.com
|
||||
|
||||
---
|
||||
|
||||
Files ic62.png under res/drawable, res/drawable-hdpi and res/drawable-ldpi
|
||||
|
||||
Based on http://de.wikipedia.org/w/index.php?title=Datei:Tux.svg&filetimestamp=20090927073505
|
||||
|
||||
The copyright holder of this file allows anyone to use it for any purpose,
|
||||
provided that the copyright holders Larry Ewing, Simon Budig and
|
||||
Anja Gerwinski are mentioned.
|
||||
|
||||
---
|
||||
|
||||
Files ic63.png under res/drawable, res/drawable-hdpi and res/drawable-ldpi
|
||||
|
||||
Based on http://en.wikipedia.org/wiki/File:ASF-logo.svg
|
||||
|
||||
Apache logo
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License. You may
|
||||
obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations under
|
||||
the License.
|
||||
|
||||
---
|
||||
|
||||
Files ic64.png under res/drawable, res/drawable-hdpi and res/drawable-ldpi
|
||||
|
||||
Created by Jeremy JAMET and licensed under the terms of the GPLv3.
|
||||
|
||||
---
|
||||
|
||||
Files ic65.png, ic67.png and ic68.png under res/drawable, res/drawable-hdpi and res/drawable-ldpi
|
||||
|
||||
Created by Tobias Selig and licensed under the terms of the GPLv2 or GPLv3.
|
||||
|
||||
---
|
||||
|
||||
File ic66.png under under res/drawable, res/drawable-hdpi and res/drawable-ldpi
|
||||
|
||||
Based on http://commons.wikimedia.org/wiki/File:Dollar_symbol_gold.svg
|
||||
|
||||
Author: Rugby471
|
||||
|
||||
Permission is granted to copy, distribute and/or modify this document under
|
||||
the terms of the GNU Free Documentation License, Version 1.2 or any later
|
||||
version published by the Free Software Foundation; with no Invariant Sections,
|
||||
no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is
|
||||
included in the section entitled "GNU Free Documentation License".
|
||||
|
||||
---
|
||||
373
LICENSES/LICENSE_MOZILLA_COMPONENTS
Normal file
373
LICENSES/LICENSE_MOZILLA_COMPONENTS
Normal file
@@ -0,0 +1,373 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Android KeepassDX
|
||||
# Android KeePassDX
|
||||
|
||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> KeepassDX is a **multi-format KeePass manager for Android devices**. The app allows creating keys and passwords in a secure way by integrating with the Android design standards.
|
||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> KeePassDX is a **multi-format KeePass manager for Android devices**. The app allows creating keys and passwords in a secure way by integrating with the Android design standards.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
- Precise management of **settings**.
|
||||
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
|
||||
|
||||
KeepassDX is **open source** and **ad-free**.
|
||||
KeePassDX is **open source** and **ad-free**.
|
||||
|
||||
## What is KeePassDX?
|
||||
|
||||
@@ -88,4 +88,4 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*This project is a fork of [KeepassDroid](https://github.com/bpellin/keepassdroid) by bpellin.*
|
||||
*This project is a fork of [KeePassDroid](https://github.com/bpellin/keepassdroid) by bpellin.*
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
theme: jekyll-theme-cayman
|
||||
@@ -4,22 +4,23 @@ apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion '29.0.3'
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion '30.0.2'
|
||||
ndkVersion '21.3.6528147'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 29
|
||||
versionCode = 38
|
||||
versionName = "2.8.2"
|
||||
targetSdkVersion 30
|
||||
versionCode = 47
|
||||
versionName = "2.9.3"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
testInstrumentationRunner = "android.test.InstrumentationTestRunner"
|
||||
|
||||
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
|
||||
manifestPlaceholders = [ googleAndroidBackupAPIKey:"" ]
|
||||
manifestPlaceholders = [ googleAndroidBackupAPIKey:"unused" ]
|
||||
|
||||
kapt {
|
||||
arguments {
|
||||
@@ -96,15 +97,15 @@ def room_version = "2.2.5"
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.biometric:biometric:1.0.1'
|
||||
implementation 'androidx.biometric:biometric:1.1.0-rc01'
|
||||
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||
implementation "androidx.core:core-ktx:1.3.1"
|
||||
implementation "androidx.core:core-ktx:1.3.2"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||
// To upgrade with style
|
||||
// WARNING: To upgrade with style, bug in edit text
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
// Database
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
@@ -114,7 +115,7 @@ dependencies {
|
||||
// Time
|
||||
implementation 'joda-time:joda-time:2.10.6'
|
||||
// Color
|
||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.3'
|
||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4'
|
||||
// Education
|
||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
|
||||
// Apache Commons Collections
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108"
|
||||
android:width="108dp"
|
||||
@@ -10,10 +11,20 @@ android:height="108dp">
|
||||
android:translateY="332">
|
||||
<path
|
||||
android:pathData="M65.728516 32.791016L58.052734 35.904297 56.173828 48.380859 35.306641 69.267578 35.238281 73.759766 69.478516 108 108 108 108 70.810547 73.09375 35.904297 65.728516 32.791016Z"
|
||||
android:fillColor="@color/long_shadow"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeMiterLimit="4" />
|
||||
android:strokeMiterLimit="4" >
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endColor="#0000"
|
||||
android:endX="80"
|
||||
android:endY="80"
|
||||
android:startColor="#4e000000"
|
||||
android:startX="0"
|
||||
android:startY="0"
|
||||
android:type="linear"/>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</group>
|
||||
<group
|
||||
android:scaleX="0.3939503"
|
||||
|
||||
BIN
app/src/free/res/drawable/ic_launcher_foreground.png
Normal file
BIN
app/src/free/res/drawable/ic_launcher_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108"
|
||||
android:width="108dp"
|
||||
@@ -10,10 +11,20 @@ android:height="108dp">
|
||||
android:translateY="332">
|
||||
<path
|
||||
android:pathData="M65.728516 32.791016L58.052734 35.904297 56.173828 48.380859 35.306641 69.267578 35.238281 73.759766 69.478516 108 108 108 108 70.810547 73.09375 35.904297 65.728516 32.791016Z"
|
||||
android:fillColor="@color/long_shadow"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeMiterLimit="4" />
|
||||
android:strokeMiterLimit="4" >
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endColor="#0000"
|
||||
android:endX="80"
|
||||
android:endY="80"
|
||||
android:startColor="#4e000000"
|
||||
android:startX="0"
|
||||
android:startY="0"
|
||||
android:type="linear"/>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</group>
|
||||
<group
|
||||
android:scaleX="0.3939503"
|
||||
|
||||
BIN
app/src/libre/res/drawable/ic_launcher_foreground.png
Normal file
BIN
app/src/libre/res/drawable/ic_launcher_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
@@ -16,6 +16,8 @@
|
||||
android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"/>
|
||||
|
||||
<application
|
||||
android:label="@string/app_name"
|
||||
@@ -47,7 +49,7 @@
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.PasswordActivity"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
android:windowSoftInputMode="adjustResize|stateUnchanged">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -148,6 +150,13 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="otpauth" android:host="totp" />
|
||||
<data android:scheme="otpauth" android:host="hotp" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.MagikeyboardLauncherActivity"
|
||||
@@ -165,13 +174,17 @@
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".notifications.AttachmentFileNotificationService"
|
||||
android:name="com.kunzisoft.keepass.notifications.AttachmentFileNotificationService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<!-- Receiver for Autofill -->
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
|
||||
|
||||
BIN
app/src/main/assets/publicsuffixes
Normal file
BIN
app/src/main/assets/publicsuffixes
Normal file
Binary file not shown.
@@ -30,39 +30,75 @@ import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.autofill.KeeAutofillService
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
class AutofillLauncherActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
// Retrieve selection mode
|
||||
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
|
||||
when (specialMode) {
|
||||
SpecialMode.SELECTION -> {
|
||||
// Build search param
|
||||
val searchInfo = SearchInfo().apply {
|
||||
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
|
||||
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
|
||||
webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME)
|
||||
}
|
||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
launchSelection(searchInfo)
|
||||
}
|
||||
}
|
||||
SpecialMode.REGISTRATION -> {
|
||||
// To register info
|
||||
val registerInfo = intent.getParcelableExtra<RegisterInfo>(KEY_REGISTER_INFO)
|
||||
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
launchRegistration(searchInfo, registerInfo)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Not an autofill call
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun launchSelection(searchInfo: SearchInfo) {
|
||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
||||
val assistStructure = AutofillHelper.retrieveAssistStructure(intent)
|
||||
|
||||
if (assistStructure == null) {
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
} else if (!KeeAutofillService.searchAllowedFor(searchInfo.applicationId,
|
||||
} else if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
|
||||
PreferencesUtil.applicationIdBlocklist(this))
|
||||
|| !KeeAutofillService.searchAllowedFor(searchInfo.webDomain,
|
||||
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
|
||||
PreferencesUtil.webDomainBlocklist(this))) {
|
||||
// If item not allowed, show a toast
|
||||
Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show()
|
||||
showBlockRestartMessage()
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
} else {
|
||||
val database = Database.getInstance()
|
||||
val readOnly = database.isReadOnly
|
||||
// If database is open
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
Database.getInstance(),
|
||||
@@ -75,9 +111,10 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
||||
{
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForAutofillResult(this,
|
||||
readOnly,
|
||||
assistStructure,
|
||||
false,
|
||||
searchInfo)
|
||||
searchInfo,
|
||||
false)
|
||||
},
|
||||
{
|
||||
// If database not open
|
||||
@@ -87,12 +124,66 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
private fun launchRegistration(searchInfo: SearchInfo, registerInfo: RegisterInfo?) {
|
||||
if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
|
||||
PreferencesUtil.applicationIdBlocklist(this))
|
||||
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
|
||||
PreferencesUtil.webDomainBlocklist(this))) {
|
||||
showBlockRestartMessage()
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
} else {
|
||||
val database = Database.getInstance()
|
||||
val readOnly = database.isReadOnly
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
database,
|
||||
searchInfo,
|
||||
{ _ ->
|
||||
if (!readOnly) {
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForRegistration(this,
|
||||
registerInfo)
|
||||
} else {
|
||||
showReadOnlySaveMessage()
|
||||
}
|
||||
},
|
||||
{
|
||||
if (!readOnly) {
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForRegistration(this,
|
||||
registerInfo)
|
||||
} else {
|
||||
showReadOnlySaveMessage()
|
||||
}
|
||||
},
|
||||
{
|
||||
// If database not open
|
||||
FileDatabaseSelectActivity.launchForRegistration(this,
|
||||
registerInfo)
|
||||
}
|
||||
)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun showBlockRestartMessage() {
|
||||
// If item not allowed, show a toast
|
||||
Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
private fun showReadOnlySaveMessage() {
|
||||
Toast.makeText(this.applicationContext, R.string.autofill_read_only_save, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||
|
||||
if (PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
|
||||
// Close the database
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
@@ -100,8 +191,11 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
||||
|
||||
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
|
||||
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
|
||||
private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME"
|
||||
|
||||
fun getAuthIntentSenderForResponse(context: Context,
|
||||
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
|
||||
|
||||
fun getAuthIntentSenderForSelection(context: Context,
|
||||
searchInfo: SearchInfo? = null): IntentSender {
|
||||
return PendingIntent.getActivity(context, 0,
|
||||
// Doesn't work with Parcelable (don't know why?)
|
||||
@@ -109,9 +203,29 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
||||
searchInfo?.let {
|
||||
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
|
||||
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
|
||||
putExtra(KEY_SEARCH_SCHEME, it.webScheme)
|
||||
}
|
||||
},
|
||||
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
||||
}
|
||||
|
||||
fun getAuthIntentSenderForRegistration(context: Context,
|
||||
registerInfo: RegisterInfo): IntentSender {
|
||||
return PendingIntent.getActivity(context, 0,
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
||||
putExtra(KEY_REGISTER_INFO, registerInfo)
|
||||
},
|
||||
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
||||
}
|
||||
|
||||
fun launchForRegistration(context: Context,
|
||||
registerInfo: RegisterInfo) {
|
||||
val intent = Intent(context, AutofillLauncherActivity::class.java)
|
||||
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
|
||||
intent.putExtra(KEY_REGISTER_INFO, registerInfo)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.graphics.drawable.ColorDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
@@ -39,14 +40,15 @@ import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryAttachment
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
|
||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||
@@ -86,7 +88,7 @@ class EntryActivity : LockingActivity() {
|
||||
private var mShowPassword: Boolean = false
|
||||
|
||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||
private var mAttachmentsToDownload: HashMap<Int, EntryAttachment> = HashMap()
|
||||
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
|
||||
|
||||
private var clipboardHelper: ClipboardHelper? = null
|
||||
private var mFirstLaunchOfActivity: Boolean = false
|
||||
@@ -212,8 +214,8 @@ class EntryActivity : LockingActivity() {
|
||||
mAttachmentFileBinderManager?.apply {
|
||||
registerProgressTask()
|
||||
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
||||
override fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment) {
|
||||
entryContentsView?.updateAttachmentDownloadProgress(attachment)
|
||||
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
||||
entryContentsView?.putAttachment(entryAttachmentState)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,14 +242,13 @@ class EntryActivity : LockingActivity() {
|
||||
toolbar?.title = entryTitle
|
||||
|
||||
// Assign basic fields
|
||||
entryContentsView?.assignUserName(entry.username)
|
||||
entryContentsView?.assignUserNameCopyListener(View.OnClickListener {
|
||||
entryContentsView?.assignUserName(entry.username) {
|
||||
database.startManageEntry(entry)
|
||||
clipboardHelper?.timeoutCopyToClipboard(entry.username,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_user_name)))
|
||||
database.stopManageEntry(entry)
|
||||
})
|
||||
}
|
||||
|
||||
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
||||
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
|
||||
@@ -274,23 +275,25 @@ class EntryActivity : LockingActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
entryContentsView?.assignPassword(entry.password, allowCopyPasswordAndProtectedFields)
|
||||
if (allowCopyPasswordAndProtectedFields) {
|
||||
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
|
||||
val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) {
|
||||
View.OnClickListener {
|
||||
database.startManageEntry(entry)
|
||||
clipboardHelper?.timeoutCopyToClipboard(entry.password,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_password)))
|
||||
database.stopManageEntry(entry)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// If dialog not already shown
|
||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||
entryContentsView?.assignPasswordCopyListener(showWarningClipboardDialogOnClickListener)
|
||||
showWarningClipboardDialogOnClickListener
|
||||
} else {
|
||||
entryContentsView?.assignPasswordCopyListener(null)
|
||||
null
|
||||
}
|
||||
}
|
||||
entryContentsView?.assignPassword(entry.password,
|
||||
allowCopyPasswordAndProtectedFields,
|
||||
onPasswordCopyClickListener)
|
||||
|
||||
//Assign OTP field
|
||||
entryContentsView?.assignOtp(entry.getOtpElement(), entryProgress,
|
||||
@@ -304,20 +307,22 @@ class EntryActivity : LockingActivity() {
|
||||
})
|
||||
|
||||
entryContentsView?.assignURL(entry.url)
|
||||
entryContentsView?.assignComment(entry.notes)
|
||||
entryContentsView?.assignNotes(entry.notes)
|
||||
|
||||
// Assign custom fields
|
||||
if (entry.allowCustomFields()) {
|
||||
if (mDatabase?.allowEntryCustomFields() == true) {
|
||||
entryContentsView?.clearExtraFields()
|
||||
for ((label, value) in entry.customFields) {
|
||||
entry.getExtraFields().forEach { field ->
|
||||
val label = field.name
|
||||
val value = field.protectedValue
|
||||
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
||||
if (allowCopyProtectedField) {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, View.OnClickListener {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
|
||||
clipboardHelper?.timeoutCopyToClipboard(
|
||||
value.toString(),
|
||||
getString(R.string.copy_field, label)
|
||||
)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// If dialog not already shown
|
||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||
@@ -328,20 +333,15 @@ class EntryActivity : LockingActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
|
||||
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
|
||||
|
||||
// Manage attachments
|
||||
entryContentsView?.assignAttachments(entry.getAttachments()) { attachmentItem ->
|
||||
when (attachmentItem.downloadState) {
|
||||
AttachmentState.NULL, AttachmentState.ERROR, AttachmentState.COMPLETE -> {
|
||||
mDatabase?.binaryPool?.let { binaryPool ->
|
||||
entryContentsView?.assignAttachments(entry.getAttachments(binaryPool).toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
|
||||
createDocument(this, attachmentItem.name)?.let { requestCode ->
|
||||
mAttachmentsToDownload[requestCode] = attachmentItem
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// TODO Stop download
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assign dates
|
||||
@@ -393,16 +393,6 @@ class EntryActivity : LockingActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeShowPasswordIcon(togglePassword: MenuItem?) {
|
||||
if (mShowPassword) {
|
||||
togglePassword?.setTitle(R.string.menu_hide_password)
|
||||
togglePassword?.setIcon(R.drawable.ic_visibility_off_white_24dp)
|
||||
} else {
|
||||
togglePassword?.setTitle(R.string.menu_showpass)
|
||||
togglePassword?.setIcon(R.drawable.ic_visibility_white_24dp)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
@@ -418,15 +408,6 @@ class EntryActivity : LockingActivity() {
|
||||
menu.findItem(R.id.menu_edit)?.isVisible = false
|
||||
}
|
||||
|
||||
val togglePassword = menu.findItem(R.id.menu_toggle_pass)
|
||||
entryContentsView?.let {
|
||||
if (it.isPasswordPresent || it.atLeastOneFieldProtectedPresent()) {
|
||||
changeShowPasswordIcon(togglePassword)
|
||||
} else {
|
||||
togglePassword?.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
val gotoUrl = menu.findItem(R.id.menu_goto_url)
|
||||
gotoUrl?.apply {
|
||||
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
|
||||
@@ -442,35 +423,38 @@ class EntryActivity : LockingActivity() {
|
||||
}
|
||||
|
||||
// Show education views
|
||||
Handler().post { performedNextEducation(EntryActivityEducation(this), menu) }
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(EntryActivityEducation(this), menu) }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
||||
menu: Menu) {
|
||||
val entryCopyEducationPerformed = entryContentsView?.isUserNamePresent == true
|
||||
val entryFieldCopyView = entryContentsView?.firstEntryFieldCopyView()
|
||||
val entryCopyEducationPerformed = entryFieldCopyView != null
|
||||
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
||||
findViewById(R.id.entry_user_name_action_image),
|
||||
entryFieldCopyView,
|
||||
{
|
||||
clipboardHelper?.timeoutCopyToClipboard(mEntry!!.username,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_user_name)))
|
||||
val appNameString = getString(R.string.app_name)
|
||||
clipboardHelper?.timeoutCopyToClipboard(appNameString,
|
||||
getString(R.string.copy_field, appNameString))
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryActivityEducation, menu)
|
||||
})
|
||||
|
||||
if (!entryCopyEducationPerformed) {
|
||||
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
|
||||
// entryEditEducationPerformed
|
||||
toolbar?.findViewById<View>(R.id.menu_edit) != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
|
||||
toolbar!!.findViewById(R.id.menu_edit),
|
||||
menuEditView != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
|
||||
menuEditView,
|
||||
{
|
||||
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryActivityEducation, menu)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,12 +464,6 @@ class EntryActivity : LockingActivity() {
|
||||
MenuUtil.onContributionItemSelected(this)
|
||||
return true
|
||||
}
|
||||
R.id.menu_toggle_pass -> {
|
||||
mShowPassword = !mShowPassword
|
||||
changeShowPasswordIcon(item)
|
||||
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
|
||||
return true
|
||||
}
|
||||
R.id.menu_edit -> {
|
||||
mEntry?.let {
|
||||
EntryEditActivity.launch(this@EntryActivity, it)
|
||||
|
||||
@@ -21,47 +21,60 @@ package com.kunzisoft.keepass.activities
|
||||
import android.app.Activity
|
||||
import android.app.DatePickerDialog
|
||||
import android.app.TimePickerDialog
|
||||
import android.app.assist.AssistStructure
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.DatePicker
|
||||
import android.widget.TimePicker
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.*
|
||||
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||
import com.kunzisoft.keepass.model.Field
|
||||
import com.kunzisoft.keepass.model.FocusedEditField
|
||||
import com.kunzisoft.keepass.model.*
|
||||
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
|
||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.view.EntryEditContentsView
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.view.showActionError
|
||||
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
||||
import org.joda.time.DateTime
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class EntryEditActivity : LockingActivity(),
|
||||
IconPickerDialogFragment.IconPickerListener,
|
||||
@@ -69,30 +82,37 @@ class EntryEditActivity : LockingActivity(),
|
||||
GeneratePasswordDialogFragment.GeneratePasswordListener,
|
||||
SetOTPDialogFragment.CreateOtpListener,
|
||||
DatePickerDialog.OnDateSetListener,
|
||||
TimePickerDialog.OnTimeSetListener {
|
||||
TimePickerDialog.OnTimeSetListener,
|
||||
FileTooBigDialogFragment.ActionChooseListener,
|
||||
ReplaceFileDialogFragment.ActionChooseListener {
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
|
||||
// Refs of an entry and group in database, are not modifiable
|
||||
private var mEntry: Entry? = null
|
||||
private var mParent: Group? = null
|
||||
// New or copy of mEntry in the database to be modifiable
|
||||
private var mNewEntry: Entry? = null
|
||||
private var mIsNew: Boolean = false
|
||||
|
||||
// Views
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var scrollView: NestedScrollView? = null
|
||||
private var entryEditContentsView: EntryEditContentsView? = null
|
||||
private var entryEditFragment: EntryEditFragment? = null
|
||||
private var entryEditAddToolBar: Toolbar? = null
|
||||
private var validateButton: View? = null
|
||||
private var lockView: View? = null
|
||||
|
||||
private var mFocusedEditExtraField: FocusedEditField? = null
|
||||
// To manage attachments
|
||||
private var mSelectFileHelper: SelectFileHelper? = null
|
||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||
private var mAllowMultipleAttachments: Boolean = false
|
||||
private var mTempAttachments = ArrayList<Attachment>()
|
||||
|
||||
// Education
|
||||
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
||||
|
||||
// To ask data lost only one time
|
||||
private var backPressedAlreadyApproved = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_entry_edit)
|
||||
@@ -108,22 +128,6 @@ class EntryEditActivity : LockingActivity(),
|
||||
scrollView = findViewById(R.id.entry_edit_scroll)
|
||||
scrollView?.scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
||||
|
||||
entryEditContentsView = findViewById(R.id.entry_edit_contents)
|
||||
entryEditContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
||||
entryEditContentsView?.onDateClickListener = View.OnClickListener {
|
||||
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
|
||||
val dateTime = DateTime(expiresDate)
|
||||
val defaultYear = dateTime.year
|
||||
val defaultMonth = dateTime.monthOfYear-1
|
||||
val defaultDay = dateTime.dayOfMonth
|
||||
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
|
||||
.show(supportFragmentManager, "DatePickerFragment")
|
||||
}
|
||||
}
|
||||
entryEditContentsView?.entryPasswordGeneratorView?.setOnClickListener {
|
||||
openPasswordGenerator()
|
||||
}
|
||||
|
||||
lockView = findViewById(R.id.lock_button)
|
||||
lockView?.setOnClickListener {
|
||||
lockAndExit()
|
||||
@@ -138,6 +142,8 @@ class EntryEditActivity : LockingActivity(),
|
||||
// Likely the app has been killed exit the activity
|
||||
mDatabase = Database.getInstance()
|
||||
|
||||
var tempEntryInfo: EntryInfo? = null
|
||||
|
||||
// Entry is retrieve, it's an entry to update
|
||||
intent.getParcelableExtra<NodeId<UUID>>(KEY_ENTRY)?.let {
|
||||
mIsNew = false
|
||||
@@ -153,84 +159,105 @@ class EntryEditActivity : LockingActivity(),
|
||||
entry.parent = mParent
|
||||
}
|
||||
}
|
||||
|
||||
// Create the new entry from the current one
|
||||
if (savedInstanceState?.containsKey(KEY_NEW_ENTRY) != true) {
|
||||
mEntry?.let { entry ->
|
||||
// Create a copy to modify
|
||||
mNewEntry = Entry(entry).also { newEntry ->
|
||||
// WARNING Remove the parent to keep memory with parcelable
|
||||
newEntry.removeParent()
|
||||
}
|
||||
}
|
||||
}
|
||||
tempEntryInfo = mEntry?.getEntryInfo(mDatabase, true)
|
||||
}
|
||||
|
||||
// Parent is retrieve, it's a new entry to create
|
||||
intent.getParcelableExtra<NodeId<*>>(KEY_PARENT)?.let {
|
||||
mIsNew = true
|
||||
// Create an empty new entry
|
||||
if (savedInstanceState?.containsKey(KEY_NEW_ENTRY) != true) {
|
||||
mNewEntry = mDatabase?.createEntry()
|
||||
}
|
||||
mParent = mDatabase?.getGroupById(it)
|
||||
// Add the default icon from parent if not a folder
|
||||
val parentIcon = mParent?.icon
|
||||
tempEntryInfo = mDatabase?.createEntry()?.getEntryInfo(mDatabase, true)
|
||||
// Set default icon
|
||||
if (parentIcon != null
|
||||
&& parentIcon.iconId != IconImage.UNKNOWN_ID
|
||||
&& parentIcon.iconId != IconImageStandard.FOLDER) {
|
||||
temporarilySaveAndShowSelectedIcon(parentIcon)
|
||||
} else {
|
||||
mDatabase?.drawFactory?.let { iconFactory ->
|
||||
entryEditContentsView?.setDefaultIcon(iconFactory)
|
||||
tempEntryInfo?.icon = parentIcon
|
||||
}
|
||||
// Set default username
|
||||
tempEntryInfo?.username = mDatabase?.defaultUsername ?: ""
|
||||
}
|
||||
|
||||
// Retrieve data from registration
|
||||
val registerInfo = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)
|
||||
val searchInfo: SearchInfo? = registerInfo?.searchInfo
|
||||
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
||||
registerInfo?.username?.let {
|
||||
tempEntryInfo?.username = it
|
||||
}
|
||||
registerInfo?.password?.let {
|
||||
tempEntryInfo?.password = it
|
||||
}
|
||||
searchInfo?.let { tempSearchInfo ->
|
||||
tempEntryInfo?.saveSearchInfo(mDatabase, tempSearchInfo)
|
||||
}
|
||||
|
||||
// Build fragment to manage entry modification
|
||||
entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment?
|
||||
if (entryEditFragment == null) {
|
||||
entryEditFragment = EntryEditFragment.getInstance(tempEntryInfo)
|
||||
}
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.entry_edit_contents, entryEditFragment!!, ENTRY_EDIT_FRAGMENT_TAG)
|
||||
.commit()
|
||||
entryEditFragment?.apply {
|
||||
drawFactory = mDatabase?.drawFactory
|
||||
setOnDateClickListener = View.OnClickListener {
|
||||
expiryTime.date.let { expiresDate ->
|
||||
val dateTime = DateTime(expiresDate)
|
||||
val defaultYear = dateTime.year
|
||||
val defaultMonth = dateTime.monthOfYear-1
|
||||
val defaultDay = dateTime.dayOfMonth
|
||||
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
|
||||
.show(supportFragmentManager, "DatePickerFragment")
|
||||
}
|
||||
}
|
||||
setOnPasswordGeneratorClickListener = View.OnClickListener {
|
||||
openPasswordGenerator()
|
||||
}
|
||||
// Add listener to the icon
|
||||
setOnIconViewClickListener = View.OnClickListener {
|
||||
IconPickerDialogFragment.launch(this@EntryEditActivity)
|
||||
}
|
||||
setOnRemoveAttachment = { attachment ->
|
||||
mAttachmentFileBinderManager?.removeBinaryAttachment(attachment)
|
||||
removeAttachment(EntryAttachmentState(attachment, StreamDirection.DOWNLOAD))
|
||||
}
|
||||
setOnEditCustomField = { field ->
|
||||
editCustomField(field)
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve the new entry after an orientation change
|
||||
if (savedInstanceState?.containsKey(KEY_NEW_ENTRY) == true) {
|
||||
mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY)
|
||||
// Retrieve temp attachments in case of deletion
|
||||
if (savedInstanceState?.containsKey(TEMP_ATTACHMENTS) == true) {
|
||||
mTempAttachments = savedInstanceState.getParcelableArrayList(TEMP_ATTACHMENTS) ?: mTempAttachments
|
||||
}
|
||||
|
||||
if (savedInstanceState?.containsKey(EXTRA_FIELD_FOCUSED_ENTRY) == true) {
|
||||
mFocusedEditExtraField = savedInstanceState.getParcelable(EXTRA_FIELD_FOCUSED_ENTRY)
|
||||
}
|
||||
|
||||
// Close the activity if entry or parent can't be retrieve
|
||||
if (mNewEntry == null || mParent == null) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
populateViewsWithEntry(mNewEntry!!)
|
||||
|
||||
// Assign title
|
||||
title = if (mIsNew) getString(R.string.add_entry) else getString(R.string.edit_entry)
|
||||
|
||||
// Add listener to the icon
|
||||
entryEditContentsView?.setOnIconViewClickListener { IconPickerDialogFragment.launch(this@EntryEditActivity) }
|
||||
|
||||
// Bottom Bar
|
||||
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
|
||||
entryEditAddToolBar?.apply {
|
||||
menuInflater.inflate(R.menu.entry_edit, menu)
|
||||
|
||||
menu.findItem(R.id.menu_add_field).apply {
|
||||
val allowLock = PreferencesUtil.showLockDatabaseButton(context)
|
||||
isEnabled = allowLock
|
||||
isVisible = allowLock
|
||||
}
|
||||
|
||||
menu.findItem(R.id.menu_add_field).apply {
|
||||
val allowCustomField = mNewEntry?.allowCustomFields() == true
|
||||
val allowCustomField = mDatabase?.allowEntryCustomFields() == true
|
||||
isEnabled = allowCustomField
|
||||
isVisible = allowCustomField
|
||||
}
|
||||
|
||||
// Attachment not compatible below KitKat
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||
menu.findItem(R.id.menu_add_attachment).isVisible = false
|
||||
}
|
||||
|
||||
menu.findItem(R.id.menu_add_otp).apply {
|
||||
val allowOTP = mDatabase?.allowOTP == true
|
||||
isEnabled = allowOTP
|
||||
isVisible = allowOTP
|
||||
// OTP not compatible below KitKat
|
||||
isVisible = allowOTP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
||||
}
|
||||
|
||||
setOnMenuItemClickListener { item ->
|
||||
@@ -239,6 +266,10 @@ class EntryEditActivity : LockingActivity(),
|
||||
addNewCustomField()
|
||||
true
|
||||
}
|
||||
R.id.menu_add_attachment -> {
|
||||
addNewAttachment(item)
|
||||
true
|
||||
}
|
||||
R.id.menu_add_otp -> {
|
||||
setupOTP()
|
||||
true
|
||||
@@ -248,6 +279,10 @@ class EntryEditActivity : LockingActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
// To retrieve attachment
|
||||
mSelectFileHelper = SelectFileHelper(this)
|
||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||
|
||||
// Save button
|
||||
validateButton = findViewById(R.id.entry_edit_validate)
|
||||
validateButton?.setOnClickListener { saveEntry() }
|
||||
@@ -260,14 +295,83 @@ class EntryEditActivity : LockingActivity(),
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_CREATE_ENTRY_TASK,
|
||||
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||
if (result.isSuccess)
|
||||
finish()
|
||||
try {
|
||||
if (result.isSuccess) {
|
||||
var newNodes: List<Node> = ArrayList()
|
||||
result.data?.getBundle(DatabaseTaskNotificationService.NEW_NODES_KEY)?.let { newNodesBundle ->
|
||||
mDatabase?.let { database ->
|
||||
newNodes = DatabaseTaskNotificationService.getListNodesFromBundle(database, newNodesBundle)
|
||||
}
|
||||
}
|
||||
if (newNodes.size == 1) {
|
||||
(newNodes[0] as? Entry?)?.let { entry ->
|
||||
mEntry = entry
|
||||
EntrySelectionHelper.doSpecialAction(intent,
|
||||
{
|
||||
// Finish naturally
|
||||
finishForEntryResult()
|
||||
},
|
||||
{
|
||||
// Nothing when search retrieved
|
||||
},
|
||||
{
|
||||
entryValidatedForSave()
|
||||
},
|
||||
{
|
||||
entryValidatedForKeyboardSelection(entry)
|
||||
},
|
||||
{ _, _ ->
|
||||
entryValidatedForAutofillSelection(entry)
|
||||
},
|
||||
{
|
||||
entryValidatedForAutofillRegistration()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve entry after database action", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
coordinatorLayout?.showActionError(result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun entryValidatedForSave() {
|
||||
onValidateSpecialMode()
|
||||
finishForEntryResult()
|
||||
}
|
||||
|
||||
private fun entryValidatedForKeyboardSelection(entry: Entry) {
|
||||
// Populate Magikeyboard with entry
|
||||
mDatabase?.let { database ->
|
||||
populateKeyboardAndMoveAppToBackground(this,
|
||||
entry.getEntryInfo(database),
|
||||
intent)
|
||||
}
|
||||
onValidateSpecialMode()
|
||||
// Don't keep activity history for entry edition
|
||||
finishForEntryResult()
|
||||
}
|
||||
|
||||
private fun entryValidatedForAutofillSelection(entry: Entry) {
|
||||
// Build Autofill response with the entry selected
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
mDatabase?.let { database ->
|
||||
AutofillHelper.buildResponse(this@EntryEditActivity,
|
||||
entry.getEntryInfo(database))
|
||||
}
|
||||
}
|
||||
onValidateSpecialMode()
|
||||
}
|
||||
|
||||
private fun entryValidatedForAutofillRegistration() {
|
||||
onValidateSpecialMode()
|
||||
finishForEntryResult()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
@@ -279,69 +383,55 @@ class EntryEditActivity : LockingActivity(),
|
||||
|
||||
// Padding if lock button visible
|
||||
entryEditAddToolBar?.updateLockPaddingLeft()
|
||||
|
||||
mAllowMultipleAttachments = mDatabase?.allowMultipleAttachments == true
|
||||
mAttachmentFileBinderManager?.apply {
|
||||
registerProgressTask()
|
||||
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
||||
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
||||
when (entryAttachmentState.downloadState) {
|
||||
AttachmentState.START -> {
|
||||
entryEditFragment?.apply {
|
||||
// When only one attachment is allowed
|
||||
if (!mAllowMultipleAttachments) {
|
||||
clearAttachments()
|
||||
}
|
||||
putAttachment(entryAttachmentState)
|
||||
// Scroll to the attachment position
|
||||
getAttachmentViewPosition(entryAttachmentState) {
|
||||
scrollView?.smoothScrollTo(0, it.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
AttachmentState.IN_PROGRESS -> {
|
||||
entryEditFragment?.putAttachment(entryAttachmentState)
|
||||
}
|
||||
AttachmentState.COMPLETE -> {
|
||||
entryEditFragment?.apply {
|
||||
putAttachment(entryAttachmentState)
|
||||
// Scroll to the attachment position
|
||||
getAttachmentViewPosition(entryAttachmentState) {
|
||||
scrollView?.smoothScrollTo(0, it.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
AttachmentState.ERROR -> {
|
||||
entryEditFragment?.removeAttachment(entryAttachmentState)
|
||||
coordinatorLayout?.let {
|
||||
Snackbar.make(it, R.string.error_file_not_create, Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateViewsWithEntry(newEntry: Entry) {
|
||||
// Don't start the field reference manager, we want to see the raw ref
|
||||
mDatabase?.stopManageEntry(newEntry)
|
||||
|
||||
// Set info in temp parameters
|
||||
temporarilySaveAndShowSelectedIcon(newEntry.icon)
|
||||
|
||||
// Set info in view
|
||||
entryEditContentsView?.apply {
|
||||
title = newEntry.title
|
||||
username = if (mIsNew && newEntry.username.isEmpty())
|
||||
mDatabase?.defaultUsername ?: ""
|
||||
else
|
||||
newEntry.username
|
||||
url = newEntry.url
|
||||
password = newEntry.password
|
||||
expires = newEntry.expires
|
||||
if (expires)
|
||||
expiresDate = newEntry.expiryTime
|
||||
notes = newEntry.notes
|
||||
assignExtraFields(newEntry.customFields.mapTo(ArrayList()) {
|
||||
Field(it.key, it.value)
|
||||
}, mFocusedEditExtraField)
|
||||
assignAttachments(newEntry.getAttachments()) { attachment ->
|
||||
newEntry.removeAttachment(attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateEntryWithViews(newEntry: Entry) {
|
||||
override fun onPause() {
|
||||
mAttachmentFileBinderManager?.unregisterProgressTask()
|
||||
|
||||
mDatabase?.startManageEntry(newEntry)
|
||||
|
||||
newEntry.apply {
|
||||
// Build info from view
|
||||
entryEditContentsView?.let { entryView ->
|
||||
removeAllFields()
|
||||
title = entryView.title
|
||||
username = entryView.username
|
||||
url = entryView.url
|
||||
password = entryView.password
|
||||
expires = entryView.expires
|
||||
if (entryView.expires) {
|
||||
expiryTime = entryView.expiresDate
|
||||
}
|
||||
notes = entryView.notes
|
||||
entryView.getExtraField().forEach { customField ->
|
||||
putExtraField(customField.name, customField.protectedValue)
|
||||
}
|
||||
mFocusedEditExtraField = entryView.getExtraFieldFocused()
|
||||
}
|
||||
}
|
||||
|
||||
mDatabase?.stopManageEntry(newEntry)
|
||||
}
|
||||
|
||||
private fun temporarilySaveAndShowSelectedIcon(icon: IconImage) {
|
||||
mNewEntry?.icon = icon
|
||||
mDatabase?.drawFactory?.let { iconDrawFactory ->
|
||||
entryEditContentsView?.setIcon(iconDrawFactory, icon)
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -358,16 +448,113 @@ class EntryEditActivity : LockingActivity(),
|
||||
EntryCustomFieldDialogFragment.getInstance().show(supportFragmentManager, "customFieldDialog")
|
||||
}
|
||||
|
||||
override fun onNewCustomFieldApproved(label: String, protection: Boolean) {
|
||||
entryEditContentsView?.putExtraField(Field(label, ProtectedString(protection)))
|
||||
private fun editCustomField(field: Field) {
|
||||
EntryCustomFieldDialogFragment.getInstance(field).show(supportFragmentManager, "customFieldDialog")
|
||||
}
|
||||
|
||||
override fun onNewCustomFieldCanceled(label: String, protection: Boolean) {}
|
||||
private fun verifyNameField(field: Field,
|
||||
actionIfNewName: () -> Unit) {
|
||||
var extraFieldAlreadyContainsName = false
|
||||
entryEditFragment?.getExtraFields()?.forEach {
|
||||
if (it.name.equals(field.name, true))
|
||||
extraFieldAlreadyContainsName = true
|
||||
}
|
||||
|
||||
if (!extraFieldAlreadyContainsName
|
||||
&& Entry.newExtraFieldNameAllowed(field)) {
|
||||
actionIfNewName.invoke()
|
||||
} else {
|
||||
Log.e(TAG, "Unable to create the new field, field name already exists")
|
||||
coordinatorLayout?.let {
|
||||
Snackbar.make(it, R.string.error_field_name_already_exists, Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewCustomFieldApproved(newField: Field) {
|
||||
verifyNameField(newField) {
|
||||
entryEditFragment?.putExtraField(newField)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEditCustomFieldApproved(oldField: Field, newField: Field) {
|
||||
verifyNameField(newField) {
|
||||
entryEditFragment?.replaceExtraField(oldField, newField)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDeleteCustomFieldApproved(oldField: Field) {
|
||||
entryEditFragment?.removeExtraField(oldField)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new attachment
|
||||
*/
|
||||
private fun addNewAttachment(item: MenuItem) {
|
||||
mSelectFileHelper?.selectFileOnClickViewListener?.onMenuItemClick(item)
|
||||
}
|
||||
|
||||
override fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?) {
|
||||
if (attachmentToUploadUri != null && fileName != null) {
|
||||
buildNewAttachment(attachmentToUploadUri, fileName)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onValidateReplaceFile(attachmentToUploadUri: Uri?, attachment: Attachment?) {
|
||||
startUploadAttachment(attachmentToUploadUri, attachment)
|
||||
}
|
||||
|
||||
private fun startUploadAttachment(attachmentToUploadUri: Uri?, attachment: Attachment?) {
|
||||
if (attachmentToUploadUri != null && attachment != null) {
|
||||
// Start uploading in service
|
||||
mAttachmentFileBinderManager?.startUploadAttachment(attachmentToUploadUri, attachment)
|
||||
// Add in temp list
|
||||
mTempAttachments.add(attachment)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNewAttachment(attachmentToUploadUri: Uri, fileName: String) {
|
||||
val compression = mDatabase?.compressionForNewEntry() ?: false
|
||||
mDatabase?.buildNewBinary(UriUtil.getBinaryDir(this), compression)?.let { binaryAttachment ->
|
||||
val entryAttachment = Attachment(fileName, binaryAttachment)
|
||||
// Ask to replace the current attachment
|
||||
if ((mDatabase?.allowMultipleAttachments != true && entryEditFragment?.containsAttachment() == true) ||
|
||||
entryEditFragment?.containsAttachment(EntryAttachmentState(entryAttachment, StreamDirection.UPLOAD)) == true) {
|
||||
ReplaceFileDialogFragment.build(attachmentToUploadUri, entryAttachment)
|
||||
.show(supportFragmentManager, "replacementFileFragment")
|
||||
} else {
|
||||
startUploadAttachment(attachmentToUploadUri, entryAttachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||
uri?.let { attachmentToUploadUri ->
|
||||
// TODO Async to get the name
|
||||
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
|
||||
documentFile.name?.let { fileName ->
|
||||
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
|
||||
FileTooBigDialogFragment.build(attachmentToUploadUri, fileName)
|
||||
.show(supportFragmentManager, "fileTooBigFragment")
|
||||
} else {
|
||||
buildNewAttachment(attachmentToUploadUri, fileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up OTP (HOTP or TOTP) and add it as extra field
|
||||
*/
|
||||
private fun setupOTP() {
|
||||
// Retrieve the current otpElement if exists
|
||||
// and open the dialog to set up the OTP
|
||||
SetOTPDialogFragment.build(mEntry?.getOtpElement()?.otpModel)
|
||||
SetOTPDialogFragment.build(entryEditFragment?.getEntryInfo()?.otpModel)
|
||||
.show(supportFragmentManager, "addOTPDialog")
|
||||
}
|
||||
|
||||
@@ -375,18 +562,28 @@ class EntryEditActivity : LockingActivity(),
|
||||
* Saves the new entry or update an existing entry in the database
|
||||
*/
|
||||
private fun saveEntry() {
|
||||
// Launch a validation and show the error if present
|
||||
if (entryEditContentsView?.isValid() == true) {
|
||||
// Clone the entry
|
||||
mNewEntry?.let { newEntry ->
|
||||
// Get the temp entry
|
||||
entryEditFragment?.getEntryInfo()?.let { newEntryInfo ->
|
||||
|
||||
if (mIsNew) {
|
||||
// Create new one
|
||||
mDatabase?.createEntry()
|
||||
} else {
|
||||
// Create a clone
|
||||
Entry(mEntry!!)
|
||||
}?.let { newEntry ->
|
||||
|
||||
// WARNING Add the parent previously deleted
|
||||
newEntry.parent = mEntry?.parent
|
||||
// Build info
|
||||
newEntry.lastAccessTime = DateInstant()
|
||||
newEntry.lastModificationTime = DateInstant()
|
||||
newEntry.setEntryInfo(mDatabase, newEntryInfo)
|
||||
|
||||
populateEntryWithViews(newEntry)
|
||||
// Delete temp attachment if not used
|
||||
mTempAttachments.forEach {
|
||||
mDatabase?.binaryPool?.let { binaryPool ->
|
||||
if (!newEntry.getAttachments(binaryPool).contains(it)) {
|
||||
mDatabase?.removeAttachmentIfNotUsed(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Open a progress dialog and save entry
|
||||
if (mIsNew) {
|
||||
@@ -419,30 +616,23 @@ class EntryEditActivity : LockingActivity(),
|
||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||
|
||||
entryEditActivityEducation?.let {
|
||||
Handler().post { performedNextEducation(it) }
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
||||
val passwordGeneratorView: View? = entryEditContentsView?.entryPasswordGeneratorView
|
||||
val generatePasswordEducationPerformed = passwordGeneratorView != null
|
||||
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
||||
passwordGeneratorView,
|
||||
{
|
||||
openPasswordGenerator()
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryEditActivityEducation)
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||
entryEditActivityEducation?.let {
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(it) }
|
||||
}
|
||||
)
|
||||
if (!generatePasswordEducationPerformed) {
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
||||
if (entryEditFragment?.generatePasswordEducationPerformed(entryEditActivityEducation) != true) {
|
||||
val addNewFieldView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_field)
|
||||
val addNewFieldEducationPerformed = mNewEntry != null
|
||||
&& mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty()
|
||||
&& addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE
|
||||
val addNewFieldEducationPerformed = mDatabase?.allowEntryCustomFields() == true
|
||||
&& addNewFieldView != null
|
||||
&& addNewFieldView.isVisible
|
||||
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
|
||||
addNewFieldView,
|
||||
{
|
||||
@@ -453,13 +643,29 @@ class EntryEditActivity : LockingActivity(),
|
||||
}
|
||||
)
|
||||
if (!addNewFieldEducationPerformed) {
|
||||
val attachmentView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_attachment)
|
||||
val addAttachmentEducationPerformed = attachmentView != null
|
||||
&& attachmentView.isVisible
|
||||
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation(
|
||||
attachmentView,
|
||||
{
|
||||
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(attachmentView)
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryEditActivityEducation)
|
||||
}
|
||||
)
|
||||
if (!addAttachmentEducationPerformed) {
|
||||
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
|
||||
setupOtpView != null && setupOtpView.visibility == View.VISIBLE
|
||||
setupOtpView != null
|
||||
&& setupOtpView.isVisible
|
||||
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
|
||||
setupOtpView,
|
||||
{
|
||||
setupOTP()
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -482,16 +688,28 @@ class EntryEditActivity : LockingActivity(),
|
||||
}
|
||||
|
||||
override fun onOtpCreated(otpElement: OtpElement) {
|
||||
var titleOTP: String? = null
|
||||
var usernameOTP: String? = null
|
||||
// Build a temp entry to get title and username (by ref)
|
||||
entryEditFragment?.getEntryInfo()?.let { entryInfo ->
|
||||
val entryTemp = mDatabase?.createEntry()
|
||||
entryTemp?.setEntryInfo(mDatabase, entryInfo)
|
||||
mDatabase?.startManageEntry(entryTemp)
|
||||
titleOTP = entryTemp?.title
|
||||
usernameOTP = entryTemp?.username
|
||||
mDatabase?.stopManageEntry(mEntry)
|
||||
}
|
||||
// Update the otp field with otpauth:// url
|
||||
val otpField = OtpEntryFields.buildOtpField(otpElement,
|
||||
mEntry?.title, mEntry?.username)
|
||||
entryEditContentsView?.putExtraField(otpField)
|
||||
mEntry?.putExtraField(otpField.name, otpField.protectedValue)
|
||||
val otpField = OtpEntryFields.buildOtpField(otpElement, titleOTP, usernameOTP)
|
||||
mEntry?.putExtraField(Field(otpField.name, otpField.protectedValue))
|
||||
entryEditFragment?.apply {
|
||||
putExtraField(otpField)
|
||||
}
|
||||
}
|
||||
|
||||
override fun iconPicked(bundle: Bundle) {
|
||||
IconPickerDialogFragment.getIconStandardFromBundle(bundle)?.let { icon ->
|
||||
temporarilySaveAndShowSelectedIcon(icon)
|
||||
entryEditFragment?.icon = icon
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,9 +717,9 @@ class EntryEditActivity : LockingActivity(),
|
||||
// To fix android 4.4 issue
|
||||
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
|
||||
if (datePicker?.isShown == true) {
|
||||
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
|
||||
entryEditFragment?.expiryTime?.date?.let { expiresDate ->
|
||||
// Save the date
|
||||
entryEditContentsView?.expiresDate =
|
||||
entryEditFragment?.expiryTime =
|
||||
DateInstant(DateTime(expiresDate)
|
||||
.withYear(year)
|
||||
.withMonthOfYear(month + 1)
|
||||
@@ -518,9 +736,9 @@ class EntryEditActivity : LockingActivity(),
|
||||
}
|
||||
|
||||
override fun onTimeSet(timePicker: TimePicker?, hours: Int, minutes: Int) {
|
||||
entryEditContentsView?.expiresDate?.date?.let { expiresDate ->
|
||||
entryEditFragment?.expiryTime?.date?.let { expiresDate ->
|
||||
// Save the date
|
||||
entryEditContentsView?.expiresDate =
|
||||
entryEditFragment?.expiryTime =
|
||||
DateInstant(DateTime(expiresDate)
|
||||
.withHourOfDay(hours)
|
||||
.withMinuteOfHour(minutes)
|
||||
@@ -529,25 +747,19 @@ class EntryEditActivity : LockingActivity(),
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
mNewEntry?.let {
|
||||
populateEntryWithViews(it)
|
||||
outState.putParcelable(KEY_NEW_ENTRY, it)
|
||||
}
|
||||
|
||||
mFocusedEditExtraField?.let {
|
||||
outState.putParcelable(EXTRA_FIELD_FOCUSED_ENTRY, it)
|
||||
}
|
||||
outState.putParcelableArrayList(TEMP_ATTACHMENTS, mTempAttachments)
|
||||
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun acceptPassword(bundle: Bundle) {
|
||||
bundle.getString(GeneratePasswordDialogFragment.KEY_PASSWORD_ID)?.let {
|
||||
entryEditContentsView?.password = it
|
||||
entryEditFragment?.password = it
|
||||
}
|
||||
|
||||
entryEditActivityEducation?.let {
|
||||
Handler().post { performedNextEducation(it) }
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,21 +768,39 @@ class EntryEditActivity : LockingActivity(),
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
onApprovedBackPressed {
|
||||
super@EntryEditActivity.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancelSpecialMode() {
|
||||
onApprovedBackPressed {
|
||||
super@EntryEditActivity.onCancelSpecialMode()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onApprovedBackPressed(approved: () -> Unit) {
|
||||
if (!backPressedAlreadyApproved) {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.discard_changes)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.discard) { _, _ ->
|
||||
super@EntryEditActivity.onBackPressed()
|
||||
backPressedAlreadyApproved = true
|
||||
approved.invoke()
|
||||
}.create().show()
|
||||
} else {
|
||||
approved.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
// Assign entry callback as a result in all case
|
||||
private fun finishForEntryResult() {
|
||||
// Assign entry callback as a result
|
||||
try {
|
||||
mNewEntry?.let {
|
||||
mEntry?.let { entry ->
|
||||
val bundle = Bundle()
|
||||
val intentEntry = Intent()
|
||||
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, mNewEntry)
|
||||
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry)
|
||||
intentEntry.putExtras(bundle)
|
||||
if (mIsNew) {
|
||||
setResult(ADD_ENTRY_RESULT_CODE, intentEntry)
|
||||
@@ -594,8 +824,7 @@ class EntryEditActivity : LockingActivity(),
|
||||
const val KEY_PARENT = "parent"
|
||||
|
||||
// SaveInstanceState
|
||||
const val KEY_NEW_ENTRY = "new_entry"
|
||||
const val EXTRA_FIELD_FOCUSED_ENTRY = "EXTRA_FIELD_FOCUSED_ENTRY"
|
||||
const val TEMP_ATTACHMENTS = "TEMP_ATTACHMENTS"
|
||||
|
||||
// Keys for callback
|
||||
const val ADD_ENTRY_RESULT_CODE = 31
|
||||
@@ -603,16 +832,19 @@ class EntryEditActivity : LockingActivity(),
|
||||
const val ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129
|
||||
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
|
||||
|
||||
const val ENTRY_EDIT_FRAGMENT_TAG = "ENTRY_EDIT_FRAGMENT_TAG"
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to update an existing entry
|
||||
*
|
||||
* @param activity from activity
|
||||
* @param pwEntry Entry to update
|
||||
* @param entry Entry to update
|
||||
*/
|
||||
fun launch(activity: Activity, pwEntry: Entry) {
|
||||
fun launch(activity: Activity,
|
||||
entry: Entry) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, pwEntry.nodeId)
|
||||
intent.putExtra(KEY_ENTRY, entry.nodeId)
|
||||
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
||||
}
|
||||
}
|
||||
@@ -621,14 +853,102 @@ class EntryEditActivity : LockingActivity(),
|
||||
* Launch EntryEditActivity to add a new entry
|
||||
*
|
||||
* @param activity from activity
|
||||
* @param pwGroup Group who will contains new entry
|
||||
* @param group Group who will contains new entry
|
||||
*/
|
||||
fun launch(activity: Activity, pwGroup: Group) {
|
||||
fun launch(activity: Activity,
|
||||
group: Group) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_PARENT, pwGroup.nodeId)
|
||||
intent.putExtra(KEY_PARENT, group.nodeId)
|
||||
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
||||
}
|
||||
}
|
||||
|
||||
fun launchForSave(context: Context,
|
||||
entry: Entry,
|
||||
searchInfo: SearchInfo) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||
val intent = Intent(context, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, entry.nodeId)
|
||||
EntrySelectionHelper.startActivityForSaveModeResult(context,
|
||||
intent,
|
||||
searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
fun launchForSave(context: Context,
|
||||
group: Group,
|
||||
searchInfo: SearchInfo) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||
val intent = Intent(context, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_PARENT, group.nodeId)
|
||||
EntrySelectionHelper.startActivityForSaveModeResult(context,
|
||||
intent,
|
||||
searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to add a new entry in keyboard selection
|
||||
*/
|
||||
fun launchForKeyboardSelectionResult(context: Context,
|
||||
group: Group,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||
val intent = Intent(context, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_PARENT, group.nodeId)
|
||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(context,
|
||||
intent,
|
||||
searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to add a new entry in autofill selection
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
fun launchForAutofillResult(activity: Activity,
|
||||
assistStructure: AssistStructure,
|
||||
group: Group,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_PARENT, group.nodeId)
|
||||
AutofillHelper.startActivityForAutofillResult(activity,
|
||||
intent,
|
||||
assistStructure,
|
||||
searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to register an updated entry (from autofill)
|
||||
*/
|
||||
fun launchForRegistration(context: Context,
|
||||
entry: Entry,
|
||||
registerInfo: RegisterInfo? = null) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||
val intent = Intent(context, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, entry.nodeId)
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
|
||||
intent,
|
||||
registerInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to register a new entry (from autofill)
|
||||
*/
|
||||
fun launchForRegistration(context: Context,
|
||||
group: Group,
|
||||
registerInfo: RegisterInfo? = null) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||
val intent = Intent(context, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_PARENT, group.nodeId)
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
|
||||
intent,
|
||||
registerInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,546 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.model.*
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||
import com.kunzisoft.keepass.view.collapse
|
||||
import com.kunzisoft.keepass.view.expand
|
||||
|
||||
class EntryEditFragment: StylishFragment() {
|
||||
|
||||
private lateinit var entryTitleLayoutView: TextInputLayout
|
||||
private lateinit var entryTitleView: EditText
|
||||
private lateinit var entryIconView: ImageView
|
||||
private lateinit var entryUserNameView: EditText
|
||||
private lateinit var entryUrlView: EditText
|
||||
private lateinit var entryPasswordLayoutView: TextInputLayout
|
||||
private lateinit var entryPasswordView: EditText
|
||||
private lateinit var entryPasswordGeneratorView: View
|
||||
private lateinit var entryExpiresCheckBox: CompoundButton
|
||||
private lateinit var entryExpiresTextView: TextView
|
||||
private lateinit var entryNotesView: EditText
|
||||
private lateinit var extraFieldsContainerView: View
|
||||
private lateinit var extraFieldsListView: ViewGroup
|
||||
private lateinit var attachmentsContainerView: View
|
||||
private lateinit var attachmentsListView: RecyclerView
|
||||
|
||||
private lateinit var attachmentsAdapter: EntryAttachmentsItemsAdapter
|
||||
|
||||
private var fontInVisibility: Boolean = false
|
||||
private var iconColor: Int = 0
|
||||
private var expiresInstant: DateInstant = DateInstant.IN_ONE_MONTH
|
||||
|
||||
var drawFactory: IconDrawableFactory? = null
|
||||
var setOnDateClickListener: View.OnClickListener? = null
|
||||
var setOnPasswordGeneratorClickListener: View.OnClickListener? = null
|
||||
var setOnIconViewClickListener: View.OnClickListener? = null
|
||||
var setOnEditCustomField: ((Field) -> Unit)? = null
|
||||
var setOnRemoveAttachment: ((Attachment) -> Unit)? = null
|
||||
|
||||
// Elements to modify the current entry
|
||||
private var mEntryInfo = EntryInfo()
|
||||
private var mLastFocusedEditField: FocusedEditField? = null
|
||||
private var mExtraViewToRequestFocus: EditText? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
val rootView = inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.fragment_entry_edit_contents, container, false)
|
||||
|
||||
fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(requireContext())
|
||||
|
||||
entryTitleLayoutView = rootView.findViewById(R.id.entry_edit_container_title)
|
||||
entryTitleView = rootView.findViewById(R.id.entry_edit_title)
|
||||
entryIconView = rootView.findViewById(R.id.entry_edit_icon_button)
|
||||
entryIconView.setOnClickListener {
|
||||
setOnIconViewClickListener?.onClick(it)
|
||||
}
|
||||
|
||||
entryUserNameView = rootView.findViewById(R.id.entry_edit_user_name)
|
||||
entryUrlView = rootView.findViewById(R.id.entry_edit_url)
|
||||
entryPasswordLayoutView = rootView.findViewById(R.id.entry_edit_container_password)
|
||||
entryPasswordView = rootView.findViewById(R.id.entry_edit_password)
|
||||
entryPasswordGeneratorView = rootView.findViewById(R.id.entry_edit_password_generator_button)
|
||||
entryPasswordGeneratorView.setOnClickListener {
|
||||
setOnPasswordGeneratorClickListener?.onClick(it)
|
||||
}
|
||||
entryExpiresCheckBox = rootView.findViewById(R.id.entry_edit_expires_checkbox)
|
||||
entryExpiresTextView = rootView.findViewById(R.id.entry_edit_expires_text)
|
||||
entryExpiresTextView.setOnClickListener {
|
||||
if (entryExpiresCheckBox.isChecked)
|
||||
setOnDateClickListener?.onClick(it)
|
||||
}
|
||||
|
||||
entryNotesView = rootView.findViewById(R.id.entry_edit_notes)
|
||||
|
||||
extraFieldsContainerView = rootView.findViewById(R.id.extra_fields_container)
|
||||
extraFieldsListView = rootView.findViewById(R.id.extra_fields_list)
|
||||
|
||||
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
|
||||
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
|
||||
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
|
||||
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
|
||||
if (previousSize > 0 && newSize == 0) {
|
||||
attachmentsContainerView.collapse(true)
|
||||
} else if (previousSize == 0 && newSize == 1) {
|
||||
attachmentsContainerView.expand(true)
|
||||
}
|
||||
}
|
||||
attachmentsListView.apply {
|
||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
adapter = attachmentsAdapter
|
||||
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
}
|
||||
|
||||
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
|
||||
assignExpiresDateText()
|
||||
}
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
|
||||
taIconColor?.recycle()
|
||||
|
||||
// Retrieve the new entry after an orientation change
|
||||
if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true)
|
||||
mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
|
||||
else if (savedInstanceState?.containsKey(KEY_TEMP_ENTRY_INFO) == true) {
|
||||
mEntryInfo = savedInstanceState.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
|
||||
}
|
||||
|
||||
if (savedInstanceState?.containsKey(KEY_LAST_FOCUSED_FIELD) == true) {
|
||||
mLastFocusedEditField = savedInstanceState.getParcelable(KEY_LAST_FOCUSED_FIELD) ?: mLastFocusedEditField
|
||||
}
|
||||
|
||||
populateViewsWithEntry()
|
||||
|
||||
return rootView
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
super.onDetach()
|
||||
|
||||
drawFactory = null
|
||||
setOnDateClickListener = null
|
||||
setOnPasswordGeneratorClickListener = null
|
||||
setOnIconViewClickListener = null
|
||||
setOnRemoveAttachment = null
|
||||
setOnEditCustomField = null
|
||||
}
|
||||
|
||||
fun getEntryInfo(): EntryInfo? {
|
||||
populateEntryWithViews()
|
||||
return mEntryInfo
|
||||
}
|
||||
|
||||
fun generatePasswordEducationPerformed(entryEditActivityEducation: EntryEditActivityEducation): Boolean {
|
||||
return entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
||||
entryPasswordGeneratorView,
|
||||
{
|
||||
GeneratePasswordDialogFragment().show(parentFragmentManager, "PasswordGeneratorFragment")
|
||||
},
|
||||
{
|
||||
try {
|
||||
(activity as? EntryEditActivity?)?.performedNextEducation(entryEditActivityEducation)
|
||||
} catch (ignore: Exception) {}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun populateViewsWithEntry() {
|
||||
// Set info in view
|
||||
icon = mEntryInfo.icon
|
||||
title = mEntryInfo.title
|
||||
username = mEntryInfo.username
|
||||
url = mEntryInfo.url
|
||||
password = mEntryInfo.password
|
||||
expires = mEntryInfo.expires
|
||||
expiryTime = mEntryInfo.expiryTime
|
||||
notes = mEntryInfo.notes
|
||||
assignExtraFields(mEntryInfo.customFields) { fields ->
|
||||
setOnEditCustomField?.invoke(fields)
|
||||
}
|
||||
assignAttachments(mEntryInfo.attachments, StreamDirection.UPLOAD) { attachment ->
|
||||
setOnRemoveAttachment?.invoke(attachment)
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateEntryWithViews() {
|
||||
// Icon already populate
|
||||
mEntryInfo.title = title
|
||||
mEntryInfo.username = username
|
||||
mEntryInfo.url = url
|
||||
mEntryInfo.password = password
|
||||
mEntryInfo.expires = expires
|
||||
mEntryInfo.expiryTime = expiryTime
|
||||
mEntryInfo.notes = notes
|
||||
mEntryInfo.customFields = getExtraFields()
|
||||
mEntryInfo.otpModel = OtpEntryFields.parseFields { key ->
|
||||
getExtraFields().firstOrNull { it.name == key }?.protectedValue?.toString()
|
||||
}?.otpModel
|
||||
mEntryInfo.attachments = getAttachments()
|
||||
}
|
||||
|
||||
var title: String
|
||||
get() {
|
||||
return entryTitleView.text.toString()
|
||||
}
|
||||
set(value) {
|
||||
entryTitleView.setText(value)
|
||||
if (fontInVisibility)
|
||||
entryTitleView.applyFontVisibility()
|
||||
}
|
||||
|
||||
var icon: IconImage
|
||||
get() {
|
||||
return mEntryInfo.icon
|
||||
}
|
||||
set(value) {
|
||||
mEntryInfo.icon = value
|
||||
drawFactory?.let { drawFactory ->
|
||||
entryIconView.assignDatabaseIcon(drawFactory, value, iconColor)
|
||||
}
|
||||
}
|
||||
|
||||
var username: String
|
||||
get() {
|
||||
return entryUserNameView.text.toString()
|
||||
}
|
||||
set(value) {
|
||||
entryUserNameView.setText(value)
|
||||
if (fontInVisibility)
|
||||
entryUserNameView.applyFontVisibility()
|
||||
}
|
||||
|
||||
var url: String
|
||||
get() {
|
||||
return entryUrlView.text.toString()
|
||||
}
|
||||
set(value) {
|
||||
entryUrlView.setText(value)
|
||||
if (fontInVisibility)
|
||||
entryUrlView.applyFontVisibility()
|
||||
}
|
||||
|
||||
var password: String
|
||||
get() {
|
||||
return entryPasswordView.text.toString()
|
||||
}
|
||||
set(value) {
|
||||
entryPasswordView.setText(value)
|
||||
if (fontInVisibility) {
|
||||
entryPasswordView.applyFontVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
private fun assignExpiresDateText() {
|
||||
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
|
||||
entryExpiresTextView.setOnClickListener(setOnDateClickListener)
|
||||
expiresInstant.getDateTimeString(resources)
|
||||
} else {
|
||||
entryExpiresTextView.setOnClickListener(null)
|
||||
resources.getString(R.string.never)
|
||||
}
|
||||
if (fontInVisibility)
|
||||
entryExpiresTextView.applyFontVisibility()
|
||||
}
|
||||
|
||||
var expires: Boolean
|
||||
get() {
|
||||
return entryExpiresCheckBox.isChecked
|
||||
}
|
||||
set(value) {
|
||||
if (!value) {
|
||||
expiresInstant = DateInstant.IN_ONE_MONTH
|
||||
}
|
||||
entryExpiresCheckBox.isChecked = value
|
||||
assignExpiresDateText()
|
||||
}
|
||||
|
||||
var expiryTime: DateInstant
|
||||
get() {
|
||||
return if (expires)
|
||||
expiresInstant
|
||||
else
|
||||
DateInstant.NEVER_EXPIRE
|
||||
}
|
||||
set(value) {
|
||||
if (expires)
|
||||
expiresInstant = value
|
||||
assignExpiresDateText()
|
||||
}
|
||||
|
||||
var notes: String
|
||||
get() {
|
||||
return entryNotesView.text.toString()
|
||||
}
|
||||
set(value) {
|
||||
entryNotesView.setText(value)
|
||||
if (fontInVisibility)
|
||||
entryNotesView.applyFontVisibility()
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* Extra Fields
|
||||
* -------------
|
||||
*/
|
||||
|
||||
private var mExtraFieldsList: MutableList<Field> = ArrayList()
|
||||
private var mOnEditButtonClickListener: ((item: Field)->Unit)? = null
|
||||
|
||||
private fun buildViewFromField(extraField: Field): View? {
|
||||
val inflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
val itemView: View? = inflater?.inflate(R.layout.item_entry_edit_extra_field, extraFieldsListView, false)
|
||||
itemView?.id = View.NO_ID
|
||||
|
||||
val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container)
|
||||
extraFieldValueContainer?.isPasswordVisibilityToggleEnabled = extraField.protectedValue.isProtected
|
||||
extraFieldValueContainer?.hint = extraField.name
|
||||
extraFieldValueContainer?.id = View.NO_ID
|
||||
|
||||
val extraFieldValue: TextInputEditText? = itemView?.findViewById(R.id.entry_extra_field_value)
|
||||
extraFieldValue?.apply {
|
||||
if (extraField.protectedValue.isProtected) {
|
||||
inputType = extraFieldValue.inputType or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
|
||||
}
|
||||
setText(extraField.protectedValue.toString())
|
||||
if (fontInVisibility)
|
||||
applyFontVisibility()
|
||||
}
|
||||
extraFieldValue?.id = View.NO_ID
|
||||
extraFieldValue?.tag = "FIELD_VALUE_TAG"
|
||||
if (mLastFocusedEditField?.field == extraField) {
|
||||
mExtraViewToRequestFocus = extraFieldValue
|
||||
}
|
||||
|
||||
val extraFieldEditButton: View? = itemView?.findViewById(R.id.entry_extra_field_edit)
|
||||
extraFieldEditButton?.setOnClickListener {
|
||||
mOnEditButtonClickListener?.invoke(extraField)
|
||||
}
|
||||
extraFieldEditButton?.id = View.NO_ID
|
||||
|
||||
return itemView
|
||||
}
|
||||
|
||||
fun getExtraFields(): List<Field> {
|
||||
mLastFocusedEditField = null
|
||||
for (index in 0 until extraFieldsListView.childCount) {
|
||||
val extraFieldValue: EditText = extraFieldsListView.getChildAt(index)
|
||||
.findViewWithTag("FIELD_VALUE_TAG")
|
||||
val extraField = mExtraFieldsList[index]
|
||||
extraField.protectedValue.stringValue = extraFieldValue.text?.toString() ?: ""
|
||||
if (extraFieldValue.isFocused) {
|
||||
mLastFocusedEditField = FocusedEditField().apply {
|
||||
field = extraField
|
||||
cursorSelectionStart = extraFieldValue.selectionStart
|
||||
cursorSelectionEnd = extraFieldValue.selectionEnd
|
||||
}
|
||||
}
|
||||
}
|
||||
return mExtraFieldsList
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all children and add new views for each field
|
||||
*/
|
||||
fun assignExtraFields(fields: List<Field>,
|
||||
onEditButtonClickListener: ((item: Field)->Unit)?) {
|
||||
extraFieldsContainerView.visibility = if (fields.isEmpty()) View.GONE else View.VISIBLE
|
||||
// Reinit focused field
|
||||
mExtraFieldsList.clear()
|
||||
mExtraFieldsList.addAll(fields)
|
||||
extraFieldsListView.removeAllViews()
|
||||
fields.forEach {
|
||||
extraFieldsListView.addView(buildViewFromField(it))
|
||||
}
|
||||
// Request last focus
|
||||
mLastFocusedEditField?.let { focusField ->
|
||||
mExtraViewToRequestFocus?.apply {
|
||||
requestFocus()
|
||||
setSelection(focusField.cursorSelectionStart,
|
||||
focusField.cursorSelectionEnd)
|
||||
}
|
||||
}
|
||||
mLastFocusedEditField = null
|
||||
mOnEditButtonClickListener = onEditButtonClickListener
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an extra field or create a new one if doesn't exists, the old value is lost
|
||||
*/
|
||||
fun putExtraField(extraField: Field) {
|
||||
extraFieldsContainerView.visibility = View.VISIBLE
|
||||
val oldField = mExtraFieldsList.firstOrNull { it.name == extraField.name }
|
||||
oldField?.let {
|
||||
val index = mExtraFieldsList.indexOf(oldField)
|
||||
mExtraFieldsList.removeAt(index)
|
||||
mExtraFieldsList.add(index, extraField)
|
||||
extraFieldsListView.removeViewAt(index)
|
||||
val newView = buildViewFromField(extraField)
|
||||
extraFieldsListView.addView(newView, index)
|
||||
newView?.requestFocus()
|
||||
} ?: kotlin.run {
|
||||
mExtraFieldsList.add(extraField)
|
||||
val newView = buildViewFromField(extraField)
|
||||
extraFieldsListView.addView(newView)
|
||||
newView?.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an extra field and keep the old value
|
||||
*/
|
||||
fun replaceExtraField(oldExtraField: Field, newExtraField: Field) {
|
||||
extraFieldsContainerView.visibility = View.VISIBLE
|
||||
val index = mExtraFieldsList.indexOf(oldExtraField)
|
||||
val oldValueEditText: EditText = extraFieldsListView.getChildAt(index)
|
||||
.findViewWithTag("FIELD_VALUE_TAG")
|
||||
val oldValue = oldValueEditText.text.toString()
|
||||
val newExtraFieldWithOldValue = Field(newExtraField).apply {
|
||||
this.protectedValue.stringValue = oldValue
|
||||
}
|
||||
mExtraFieldsList.removeAt(index)
|
||||
mExtraFieldsList.add(index, newExtraFieldWithOldValue)
|
||||
extraFieldsListView.removeViewAt(index)
|
||||
val newView = buildViewFromField(newExtraFieldWithOldValue)
|
||||
extraFieldsListView.addView(newView, index)
|
||||
newView?.requestFocus()
|
||||
}
|
||||
|
||||
fun removeExtraField(oldExtraField: Field) {
|
||||
val previousSize = mExtraFieldsList.size
|
||||
val index = mExtraFieldsList.indexOf(oldExtraField)
|
||||
extraFieldsListView.getChildAt(index)?.let {
|
||||
it.collapse(true) {
|
||||
mExtraFieldsList.removeAt(index)
|
||||
extraFieldsListView.removeViewAt(index)
|
||||
val newSize = mExtraFieldsList.size
|
||||
|
||||
if (previousSize > 0 && newSize == 0) {
|
||||
extraFieldsContainerView.collapse(true)
|
||||
} else if (previousSize == 0 && newSize == 1) {
|
||||
extraFieldsContainerView.expand(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* Attachments
|
||||
* -------------
|
||||
*/
|
||||
|
||||
fun getAttachments(): List<Attachment> {
|
||||
return attachmentsAdapter.itemsList.map { it.attachment }
|
||||
}
|
||||
|
||||
fun assignAttachments(attachments: List<Attachment>,
|
||||
streamDirection: StreamDirection,
|
||||
onDeleteItem: (attachment: Attachment)->Unit) {
|
||||
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
|
||||
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
|
||||
attachmentsAdapter.onDeleteButtonClickListener = { item ->
|
||||
onDeleteItem.invoke(item.attachment)
|
||||
}
|
||||
}
|
||||
|
||||
fun containsAttachment(): Boolean {
|
||||
return !attachmentsAdapter.isEmpty()
|
||||
}
|
||||
|
||||
fun containsAttachment(attachment: EntryAttachmentState): Boolean {
|
||||
return attachmentsAdapter.contains(attachment)
|
||||
}
|
||||
|
||||
fun putAttachment(attachment: EntryAttachmentState) {
|
||||
attachmentsContainerView.visibility = View.VISIBLE
|
||||
attachmentsAdapter.putItem(attachment)
|
||||
}
|
||||
|
||||
fun removeAttachment(attachment: EntryAttachmentState) {
|
||||
attachmentsAdapter.removeItem(attachment)
|
||||
}
|
||||
|
||||
fun clearAttachments() {
|
||||
attachmentsAdapter.clear()
|
||||
}
|
||||
|
||||
fun getAttachmentViewPosition(attachment: EntryAttachmentState, position: (Float) -> Unit) {
|
||||
attachmentsListView.postDelayed({
|
||||
position.invoke(attachmentsContainerView.y
|
||||
+ attachmentsListView.y
|
||||
+ (attachmentsListView.getChildAt(attachmentsAdapter.indexOf(attachment))?.y
|
||||
?: 0F)
|
||||
)
|
||||
}, 250)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
populateEntryWithViews()
|
||||
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
|
||||
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
|
||||
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
|
||||
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
|
||||
|
||||
fun getInstance(entryInfo: EntryInfo?): EntryEditFragment {
|
||||
return EntryEditFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
@@ -31,6 +32,7 @@ import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
|
||||
/**
|
||||
@@ -42,34 +44,70 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
var sharedWebDomain: String? = null
|
||||
var otpString: String? = null
|
||||
|
||||
when (intent?.action) {
|
||||
Intent.ACTION_SEND -> {
|
||||
if ("text/plain" == intent.type) {
|
||||
// Retrieve web domain
|
||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
||||
sharedWebDomain = Uri.parse(it).host
|
||||
// Retrieve web domain or OTP
|
||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
|
||||
if (OtpEntryFields.isOTPUri(extra))
|
||||
otpString = extra
|
||||
else
|
||||
sharedWebDomain = Uri.parse(extra).host
|
||||
}
|
||||
}
|
||||
}
|
||||
Intent.ACTION_VIEW -> {
|
||||
// Retrieve OTP
|
||||
intent.dataString?.let { extra ->
|
||||
if (OtpEntryFields.isOTPUri(extra))
|
||||
otpString = extra
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
|
||||
// Build domain search param
|
||||
val searchInfo = SearchInfo().apply {
|
||||
this.webDomain = sharedWebDomain
|
||||
this.otpString = otpString
|
||||
}
|
||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
launch(searchInfo)
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun launch(searchInfo: SearchInfo) {
|
||||
|
||||
if (!searchInfo.containsOnlyNullValues()) {
|
||||
// Setting to integrate Magikeyboard
|
||||
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
|
||||
|
||||
// Build search param
|
||||
val searchInfo = SearchInfo().apply {
|
||||
webDomain = sharedWebDomain
|
||||
}
|
||||
|
||||
// If database is open
|
||||
val database = Database.getInstance()
|
||||
val readOnly = database.isReadOnly
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
Database.getInstance(),
|
||||
database,
|
||||
searchInfo,
|
||||
{ items ->
|
||||
// Items found
|
||||
if (searchShareForMagikeyboard) {
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
GroupActivity.launchForSaveResult(this,
|
||||
searchInfo,
|
||||
false)
|
||||
} else {
|
||||
Toast.makeText(applicationContext,
|
||||
R.string.autofill_read_only_save,
|
||||
Toast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
if (items.size == 1) {
|
||||
// Automatically populate keyboard
|
||||
val entryPopulate = items[0]
|
||||
@@ -78,43 +116,65 @@ class EntrySelectionLauncherActivity : AppCompatActivity() {
|
||||
intent)
|
||||
} else {
|
||||
// Select the one we want
|
||||
GroupActivity.launchForEntrySelectionResult(this,
|
||||
true,
|
||||
searchInfo)
|
||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||
readOnly,
|
||||
searchInfo,
|
||||
true)
|
||||
}
|
||||
} else {
|
||||
GroupActivity.launch(this,
|
||||
true,
|
||||
searchInfo)
|
||||
GroupActivity.launchForSearchResult(this,
|
||||
readOnly,
|
||||
searchInfo,
|
||||
true)
|
||||
}
|
||||
},
|
||||
{
|
||||
// Show the database UI to select the entry
|
||||
if (searchShareForMagikeyboard) {
|
||||
GroupActivity.launchForEntrySelectionResult(this,
|
||||
false,
|
||||
searchInfo)
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
GroupActivity.launchForSaveResult(this,
|
||||
searchInfo,
|
||||
false)
|
||||
} else {
|
||||
GroupActivity.launch(this,
|
||||
false,
|
||||
searchInfo)
|
||||
Toast.makeText(applicationContext,
|
||||
R.string.autofill_read_only_save,
|
||||
Toast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
} else if (readOnly || searchShareForMagikeyboard) {
|
||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||
readOnly,
|
||||
searchInfo,
|
||||
false)
|
||||
} else {
|
||||
GroupActivity.launchForSaveResult(this,
|
||||
searchInfo,
|
||||
false)
|
||||
}
|
||||
},
|
||||
{
|
||||
// If database not open
|
||||
if (searchShareForMagikeyboard) {
|
||||
FileDatabaseSelectActivity.launchForEntrySelectionResult(this,
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
FileDatabaseSelectActivity.launchForSaveResult(this,
|
||||
searchInfo)
|
||||
} else {
|
||||
FileDatabaseSelectActivity.launch(this,
|
||||
Toast.makeText(applicationContext,
|
||||
R.string.autofill_read_only_save,
|
||||
Toast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
|
||||
searchInfo)
|
||||
} else {
|
||||
FileDatabaseSelectActivity.launchForSearchResult(this,
|
||||
searchInfo)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
finish()
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +185,6 @@ fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
||||
// Populate Magikeyboard with entry
|
||||
MagikIME.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
||||
// Consume the selection mode
|
||||
EntrySelectionHelper.removeEntrySelectionModeFromIntent(intent)
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
activity.moveTaskToBack(true)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.assist.AssistStructure
|
||||
import android.content.Context
|
||||
@@ -28,6 +27,7 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
@@ -36,7 +36,6 @@ import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
@@ -44,8 +43,8 @@ import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
|
||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
@@ -53,8 +52,10 @@ import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
@@ -69,7 +70,6 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
|
||||
// Views
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var fileManagerExplanationButton: View? = null
|
||||
private var createDatabaseButtonView: View? = null
|
||||
private var openDatabaseButtonView: View? = null
|
||||
|
||||
@@ -82,7 +82,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
|
||||
private var mDatabaseFileUri: Uri? = null
|
||||
|
||||
private var mOpenFileHelper: OpenFileHelper? = null
|
||||
private var mSelectFileHelper: SelectFileHelper? = null
|
||||
|
||||
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||
|
||||
@@ -98,20 +98,15 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
toolbar.title = ""
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
fileManagerExplanationButton = findViewById(R.id.file_manager_explanation_button)
|
||||
fileManagerExplanationButton?.setOnClickListener {
|
||||
UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
|
||||
}
|
||||
|
||||
// Create database button
|
||||
createDatabaseButtonView = findViewById(R.id.create_database_button)
|
||||
createDatabaseButtonView?.setOnClickListener { createNewFile() }
|
||||
|
||||
// Open database button
|
||||
mOpenFileHelper = OpenFileHelper(this)
|
||||
mSelectFileHelper = SelectFileHelper(this)
|
||||
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
||||
openDatabaseButtonView?.apply {
|
||||
mOpenFileHelper?.openFileOnClickViewListener?.let {
|
||||
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
||||
setOnClickListener(it)
|
||||
setOnLongClickListener(it)
|
||||
}
|
||||
@@ -166,7 +161,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
}
|
||||
|
||||
// Observe list of databases
|
||||
databaseFilesViewModel.databaseFilesLoaded.observe(this, Observer { databaseFiles ->
|
||||
databaseFilesViewModel.databaseFilesLoaded.observe(this) { databaseFiles ->
|
||||
when (databaseFiles.databaseFileAction) {
|
||||
DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
|
||||
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
|
||||
@@ -175,7 +170,8 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
|
||||
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
|
||||
}
|
||||
GroupActivity.launch(this@FileDatabaseSelectActivity)
|
||||
GroupActivity.launch(this@FileDatabaseSelectActivity,
|
||||
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity))
|
||||
}
|
||||
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
|
||||
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
|
||||
@@ -189,13 +185,13 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
}
|
||||
}
|
||||
databaseFilesViewModel.consumeAction()
|
||||
})
|
||||
}
|
||||
|
||||
// Observe default database
|
||||
databaseFilesViewModel.defaultDatabase.observe(this, Observer {
|
||||
databaseFilesViewModel.defaultDatabase.observe(this) {
|
||||
// Retrieve settings for default database
|
||||
mAdapterDatabaseHistory?.setDefaultDatabase(it)
|
||||
})
|
||||
}
|
||||
|
||||
// Attach the dialog thread to this activity
|
||||
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
|
||||
@@ -207,6 +203,24 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
databaseFilesViewModel.addDatabaseFile(databaseUri, keyFileUri)
|
||||
}
|
||||
}
|
||||
ACTION_DATABASE_LOAD_TASK -> {
|
||||
val database = Database.getInstance()
|
||||
if (result.isSuccess
|
||||
&& database.loaded) {
|
||||
launchGroupActivity(database)
|
||||
} else {
|
||||
var resultError = ""
|
||||
val resultMessage = result.message
|
||||
// Show error message
|
||||
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
||||
resultError = "$resultError $resultMessage"
|
||||
}
|
||||
Log.e(TAG, resultError)
|
||||
Snackbar.make(activity_file_selection_coordinator_layout,
|
||||
resultError,
|
||||
Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,7 +229,6 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
/**
|
||||
* Create a new file by calling the content provider
|
||||
*/
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun createNewFile() {
|
||||
createDocument(this, getString(R.string.database_file_name_default) +
|
||||
getString(R.string.database_file_extension_default), "application/x-keepass")
|
||||
@@ -223,78 +236,39 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
|
||||
private fun fileNoFoundAction(e: FileNotFoundException) {
|
||||
val error = getString(R.string.file_not_found_content)
|
||||
Log.e(TAG, error, e)
|
||||
coordinatorLayout?.let {
|
||||
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
Log.e(TAG, error, e)
|
||||
}
|
||||
|
||||
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
||||
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
|
||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||
{
|
||||
try {
|
||||
PasswordActivity.launch(this@FileDatabaseSelectActivity,
|
||||
databaseUri, keyFile,
|
||||
searchInfo)
|
||||
} catch (e: FileNotFoundException) {
|
||||
fileNoFoundAction(e)
|
||||
}
|
||||
// Remove the search info from intent
|
||||
if (searchInfo != null) {
|
||||
finish()
|
||||
}
|
||||
PasswordActivity.launch(this,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
{ exception ->
|
||||
fileNoFoundAction(exception)
|
||||
},
|
||||
{
|
||||
try {
|
||||
PasswordActivity.launchForKeyboardResult(this@FileDatabaseSelectActivity,
|
||||
databaseUri, keyFile,
|
||||
searchInfo)
|
||||
} catch (e: FileNotFoundException) {
|
||||
fileNoFoundAction(e)
|
||||
}
|
||||
finish()
|
||||
},
|
||||
{ assistStructure ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
try {
|
||||
PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
||||
databaseUri, keyFile,
|
||||
assistStructure,
|
||||
searchInfo)
|
||||
} catch (e: FileNotFoundException) {
|
||||
fileNoFoundAction(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() })
|
||||
}
|
||||
|
||||
private fun launchGroupActivity(readOnly: Boolean) {
|
||||
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
|
||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||
{
|
||||
GroupActivity.launch(this@FileDatabaseSelectActivity,
|
||||
false,
|
||||
searchInfo,
|
||||
readOnly)
|
||||
},
|
||||
{
|
||||
GroupActivity.launchForEntrySelectionResult(this@FileDatabaseSelectActivity,
|
||||
false,
|
||||
searchInfo,
|
||||
readOnly)
|
||||
// Do not keep history
|
||||
finish()
|
||||
},
|
||||
{ assistStructure ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
GroupActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
||||
assistStructure,
|
||||
false,
|
||||
searchInfo,
|
||||
readOnly)
|
||||
private fun launchGroupActivity(database: Database) {
|
||||
GroupActivity.launch(this,
|
||||
database.isReadOnly,
|
||||
{ onValidateSpecialMode() },
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() })
|
||||
}
|
||||
})
|
||||
|
||||
override fun onValidateSpecialMode() {
|
||||
super.onValidateSpecialMode()
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onCancelSpecialMode() {
|
||||
super.onCancelSpecialMode()
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
||||
@@ -308,10 +282,8 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
super.onResume()
|
||||
|
||||
// Show open and create button or special mode
|
||||
if (mSelectionMode) {
|
||||
// Disable create button if in selection mode or request for autofill
|
||||
createDatabaseButtonView?.visibility = View.GONE
|
||||
} else {
|
||||
when (mSpecialMode) {
|
||||
SpecialMode.DEFAULT -> {
|
||||
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
||||
// There is an activity which can handle this intent.
|
||||
createDatabaseButtonView?.visibility = View.VISIBLE
|
||||
@@ -320,10 +292,15 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
createDatabaseButtonView?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Disable create button if in selection mode or request for autofill
|
||||
createDatabaseButtonView?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
val database = Database.getInstance()
|
||||
if (database.loaded) {
|
||||
launchGroupActivity(database.isReadOnly)
|
||||
launchGroupActivity(database)
|
||||
} else {
|
||||
// Construct adapter with listeners
|
||||
if (PreferencesUtil.showRecentFiles(this)) {
|
||||
@@ -389,7 +366,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||
if (uri != null) {
|
||||
launchPasswordActivityWithPath(uri)
|
||||
}
|
||||
@@ -414,11 +391,11 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
if (!mSelectionMode) {
|
||||
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||
MenuUtil.defaultMenuInflater(menuInflater, menu)
|
||||
}
|
||||
|
||||
Handler().post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -445,7 +422,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
openDatabaseButtonView!!,
|
||||
{tapTargetView ->
|
||||
tapTargetView?.let {
|
||||
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it)
|
||||
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
|
||||
}
|
||||
},
|
||||
{}
|
||||
@@ -454,6 +431,10 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
|
||||
}
|
||||
|
||||
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
@@ -465,17 +446,38 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Launch only to standard search, else pass by PasswordActivity
|
||||
* Standard Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
fun launch(context: Context,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
val intent = Intent(context, FileDatabaseSelectActivity::class.java)
|
||||
searchInfo?.let {
|
||||
intent.putExtra(KEY_SEARCH_INFO, it)
|
||||
fun launch(context: Context) {
|
||||
context.startActivity(Intent(context, FileDatabaseSelectActivity::class.java))
|
||||
}
|
||||
context.startActivity(intent)
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Search Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
fun launchForSearchResult(context: Context,
|
||||
searchInfo: SearchInfo) {
|
||||
EntrySelectionHelper.startActivityForSearchModeResult(context,
|
||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
searchInfo)
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Save Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
fun launchForSaveResult(context: Context,
|
||||
searchInfo: SearchInfo) {
|
||||
EntrySelectionHelper.startActivityForSaveModeResult(context,
|
||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
searchInfo)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -484,9 +486,9 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
fun launchForEntrySelectionResult(activity: Activity,
|
||||
fun launchForKeyboardSelectionResult(activity: Activity,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
EntrySelectionHelper.startActivityForEntrySelectionResult(activity,
|
||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(activity,
|
||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||
searchInfo)
|
||||
}
|
||||
@@ -506,5 +508,17 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
assistStructure,
|
||||
searchInfo)
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Registration Launch
|
||||
* -------------------------
|
||||
*/
|
||||
fun launchForRegistration(context: Context,
|
||||
registerInfo: RegisterInfo? = null) {
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
|
||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
registerInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
@@ -35,6 +36,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.SearchView
|
||||
@@ -43,13 +45,10 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.*
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
@@ -61,14 +60,16 @@ import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.education.GroupActivityEducation
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.model.getSearchString
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.OLD_NODES_KEY
|
||||
@@ -202,16 +203,48 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
|
||||
// Add listeners to the add buttons
|
||||
addNodeButtonView?.setAddGroupClickListener(View.OnClickListener {
|
||||
addNodeButtonView?.setAddGroupClickListener {
|
||||
GroupEditDialogFragment.build()
|
||||
.show(supportFragmentManager,
|
||||
GroupEditDialogFragment.TAG_CREATE_GROUP)
|
||||
})
|
||||
addNodeButtonView?.setAddEntryClickListener(View.OnClickListener {
|
||||
mCurrentGroup?.let { currentGroup ->
|
||||
EntryEditActivity.launch(this@GroupActivity, currentGroup)
|
||||
}
|
||||
})
|
||||
addNodeButtonView?.setAddEntryClickListener {
|
||||
mCurrentGroup?.let { currentGroup ->
|
||||
EntrySelectionHelper.doSpecialAction(intent,
|
||||
{
|
||||
EntryEditActivity.launch(this@GroupActivity, currentGroup)
|
||||
},
|
||||
{
|
||||
// Search not used
|
||||
},
|
||||
{ searchInfo ->
|
||||
EntryEditActivity.launchForSave(this@GroupActivity,
|
||||
currentGroup, searchInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo ->
|
||||
EntryEditActivity.launchForKeyboardSelectionResult(this@GroupActivity,
|
||||
currentGroup, searchInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo, assistStructure ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
EntryEditActivity.launchForAutofillResult(this@GroupActivity,
|
||||
assistStructure,
|
||||
currentGroup, searchInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
} else {
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
},
|
||||
{ searchInfo ->
|
||||
EntryEditActivity.launchForRegistration(this@GroupActivity,
|
||||
currentGroup, searchInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
mDatabase?.let { database ->
|
||||
// Search suggestion
|
||||
@@ -232,6 +265,41 @@ class GroupActivity : LockingActivity(),
|
||||
refreshSearchGroup()
|
||||
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||
if (result.isSuccess) {
|
||||
mListNodesFragment?.updateNodes(oldNodes, newNodes)
|
||||
EntrySelectionHelper.doSpecialAction(intent,
|
||||
{
|
||||
// Standard not used after task
|
||||
},
|
||||
{
|
||||
// Search not used
|
||||
},
|
||||
{
|
||||
// Save not used
|
||||
},
|
||||
{
|
||||
try {
|
||||
val entry = newNodes[0] as Entry
|
||||
entrySelectedForKeyboardSelection(entry)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to perform action for keyboard selection after entry update", e)
|
||||
}
|
||||
},
|
||||
{ _, _ ->
|
||||
try {
|
||||
val entry = newNodes[0] as Entry
|
||||
entrySelectedForAutofillSelection(entry)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to perform action for autofill selection after entry update", e)
|
||||
}
|
||||
},
|
||||
{
|
||||
// Not use
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
ACTION_DATABASE_UPDATE_GROUP_TASK -> {
|
||||
if (result.isSuccess) {
|
||||
mListNodesFragment?.updateNodes(oldNodes, newNodes)
|
||||
@@ -305,11 +373,11 @@ class GroupActivity : LockingActivity(),
|
||||
*/
|
||||
private fun manageSearchInfoIntent(intent: Intent): Boolean {
|
||||
// To relaunch the activity as ACTION_SEARCH
|
||||
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
|
||||
val searchInfo: SearchInfo? = EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
||||
val autoSearch = intent.getBooleanExtra(AUTO_SEARCH_KEY, false)
|
||||
if (searchInfo != null && autoSearch) {
|
||||
intent.action = Intent.ACTION_SEARCH
|
||||
intent.putExtra(SearchManager.QUERY, searchInfo.getSearchString(this))
|
||||
intent.putExtra(SearchManager.QUERY, searchInfo.toString())
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -354,7 +422,7 @@ class GroupActivity : LockingActivity(),
|
||||
fragmentTransaction.addToBackStack(fragmentTag)
|
||||
fragmentTransaction.commit()
|
||||
|
||||
if (mSelectionMode)
|
||||
if (mSpecialMode != SpecialMode.DEFAULT)
|
||||
mSelectionModeCountBackStack++
|
||||
|
||||
// Update last access time.
|
||||
@@ -482,20 +550,6 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancelSpecialMode() {
|
||||
// To remove the navigation history and
|
||||
EntrySelectionHelper.removeEntrySelectionModeFromIntent(intent)
|
||||
val fragmentManager = supportFragmentManager
|
||||
if (mSelectionModeCountBackStack > 0) {
|
||||
for (selectionMode in 0 .. mSelectionModeCountBackStack) {
|
||||
fragmentManager.popBackStack()
|
||||
}
|
||||
}
|
||||
// Reinit the counter for navigation history
|
||||
mSelectionModeCountBackStack = 0
|
||||
backToTheAppCaller()
|
||||
}
|
||||
|
||||
private fun refreshNumberOfChildren() {
|
||||
numberChildrenView?.apply {
|
||||
if (PreferencesUtil.showNumberEntries(context)) {
|
||||
@@ -523,27 +577,57 @@ class GroupActivity : LockingActivity(),
|
||||
|
||||
Type.ENTRY -> try {
|
||||
val entryVersioned = node as Entry
|
||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||
EntrySelectionHelper.doSpecialAction(intent,
|
||||
{
|
||||
EntryActivity.launch(this@GroupActivity, entryVersioned, mReadOnly)
|
||||
},
|
||||
{
|
||||
rebuildListNodes()
|
||||
// Populate Magikeyboard with entry
|
||||
mDatabase?.let { database ->
|
||||
populateKeyboardAndMoveAppToBackground(this@GroupActivity,
|
||||
entryVersioned.getEntryInfo(database),
|
||||
intent)
|
||||
// Nothing here, a search is simply performed
|
||||
},
|
||||
{ searchInfo ->
|
||||
if (!mReadOnly)
|
||||
entrySelectedForSave(entryVersioned, searchInfo)
|
||||
else
|
||||
finish()
|
||||
},
|
||||
{ searchInfo ->
|
||||
// Recheck search, only to fix #783 because workflow allows to open multiple search elements
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
mDatabase!!,
|
||||
searchInfo,
|
||||
{ _ ->
|
||||
// Item in search, don't save
|
||||
entrySelectedForKeyboardSelection(entryVersioned)
|
||||
},
|
||||
{
|
||||
// Item not found, save it if required
|
||||
if (!mReadOnly
|
||||
&& searchInfo != null
|
||||
&& PreferencesUtil.isKeyboardSaveSearchInfoEnable(this@GroupActivity)) {
|
||||
updateEntryWithSearchInfo(entryVersioned, searchInfo)
|
||||
} else {
|
||||
entrySelectedForKeyboardSelection(entryVersioned)
|
||||
}
|
||||
},
|
||||
{
|
||||
// Build response with the entry selected
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) {
|
||||
mDatabase?.let { database ->
|
||||
AutofillHelper.buildResponse(this@GroupActivity,
|
||||
entryVersioned.getEntryInfo(database))
|
||||
// Normally not append
|
||||
finish()
|
||||
}
|
||||
)
|
||||
},
|
||||
{ searchInfo, _ ->
|
||||
if (!mReadOnly
|
||||
&& searchInfo != null
|
||||
&& PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity)) {
|
||||
updateEntryWithSearchInfo(entryVersioned, searchInfo)
|
||||
} else {
|
||||
entrySelectedForAutofillSelection(entryVersioned)
|
||||
}
|
||||
},
|
||||
{ registerInfo ->
|
||||
if (!mReadOnly)
|
||||
entrySelectedForRegistration(entryVersioned, registerInfo)
|
||||
else
|
||||
finish()
|
||||
})
|
||||
} catch (e: ClassCastException) {
|
||||
@@ -552,6 +636,57 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
private fun entrySelectedForSave(entry: Entry, searchInfo: SearchInfo) {
|
||||
rebuildListNodes()
|
||||
// Save to update the entry
|
||||
EntryEditActivity.launchForSave(this@GroupActivity,
|
||||
entry, searchInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
}
|
||||
|
||||
private fun entrySelectedForKeyboardSelection(entry: Entry) {
|
||||
rebuildListNodes()
|
||||
// Populate Magikeyboard with entry
|
||||
mDatabase?.let { database ->
|
||||
populateKeyboardAndMoveAppToBackground(this,
|
||||
entry.getEntryInfo(database),
|
||||
intent)
|
||||
}
|
||||
onValidateSpecialMode()
|
||||
}
|
||||
|
||||
private fun entrySelectedForAutofillSelection(entry: Entry) {
|
||||
// Build response with the entry selected
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) {
|
||||
mDatabase?.let { database ->
|
||||
AutofillHelper.buildResponse(this,
|
||||
entry.getEntryInfo(database))
|
||||
}
|
||||
}
|
||||
onValidateSpecialMode()
|
||||
}
|
||||
|
||||
private fun entrySelectedForRegistration(entry: Entry, registerInfo: RegisterInfo?) {
|
||||
rebuildListNodes()
|
||||
// Registration to update the entry
|
||||
EntryEditActivity.launchForRegistration(this@GroupActivity,
|
||||
entry, registerInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
}
|
||||
|
||||
private fun updateEntryWithSearchInfo(entry: Entry, searchInfo: SearchInfo) {
|
||||
val newEntry = Entry(entry)
|
||||
newEntry.setEntryInfo(mDatabase, newEntry.getEntryInfo(mDatabase, true).apply {
|
||||
saveSearchInfo(mDatabase, searchInfo)
|
||||
})
|
||||
// In selection mode, it's forced read-only, so update not allowed
|
||||
mProgressDatabaseTaskProvider?.startDatabaseUpdateEntry(
|
||||
entry,
|
||||
newEntry,
|
||||
!mReadOnly && mAutoSaveEnable
|
||||
)
|
||||
}
|
||||
|
||||
private fun finishNodeAction() {
|
||||
actionNodeMode?.finish()
|
||||
}
|
||||
@@ -661,7 +796,7 @@ class GroupActivity : LockingActivity(),
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDeleteMenuClick(nodes: List<Node>): Boolean {
|
||||
private fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false): Boolean {
|
||||
val database = mDatabase
|
||||
|
||||
// If recycle bin enabled, ensure it exists
|
||||
@@ -681,13 +816,22 @@ class GroupActivity : LockingActivity(),
|
||||
}
|
||||
// else open the dialog to confirm deletion
|
||||
else {
|
||||
val deleteNodesDialogFragment: DeleteNodesDialogFragment =
|
||||
if (recycleBin) {
|
||||
EmptyRecycleBinDialogFragment.getInstance(nodes)
|
||||
} else {
|
||||
DeleteNodesDialogFragment.getInstance(nodes)
|
||||
.show(supportFragmentManager, "deleteNodesDialogFragment")
|
||||
}
|
||||
deleteNodesDialogFragment.show(supportFragmentManager, "deleteNodesDialogFragment")
|
||||
}
|
||||
finishNodeAction()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDeleteMenuClick(nodes: List<Node>): Boolean {
|
||||
return deleteNodes(nodes)
|
||||
}
|
||||
|
||||
override fun permanentlyDeleteNodes(nodes: List<Node>) {
|
||||
mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes(
|
||||
nodes,
|
||||
@@ -726,7 +870,7 @@ class GroupActivity : LockingActivity(),
|
||||
if (mReadOnly) {
|
||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||
}
|
||||
if (!mSelectionMode) {
|
||||
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||
MenuUtil.defaultMenuInflater(inflater, menu)
|
||||
}
|
||||
|
||||
@@ -776,7 +920,7 @@ class GroupActivity : LockingActivity(),
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
// Launch education screen
|
||||
Handler().post { performedNextEducation(GroupActivityEducation(this), menu) }
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(GroupActivityEducation(this), menu) }
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -856,7 +1000,7 @@ class GroupActivity : LockingActivity(),
|
||||
R.id.menu_empty_recycle_bin -> {
|
||||
mCurrentGroup?.getChildren()?.let { listChildren ->
|
||||
// Automatically delete all elements
|
||||
onDeleteMenuClick(listChildren)
|
||||
deleteNodes(listChildren, true)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -992,41 +1136,51 @@ class GroupActivity : LockingActivity(),
|
||||
assignGroupViewElements()
|
||||
}
|
||||
|
||||
private fun backToTheAppCaller() {
|
||||
if (mAutofillSelection) {
|
||||
// To get the app caller, only for autofill
|
||||
super.onBackPressed()
|
||||
} else {
|
||||
// To move the app in background
|
||||
moveTaskToBack(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (mListNodesFragment?.nodeActionSelectionMode == true) {
|
||||
finishNodeAction()
|
||||
} else {
|
||||
// Normal way when we are not in root
|
||||
if (mRootGroup != null && mRootGroup != mCurrentGroup) {
|
||||
super.onBackPressed()
|
||||
super.onRegularBackPressed()
|
||||
rebuildListNodes()
|
||||
}
|
||||
// Else in root, lock if needed
|
||||
else {
|
||||
intent.removeExtra(AUTO_SEARCH_KEY)
|
||||
intent.removeExtra(KEY_SEARCH_INFO)
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||
if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) {
|
||||
lockAndExit()
|
||||
super.onBackPressed()
|
||||
super.onRegularBackPressed()
|
||||
} else {
|
||||
// To restore standard mode
|
||||
EntrySelectionHelper.removeEntrySelectionModeFromIntent(intent)
|
||||
backToTheAppCaller()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeFragmentHistory() {
|
||||
val fragmentManager = supportFragmentManager
|
||||
if (mSelectionModeCountBackStack > 0) {
|
||||
for (selectionMode in 0 .. mSelectionModeCountBackStack) {
|
||||
fragmentManager.popBackStack()
|
||||
}
|
||||
}
|
||||
// Reinit the counter for navigation history
|
||||
mSelectionModeCountBackStack = 0
|
||||
}
|
||||
|
||||
override fun onValidateSpecialMode() {
|
||||
removeFragmentHistory()
|
||||
super.onValidateSpecialMode()
|
||||
}
|
||||
|
||||
override fun onCancelSpecialMode() {
|
||||
removeFragmentHistory()
|
||||
super.onCancelSpecialMode()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = GroupActivity::class.java.name
|
||||
@@ -1074,30 +1228,62 @@ class GroupActivity : LockingActivity(),
|
||||
* -------------------------
|
||||
*/
|
||||
fun launch(context: Context,
|
||||
autoSearch: Boolean = false,
|
||||
searchInfo: SearchInfo? = null,
|
||||
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
|
||||
readOnly: Boolean,
|
||||
autoSearch: Boolean = false) {
|
||||
checkTimeAndBuildIntent(context, null, readOnly) { intent ->
|
||||
searchInfo?.let {
|
||||
intent.putExtra(KEY_SEARCH_INFO, it)
|
||||
}
|
||||
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Search Launch
|
||||
* -------------------------
|
||||
*/
|
||||
fun launchForSearchResult(context: Context,
|
||||
readOnly: Boolean,
|
||||
searchInfo: SearchInfo,
|
||||
autoSearch: Boolean = false) {
|
||||
checkTimeAndBuildIntent(context, null, readOnly) { intent ->
|
||||
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
||||
EntrySelectionHelper.addSearchInfoInIntent(
|
||||
intent,
|
||||
searchInfo)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Search save Launch
|
||||
* -------------------------
|
||||
*/
|
||||
fun launchForSaveResult(context: Context,
|
||||
searchInfo: SearchInfo,
|
||||
autoSearch: Boolean = false) {
|
||||
checkTimeAndBuildIntent(context, null, false) { intent ->
|
||||
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
||||
EntrySelectionHelper.startActivityForSaveModeResult(context,
|
||||
intent,
|
||||
searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Keyboard Launch
|
||||
* -------------------------
|
||||
*/
|
||||
fun launchForEntrySelectionResult(context: Context,
|
||||
autoSearch: Boolean = false,
|
||||
fun launchForKeyboardSelectionResult(context: Context,
|
||||
readOnly: Boolean,
|
||||
searchInfo: SearchInfo? = null,
|
||||
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(context)) {
|
||||
autoSearch: Boolean = false) {
|
||||
checkTimeAndBuildIntent(context, null, readOnly) { intent ->
|
||||
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
||||
EntrySelectionHelper.startActivityForEntrySelectionResult(context, intent, searchInfo)
|
||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(context,
|
||||
intent,
|
||||
searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1108,14 +1294,189 @@ class GroupActivity : LockingActivity(),
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
fun launchForAutofillResult(activity: Activity,
|
||||
readOnly: Boolean,
|
||||
assistStructure: AssistStructure,
|
||||
autoSearch: Boolean = false,
|
||||
searchInfo: SearchInfo? = null,
|
||||
readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(activity)) {
|
||||
autoSearch: Boolean = false) {
|
||||
checkTimeAndBuildIntent(activity, null, readOnly) { intent ->
|
||||
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
||||
AutofillHelper.startActivityForAutofillResult(activity, intent, assistStructure, searchInfo)
|
||||
}
|
||||
AutofillHelper.startActivityForAutofillResult(activity,
|
||||
intent,
|
||||
assistStructure,
|
||||
searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Registration Launch
|
||||
* -------------------------
|
||||
*/
|
||||
fun launchForRegistration(context: Context,
|
||||
registerInfo: RegisterInfo? = null) {
|
||||
checkTimeAndBuildIntent(context, null, false) { intent ->
|
||||
intent.putExtra(AUTO_SEARCH_KEY, false)
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
|
||||
intent,
|
||||
registerInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Global Launch
|
||||
* -------------------------
|
||||
*/
|
||||
fun launch(activity: Activity,
|
||||
readOnly: Boolean,
|
||||
onValidateSpecialMode: () -> Unit,
|
||||
onCancelSpecialMode: () -> Unit,
|
||||
onLaunchActivitySpecialMode: () -> Unit) {
|
||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||
{
|
||||
GroupActivity.launch(activity,
|
||||
readOnly,
|
||||
true)
|
||||
},
|
||||
{ searchInfo ->
|
||||
SearchHelper.checkAutoSearchInfo(activity,
|
||||
Database.getInstance(),
|
||||
searchInfo,
|
||||
{ _ ->
|
||||
// Response is build
|
||||
GroupActivity.launchForSearchResult(activity,
|
||||
readOnly,
|
||||
searchInfo,
|
||||
true)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{
|
||||
// Here no search info found
|
||||
if (readOnly) {
|
||||
GroupActivity.launchForSearchResult(activity,
|
||||
readOnly,
|
||||
searchInfo,
|
||||
false)
|
||||
} else {
|
||||
GroupActivity.launchForSaveResult(activity,
|
||||
searchInfo,
|
||||
false)
|
||||
}
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{
|
||||
// Simply close if database not opened, normally not happened
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
)
|
||||
},
|
||||
{ searchInfo ->
|
||||
// Save info used with OTP
|
||||
if (!readOnly) {
|
||||
GroupActivity.launchForSaveResult(activity,
|
||||
searchInfo,
|
||||
false)
|
||||
onLaunchActivitySpecialMode()
|
||||
} else {
|
||||
Toast.makeText(activity.applicationContext,
|
||||
R.string.autofill_read_only_save,
|
||||
Toast.LENGTH_LONG)
|
||||
.show()
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
},
|
||||
{ searchInfo ->
|
||||
SearchHelper.checkAutoSearchInfo(activity,
|
||||
Database.getInstance(),
|
||||
searchInfo,
|
||||
{ items ->
|
||||
// Response is build
|
||||
if (items.size == 1) {
|
||||
populateKeyboardAndMoveAppToBackground(activity,
|
||||
items[0],
|
||||
activity.intent)
|
||||
onValidateSpecialMode()
|
||||
} else {
|
||||
// Select the one we want
|
||||
GroupActivity.launchForKeyboardSelectionResult(activity,
|
||||
readOnly,
|
||||
searchInfo,
|
||||
true)
|
||||
onLaunchActivitySpecialMode()
|
||||
}
|
||||
},
|
||||
{
|
||||
// Here no search info found, disable auto search
|
||||
GroupActivity.launchForKeyboardSelectionResult(activity,
|
||||
readOnly,
|
||||
searchInfo,
|
||||
false)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{
|
||||
// Simply close if database not opened, normally not happened
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
)
|
||||
},
|
||||
{ searchInfo, assistStructure ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
SearchHelper.checkAutoSearchInfo(activity,
|
||||
Database.getInstance(),
|
||||
searchInfo,
|
||||
{ items ->
|
||||
// Response is build
|
||||
AutofillHelper.buildResponse(activity, items)
|
||||
onValidateSpecialMode()
|
||||
},
|
||||
{
|
||||
// Here no search info found, disable auto search
|
||||
GroupActivity.launchForAutofillResult(activity,
|
||||
readOnly,
|
||||
assistStructure,
|
||||
searchInfo,
|
||||
false)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{
|
||||
// Simply close if database not opened, normally not happened
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
},
|
||||
{ registerInfo ->
|
||||
if (!readOnly) {
|
||||
SearchHelper.checkAutoSearchInfo(activity,
|
||||
Database.getInstance(),
|
||||
registerInfo?.searchInfo,
|
||||
{ _ ->
|
||||
// No auto search, it's a registration
|
||||
GroupActivity.launchForRegistration(activity,
|
||||
registerInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{
|
||||
// Here no search info found, disable auto search
|
||||
GroupActivity.launchForRegistration(activity,
|
||||
registerInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{
|
||||
// Simply close if database not opened, normally not happened
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Toast.makeText(activity.applicationContext,
|
||||
R.string.autofill_read_only_save,
|
||||
Toast.LENGTH_LONG)
|
||||
.show()
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import java.util.*
|
||||
@@ -69,10 +70,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
|
||||
|
||||
private var readOnly: Boolean = false
|
||||
get() {
|
||||
return field || selectionMode
|
||||
}
|
||||
private var selectionMode: Boolean = false
|
||||
private var specialMode: SpecialMode = SpecialMode.DEFAULT
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
|
||||
@@ -97,6 +95,12 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
nodeClickListener = null
|
||||
onScrollListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -189,7 +193,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
super.onResume()
|
||||
|
||||
activity?.intent?.let {
|
||||
selectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(it)
|
||||
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
|
||||
}
|
||||
|
||||
// Refresh data
|
||||
|
||||
@@ -30,16 +30,22 @@ import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
class MagikeyboardLauncherActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val database = Database.getInstance()
|
||||
val readOnly = database.isReadOnly
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
Database.getInstance(),
|
||||
database,
|
||||
null,
|
||||
{},
|
||||
{
|
||||
GroupActivity.launchForEntrySelectionResult(this)
|
||||
// Not called
|
||||
// if items found directly returns before calling this activity
|
||||
},
|
||||
{
|
||||
// Select if not found
|
||||
GroupActivity.launchForKeyboardSelectionResult(this, readOnly)
|
||||
},
|
||||
{
|
||||
// Pass extra to get entry
|
||||
FileDatabaseSelectActivity.launchForEntrySelectionResult(this)
|
||||
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this)
|
||||
}
|
||||
)
|
||||
finish()
|
||||
|
||||
@@ -27,6 +27,7 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.Log
|
||||
@@ -38,24 +39,25 @@ import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
|
||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||
import com.kunzisoft.keepass.activities.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.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
|
||||
import com.kunzisoft.keepass.biometric.BiometricUnlockDatabaseHelper
|
||||
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
|
||||
@@ -64,7 +66,9 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.MASTER_PASSWORD_KEY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
@@ -88,11 +92,12 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
|
||||
private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||
|
||||
private var mDefaultDatabase: Boolean = false
|
||||
private var mDatabaseFileUri: Uri? = null
|
||||
private var mDatabaseKeyFileUri: Uri? = null
|
||||
|
||||
private var mRememberKeyFile: Boolean = false
|
||||
private var mOpenFileHelper: OpenFileHelper? = null
|
||||
private var mSelectFileHelper: SelectFileHelper? = null
|
||||
|
||||
private var mPermissionAsked = false
|
||||
private var readOnly: Boolean = false
|
||||
@@ -136,9 +141,9 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||
|
||||
mOpenFileHelper = OpenFileHelper(this@PasswordActivity)
|
||||
mSelectFileHelper = SelectFileHelper(this@PasswordActivity)
|
||||
keyFileSelectionView?.apply {
|
||||
mOpenFileHelper?.openFileOnClickViewListener?.let {
|
||||
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
||||
setOnClickListener(it)
|
||||
setOnLongClickListener(it)
|
||||
}
|
||||
@@ -169,8 +174,13 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
|
||||
}
|
||||
|
||||
// Observe if default database
|
||||
databaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
||||
mDefaultDatabase = isDefaultDatabase
|
||||
}
|
||||
|
||||
// Observe database file change
|
||||
databaseFileViewModel.databaseFileLoaded.observe(this, Observer { databaseFile ->
|
||||
databaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
|
||||
// Force read only if the file does not exists
|
||||
mForceReadOnly = databaseFile?.let {
|
||||
!it.databaseFileExists
|
||||
@@ -190,17 +200,17 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
filenameView?.text = databaseFile?.databaseAlias ?: ""
|
||||
|
||||
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
|
||||
})
|
||||
}
|
||||
|
||||
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
|
||||
onActionFinish = { actionTask, result ->
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_LOAD_TASK -> {
|
||||
// Recheck biometric if error
|
||||
// Recheck advanced unlock if error
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (PreferencesUtil.isBiometricUnlockEnable(this@PasswordActivity)) {
|
||||
if (PreferencesUtil.isAdvancedUnlockEnable(this@PasswordActivity)) {
|
||||
// Stay with the same mode and init it
|
||||
advancedUnlockedManager?.initBiometricMode()
|
||||
advancedUnlockedManager?.initAdvancedUnlockMode()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,8 +226,9 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
if (resultException != null) {
|
||||
resultError = resultException.getLocalizedMessage(resources)
|
||||
|
||||
when (resultException) {
|
||||
is DuplicateUuidDatabaseException -> {
|
||||
// Relaunch loading if we need to fix UUID
|
||||
if (resultException is DuplicateUuidDatabaseException) {
|
||||
showLoadDatabaseDuplicateUuidMessage {
|
||||
|
||||
var databaseUri: Uri? = null
|
||||
@@ -245,6 +256,13 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
is FileNotFoundDatabaseException -> {
|
||||
// Remove this default database inaccessible
|
||||
if (mDefaultDatabase) {
|
||||
databaseFileViewModel.removeDefaultDatabase()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show error message
|
||||
@@ -273,6 +291,9 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
|
||||
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
|
||||
}
|
||||
mDatabaseFileUri?.let {
|
||||
databaseFileViewModel.checkIfIsDefaultDatabase(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
@@ -281,74 +302,22 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
}
|
||||
|
||||
private fun launchGroupActivity() {
|
||||
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
|
||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||
{
|
||||
GroupActivity.launch(this@PasswordActivity,
|
||||
true,
|
||||
searchInfo,
|
||||
readOnly)
|
||||
// Finish activity if no search info
|
||||
if (searchInfo != null) {
|
||||
finish()
|
||||
}
|
||||
},
|
||||
{
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
Database.getInstance(),
|
||||
searchInfo,
|
||||
{ items ->
|
||||
// Response is build
|
||||
if (items.size == 1) {
|
||||
populateKeyboardAndMoveAppToBackground(this@PasswordActivity,
|
||||
items[0],
|
||||
intent)
|
||||
} else {
|
||||
// Select the one we want
|
||||
GroupActivity.launchForEntrySelectionResult(this,
|
||||
true,
|
||||
searchInfo)
|
||||
}
|
||||
},
|
||||
{
|
||||
// Here no search info found, disable auto search
|
||||
GroupActivity.launchForEntrySelectionResult(this@PasswordActivity,
|
||||
false,
|
||||
searchInfo,
|
||||
readOnly)
|
||||
},
|
||||
{
|
||||
// Simply close if database not opened, normally not happened
|
||||
}
|
||||
)
|
||||
// Do not keep history
|
||||
finish()
|
||||
},
|
||||
{ assistStructure ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
Database.getInstance(),
|
||||
searchInfo,
|
||||
{ items ->
|
||||
// Response is build
|
||||
AutofillHelper.buildResponse(this, items)
|
||||
finish()
|
||||
},
|
||||
{
|
||||
// Here no search info found, disable auto search
|
||||
GroupActivity.launchForAutofillResult(this@PasswordActivity,
|
||||
assistStructure,
|
||||
false,
|
||||
searchInfo,
|
||||
readOnly)
|
||||
},
|
||||
{
|
||||
// Simply close if database not opened, normally not happened
|
||||
finish()
|
||||
}
|
||||
GroupActivity.launch(this,
|
||||
readOnly,
|
||||
{ onValidateSpecialMode() },
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
override fun onValidateSpecialMode() {
|
||||
super.onValidateSpecialMode()
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onCancelSpecialMode() {
|
||||
super.onCancelSpecialMode()
|
||||
finish()
|
||||
}
|
||||
|
||||
private val onEditorActionListener = object : TextView.OnEditorActionListener {
|
||||
@@ -387,7 +356,6 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
false
|
||||
else
|
||||
mAllowAutoOpenBiometricPrompt
|
||||
|
||||
mDatabaseFileUri?.let { databaseFileUri ->
|
||||
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||
}
|
||||
@@ -418,10 +386,10 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
|
||||
} else {
|
||||
// Init Biometric elements
|
||||
var biometricInitialize = false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (PreferencesUtil.isBiometricUnlockEnable(this)) {
|
||||
if (advancedUnlockedManager == null && databaseFileUri != null) {
|
||||
if (PreferencesUtil.isAdvancedUnlockEnable(this)) {
|
||||
if (advancedUnlockedManager == null
|
||||
&& databaseFileUri != null) {
|
||||
advancedUnlockedManager = AdvancedUnlockedManager(this,
|
||||
databaseFileUri,
|
||||
advancedUnlockInfoView,
|
||||
@@ -447,15 +415,16 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
}
|
||||
})
|
||||
}
|
||||
advancedUnlockedManager?.isBiometricPromptAutoOpenEnable = mAllowAutoOpenBiometricPrompt
|
||||
advancedUnlockedManager?.isBiometricPromptAutoOpenEnable =
|
||||
mAllowAutoOpenBiometricPrompt && mProgressDatabaseTaskProvider?.isBinded() != true
|
||||
advancedUnlockedManager?.checkBiometricAvailability()
|
||||
biometricInitialize = true
|
||||
} else {
|
||||
advancedUnlockedManager?.destroy()
|
||||
advancedUnlockInfoView?.visibility = View.GONE
|
||||
advancedUnlockedManager?.destroy()
|
||||
advancedUnlockedManager = null
|
||||
}
|
||||
}
|
||||
if (!biometricInitialize) {
|
||||
if (advancedUnlockedManager == null) {
|
||||
checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
||||
}
|
||||
checkboxKeyFileView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
||||
@@ -565,6 +534,15 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
clearCredentialsViews()
|
||||
}
|
||||
|
||||
if (readOnly && (
|
||||
mSpecialMode == SpecialMode.SAVE
|
||||
|| mSpecialMode == SpecialMode.REGISTRATION)
|
||||
) {
|
||||
Log.e(TAG, getString(R.string.autofill_read_only_save))
|
||||
Snackbar.make(activity_password_coordinator_layout,
|
||||
R.string.autofill_read_only_save,
|
||||
Snackbar.LENGTH_LONG).asError().show()
|
||||
} else {
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
// Show the progress dialog and load the database
|
||||
showProgressDialogAndLoadDatabase(
|
||||
@@ -576,6 +554,7 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
|
||||
password: String?,
|
||||
@@ -603,13 +582,13 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
val inflater = menuInflater
|
||||
// Read menu
|
||||
inflater.inflate(R.menu.open_file, menu)
|
||||
if (mSelectionMode || mForceReadOnly) {
|
||||
if (mForceReadOnly) {
|
||||
menu.removeItem(R.id.menu_open_file_read_mode_key)
|
||||
} else {
|
||||
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
|
||||
}
|
||||
|
||||
if (!mSelectionMode) {
|
||||
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||
MenuUtil.defaultMenuInflater(inflater, menu)
|
||||
}
|
||||
|
||||
@@ -660,7 +639,7 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
if (!performedEductionInProgress) {
|
||||
performedEductionInProgress = true
|
||||
// Show education views
|
||||
Handler().post { performedNextEducation(PasswordActivityEducation(this), menu) }
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(PasswordActivityEducation(this), menu) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -682,19 +661,24 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
|
||||
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
|
||||
{
|
||||
onOptionsItemSelected(menu.findItem(R.id.menu_open_file_read_mode_key))
|
||||
try {
|
||||
menu.findItem(R.id.menu_open_file_read_mode_key)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to find read mode menu")
|
||||
}
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
},
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
})
|
||||
|
||||
if (!readOnlyEducationPerformed) {
|
||||
val biometricCanAuthenticate = BiometricManager.from(this).canAuthenticate()
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& PreferencesUtil.isBiometricUnlockEnable(applicationContext)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& !readOnlyEducationPerformed) {
|
||||
val biometricCanAuthenticate = BiometricUnlockDatabaseHelper.canAuthenticate(this)
|
||||
PreferencesUtil.isAdvancedUnlockEnable(applicationContext)
|
||||
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
||||
&& advancedUnlockInfoView != null && advancedUnlockInfoView?.unlockIconImageView != null
|
||||
&& advancedUnlockInfoView != null && advancedUnlockInfoView?.visibility == View.VISIBLE
|
||||
&& advancedUnlockInfoView?.unlockIconImageView != null
|
||||
&& passwordActivityEducation.checkAndPerformedBiometricEducation(advancedUnlockInfoView?.unlockIconImageView!!,
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
@@ -724,8 +708,8 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
readOnly = !readOnly
|
||||
changeOpenFileReadIcon(item)
|
||||
}
|
||||
R.id.menu_biometric_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
advancedUnlockedManager?.deleteEntryKey()
|
||||
R.id.menu_keystore_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
advancedUnlockedManager?.deleteEncryptedDatabaseKey()
|
||||
}
|
||||
else -> return MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
||||
}
|
||||
@@ -747,7 +731,7 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
}
|
||||
|
||||
var keyFileResult = false
|
||||
mOpenFileHelper?.let {
|
||||
mSelectFileHelper?.let {
|
||||
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
@@ -761,7 +745,7 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
when (resultCode) {
|
||||
LockingActivity.RESULT_EXIT_LOCK -> {
|
||||
clearCredentialsViews()
|
||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
||||
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this))
|
||||
}
|
||||
Activity.RESULT_CANCELED -> {
|
||||
clearCredentialsViews()
|
||||
@@ -801,16 +785,49 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launch(
|
||||
activity: Activity,
|
||||
fun launch(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Share Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForSearchResult(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
searchInfo: SearchInfo?) {
|
||||
searchInfo: SearchInfo) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
searchInfo?.let {
|
||||
intent.putExtra(KEY_SEARCH_INFO, it)
|
||||
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||
activity,
|
||||
intent,
|
||||
searchInfo)
|
||||
}
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Save Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForSaveResult(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
searchInfo: SearchInfo) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
EntrySelectionHelper.startActivityForSaveModeResult(
|
||||
activity,
|
||||
intent,
|
||||
searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -821,13 +838,12 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForKeyboardResult(
|
||||
activity: Activity,
|
||||
fun launchForKeyboardResult(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
searchInfo: SearchInfo?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
EntrySelectionHelper.startActivityForEntrySelectionResult(
|
||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
|
||||
activity,
|
||||
intent,
|
||||
searchInfo)
|
||||
@@ -842,13 +858,11 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForAutofillResult(
|
||||
activity: Activity,
|
||||
fun launchForAutofillResult(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
assistStructure: AssistStructure?,
|
||||
assistStructure: AssistStructure,
|
||||
searchInfo: SearchInfo?) {
|
||||
if (assistStructure != null) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
AutofillHelper.startActivityForAutofillResult(
|
||||
activity,
|
||||
@@ -856,8 +870,81 @@ open class PasswordActivity : SpecialModeActivity() {
|
||||
assistStructure,
|
||||
searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Registration Launch
|
||||
* -------------------------
|
||||
*/
|
||||
fun launchForRegistration(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
registerInfo: RegisterInfo?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||
activity,
|
||||
intent,
|
||||
registerInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Global Launch
|
||||
* -------------------------
|
||||
*/
|
||||
fun launch(activity: Activity,
|
||||
databaseUri: Uri,
|
||||
keyFile: Uri?,
|
||||
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
||||
onCancelSpecialMode: () -> Unit,
|
||||
onLaunchActivitySpecialMode: () -> Unit) {
|
||||
|
||||
try {
|
||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||
{
|
||||
PasswordActivity.launch(activity,
|
||||
databaseUri, keyFile)
|
||||
},
|
||||
{ searchInfo -> // Search Action
|
||||
PasswordActivity.launchForSearchResult(activity,
|
||||
databaseUri, keyFile,
|
||||
searchInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo -> // Save Action
|
||||
PasswordActivity.launchForSaveResult(activity,
|
||||
databaseUri, keyFile,
|
||||
searchInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo -> // Keyboard Selection Action
|
||||
PasswordActivity.launchForKeyboardResult(activity,
|
||||
databaseUri, keyFile,
|
||||
searchInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo, assistStructure -> // Autofill Selection Action
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
PasswordActivity.launchForAutofillResult(activity,
|
||||
databaseUri, keyFile,
|
||||
assistStructure,
|
||||
searchInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
} else {
|
||||
launch(activity, databaseFile, keyFile, searchInfo)
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
},
|
||||
{ registerInfo -> // Registration Action
|
||||
PasswordActivity.launchForRegistration(activity,
|
||||
databaseUri, keyFile,
|
||||
registerInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
}
|
||||
)
|
||||
} catch (e: FileNotFoundException) {
|
||||
fileNoFoundAction(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,16 +25,19 @@ import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.text.Editable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||
|
||||
class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
@@ -56,7 +59,11 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
|
||||
private var mListener: AssignPasswordDialogListener? = null
|
||||
|
||||
private var mOpenFileHelper: OpenFileHelper? = null
|
||||
private var mSelectFileHelper: SelectFileHelper? = null
|
||||
|
||||
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
|
||||
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
||||
private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null
|
||||
|
||||
private val passwordTextWatcher = object : TextWatcher {
|
||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
@@ -85,6 +92,17 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
mEmptyPasswordConfirmationDialog?.dismiss()
|
||||
mEmptyPasswordConfirmationDialog = null
|
||||
mNoKeyConfirmationDialog?.dismiss()
|
||||
mNoKeyConfirmationDialog = null
|
||||
mEmptyKeyFileConfirmationDialog?.dismiss()
|
||||
mEmptyKeyFileConfirmationDialog = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
|
||||
@@ -99,11 +117,15 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
|
||||
rootView = inflater.inflate(R.layout.fragment_set_password, null)
|
||||
builder.setView(rootView)
|
||||
.setTitle(R.string.assign_master_key)
|
||||
// Add action buttons
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
|
||||
val credentialsInfo: ImageView? = rootView?.findViewById(R.id.credentials_information)
|
||||
credentialsInfo?.setOnClickListener {
|
||||
UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
|
||||
}
|
||||
|
||||
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
||||
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
|
||||
passwordView = rootView?.findViewById(R.id.pass_password)
|
||||
@@ -113,10 +135,10 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
||||
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
||||
|
||||
mOpenFileHelper = OpenFileHelper(this)
|
||||
mSelectFileHelper = SelectFileHelper(this)
|
||||
keyFileSelectionView?.apply {
|
||||
setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener)
|
||||
setOnLongClickListener(mOpenFileHelper?.openFileOnClickViewListener)
|
||||
setOnClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
||||
setOnLongClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
||||
}
|
||||
|
||||
val dialog = builder.create()
|
||||
@@ -129,7 +151,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
mMasterPassword = ""
|
||||
mKeyFile = null
|
||||
|
||||
var error = verifyPassword() || verifyFile()
|
||||
var error = verifyPassword() || verifyKeyFile()
|
||||
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
|
||||
error = true
|
||||
if (allowNoMasterKey)
|
||||
@@ -199,7 +221,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
return error
|
||||
}
|
||||
|
||||
private fun verifyFile(): Boolean {
|
||||
private fun verifyKeyFile(): Boolean {
|
||||
var error = false
|
||||
if (keyFileCheckBox != null
|
||||
&& keyFileCheckBox!!.isChecked) {
|
||||
@@ -219,7 +241,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
val builder = AlertDialog.Builder(it)
|
||||
builder.setMessage(R.string.warning_empty_password)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
if (!verifyFile()) {
|
||||
if (!verifyKeyFile()) {
|
||||
mListener?.onAssignKeyDialogPositiveClick(
|
||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
||||
@@ -227,7 +249,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
builder.create().show()
|
||||
mEmptyPasswordConfirmationDialog = builder.create()
|
||||
mEmptyPasswordConfirmationDialog?.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,18 +265,44 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
this@AssignMasterKeyDialogFragment.dismiss()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
builder.create().show()
|
||||
mNoKeyConfirmationDialog = builder.create()
|
||||
mNoKeyConfirmationDialog?.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showEmptyKeyFileConfirmationDialog() {
|
||||
activity?.let {
|
||||
val builder = AlertDialog.Builder(it)
|
||||
builder.setMessage(SpannableStringBuilder().apply {
|
||||
append(getString(R.string.warning_empty_keyfile))
|
||||
append("\n\n")
|
||||
append(getString(R.string.warning_empty_keyfile_explanation))
|
||||
append("\n\n")
|
||||
append(getString(R.string.warning_sure_add_file))
|
||||
})
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
keyFileCheckBox?.isChecked = false
|
||||
keyFileSelectionView?.uri = null
|
||||
}
|
||||
mEmptyKeyFileConfirmationDialog = builder.create()
|
||||
mEmptyKeyFileConfirmationDialog?.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
||||
) { uri ->
|
||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||
uri?.let { pathUri ->
|
||||
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
||||
keyFileSelectionView?.error = null
|
||||
keyFileCheckBox?.isChecked = true
|
||||
keyFileSelectionView?.uri = pathUri
|
||||
if (lengthFile <= 0L) {
|
||||
showEmptyKeyFileConfirmationDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,11 @@ class DatePickerFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
// Create a new instance of DatePickerDialog and return it
|
||||
return context?.let {
|
||||
|
||||
@@ -31,7 +31,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
||||
|
||||
class DeleteNodesDialogFragment : DialogFragment() {
|
||||
open class DeleteNodesDialogFragment : DialogFragment() {
|
||||
|
||||
private var mNodesToDelete: List<Node> = ArrayList()
|
||||
private var mListener: DeleteNodeListener? = null
|
||||
@@ -46,6 +46,15 @@ class DeleteNodesDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
protected open fun retrieveMessage(): String {
|
||||
return getString(R.string.warning_permanently_delete_nodes)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
||||
arguments?.apply {
|
||||
@@ -63,11 +72,11 @@ class DeleteNodesDialogFragment : DialogFragment() {
|
||||
// Use the Builder class for convenient dialog construction
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
builder.setMessage(getString(R.string.warning_permanently_delete_nodes))
|
||||
builder.setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
builder.setMessage(retrieveMessage())
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mListener?.permanentlyDeleteNodes(mNodesToDelete)
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.no) { _, _ -> dismiss() }
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||
// Create the AlertDialog object and return it
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||
|
||||
class EmptyRecycleBinDialogFragment : DeleteNodesDialogFragment() {
|
||||
|
||||
override fun retrieveMessage(): String {
|
||||
return getString(R.string.warning_empty_recycle_bin)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getInstance(nodesToDelete: List<Node>): EmptyRecycleBinDialogFragment {
|
||||
return EmptyRecycleBinDialogFragment().apply {
|
||||
arguments = getBundleFromListNodes(nodesToDelete)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,24 +22,31 @@ package com.kunzisoft.keepass.activities.dialogs
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.Button
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.model.Field
|
||||
|
||||
|
||||
class EntryCustomFieldDialogFragment: DialogFragment() {
|
||||
|
||||
private var oldField: Field? = null
|
||||
|
||||
private var entryCustomFieldListener: EntryCustomFieldListener? = null
|
||||
|
||||
private var customFieldLabelContainer: TextInputLayout? = null
|
||||
private var customFieldLabel: TextView? = null
|
||||
private var customFieldDeleteButton: ImageView? = null
|
||||
private var customFieldProtectionButton: CompoundButton? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
@@ -53,22 +60,37 @@ class EntryCustomFieldDialogFragment: DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
entryCustomFieldListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_entry_new_field, null)
|
||||
customFieldLabelContainer = root?.findViewById(R.id.entry_custom_field_label_container)
|
||||
customFieldLabel = root?.findViewById(R.id.entry_custom_field_label)
|
||||
customFieldDeleteButton = root?.findViewById(R.id.entry_custom_field_delete)
|
||||
customFieldProtectionButton = root?.findViewById(R.id.entry_custom_field_protection)
|
||||
|
||||
oldField = arguments?.getParcelable(KEY_FIELD)
|
||||
oldField?.let { oldCustomField ->
|
||||
customFieldLabel?.text = oldCustomField.name
|
||||
customFieldProtectionButton?.isChecked = oldCustomField.protectedValue.isProtected
|
||||
|
||||
customFieldDeleteButton?.visibility = View.VISIBLE
|
||||
customFieldDeleteButton?.setOnClickListener {
|
||||
entryCustomFieldListener?.onDeleteCustomFieldApproved(oldCustomField)
|
||||
(dialog as AlertDialog?)?.dismiss()
|
||||
}
|
||||
} ?: run {
|
||||
customFieldDeleteButton?.visibility = View.GONE
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setView(root)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
entryCustomFieldListener?.onNewCustomFieldCanceled(
|
||||
customFieldLabel?.text.toString(),
|
||||
customFieldProtectionButton?.isChecked == true
|
||||
)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
val dialogCreated = builder.create()
|
||||
|
||||
customFieldLabel?.requestFocus()
|
||||
@@ -102,10 +124,19 @@ class EntryCustomFieldDialogFragment: DialogFragment() {
|
||||
|
||||
private fun approveIfValid() {
|
||||
if (isValid()) {
|
||||
entryCustomFieldListener?.onNewCustomFieldApproved(
|
||||
customFieldLabel?.text.toString(),
|
||||
customFieldProtectionButton?.isChecked == true
|
||||
oldField?.let {
|
||||
// New property with old value
|
||||
entryCustomFieldListener?.onEditCustomFieldApproved(it,
|
||||
Field(customFieldLabel?.text?.toString() ?: "",
|
||||
ProtectedString(customFieldProtectionButton?.isChecked == true,
|
||||
it.protectedValue.stringValue))
|
||||
)
|
||||
} ?: run {
|
||||
entryCustomFieldListener?.onNewCustomFieldApproved(
|
||||
Field(customFieldLabel?.text?.toString() ?: "",
|
||||
ProtectedString(customFieldProtectionButton?.isChecked == true))
|
||||
)
|
||||
}
|
||||
(dialog as AlertDialog?)?.dismiss()
|
||||
}
|
||||
}
|
||||
@@ -127,13 +158,25 @@ class EntryCustomFieldDialogFragment: DialogFragment() {
|
||||
}
|
||||
|
||||
interface EntryCustomFieldListener {
|
||||
fun onNewCustomFieldApproved(label: String, protection: Boolean)
|
||||
fun onNewCustomFieldCanceled(label: String, protection: Boolean)
|
||||
fun onNewCustomFieldApproved(newField: Field)
|
||||
fun onEditCustomFieldApproved(oldField: Field, newField: Field)
|
||||
fun onDeleteCustomFieldApproved(oldField: Field)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_FIELD = "KEY_FIELD"
|
||||
|
||||
fun getInstance(): EntryCustomFieldDialogFragment {
|
||||
return EntryCustomFieldDialogFragment()
|
||||
}
|
||||
|
||||
fun getInstance(field: Field): EntryCustomFieldDialogFragment {
|
||||
return EntryCustomFieldDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(KEY_FIELD, field)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
/**
|
||||
* Custom Dialog to confirm big file to upload
|
||||
*/
|
||||
class FileTooBigDialogFragment : DialogFragment() {
|
||||
|
||||
private var mActionChooseListener: ActionChooseListener? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
// Verify that the host activity implements the callback interface
|
||||
try {
|
||||
mActionChooseListener = context as ActionChooseListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + ActionChooseListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mActionChooseListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
// Use the Builder class for convenient dialog construction
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setMessage(SpannableStringBuilder().apply {
|
||||
append(getString(R.string.warning_file_too_big))
|
||||
append("\n\n")
|
||||
append(getString(R.string.warning_sure_add_file))
|
||||
})
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mActionChooseListener?.onValidateUploadFileTooBig(
|
||||
arguments?.getParcelable(KEY_FILE_URI),
|
||||
arguments?.getString(KEY_FILE_NAME))
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
dismiss()
|
||||
}
|
||||
// Create the AlertDialog object and return it
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
interface ActionChooseListener {
|
||||
fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MAX_WARNING_BINARY_FILE = 5242880
|
||||
|
||||
private const val KEY_FILE_URI = "KEY_FILE_URI"
|
||||
private const val KEY_FILE_NAME = "KEY_FILE_NAME"
|
||||
|
||||
fun build(attachmentToUploadUri: Uri,
|
||||
fileName: String): FileTooBigDialogFragment {
|
||||
val fragment = FileTooBigDialogFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
putParcelable(KEY_FILE_URI, attachmentToUploadUri)
|
||||
putString(KEY_FILE_NAME, fileName)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,13 +26,11 @@ import com.google.android.material.textfield.TextInputLayout
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.EditText
|
||||
import android.widget.SeekBar
|
||||
import android.widget.*
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.password.PasswordGenerator
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||
|
||||
class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
@@ -64,6 +62,11 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
@@ -73,6 +76,15 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
passwordInputLayoutView = root?.findViewById(R.id.password_input_layout)
|
||||
passwordView = root?.findViewById(R.id.password)
|
||||
passwordView?.applyFontVisibility()
|
||||
val passwordCopyView: ImageView? = root?.findViewById(R.id.password_copy_button)
|
||||
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyPasswordAndProtectedFields(activity))
|
||||
View.VISIBLE else View.GONE
|
||||
val clipboardHelper = ClipboardHelper(activity)
|
||||
passwordCopyView?.setOnClickListener {
|
||||
clipboardHelper.timeoutCopyToClipboard(passwordView!!.text.toString(),
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_password)))
|
||||
}
|
||||
|
||||
lengthTextView = root?.findViewById(R.id.length)
|
||||
|
||||
|
||||
@@ -73,7 +73,11 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + GroupEditDialogFragment::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
editGroupListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
||||
@@ -34,6 +34,7 @@ import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.widget.ImageViewCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import com.kunzisoft.keepass.icons.IconPack
|
||||
@@ -56,6 +57,11 @@ class IconPickerDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
iconPickerListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
@@ -132,7 +138,7 @@ class IconPickerDialogFragment : DialogFragment() {
|
||||
return bundle.getParcelable(KEY_ICON_STANDARD)
|
||||
}
|
||||
|
||||
fun launch(activity: AppCompatActivity) {
|
||||
fun launch(activity: FragmentActivity) {
|
||||
// Create an instance of the dialog fragment and show it
|
||||
val dialog = IconPickerDialogFragment()
|
||||
dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment")
|
||||
|
||||
@@ -21,20 +21,51 @@ package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
class PasswordEncodingDialogFragment : DialogFragment() {
|
||||
|
||||
var positiveButtonClickListener: DialogInterface.OnClickListener? = null
|
||||
private var mListener: Listener? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
try {
|
||||
mListener = context as Listener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + Listener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
||||
val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY)
|
||||
val masterPasswordChecked: Boolean = savedInstanceState?.getBoolean(MASTER_PASSWORD_CHECKED_KEY) ?: false
|
||||
val masterPassword: String? = savedInstanceState?.getString(MASTER_PASSWORD_KEY)
|
||||
val keyFileChecked: Boolean = savedInstanceState?.getBoolean(KEY_FILE_CHECKED_KEY) ?: false
|
||||
val keyFile: Uri? = savedInstanceState?.getParcelable(KEY_FILE_URI_KEY)
|
||||
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning)
|
||||
builder.setPositiveButton(android.R.string.ok, positiveButtonClickListener)
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mListener?.onPasswordEncodingValidateListener(
|
||||
databaseUri,
|
||||
masterPasswordChecked,
|
||||
masterPassword,
|
||||
keyFileChecked,
|
||||
keyFile
|
||||
)
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
|
||||
|
||||
return builder.create()
|
||||
@@ -42,5 +73,36 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
|
||||
interface Listener {
|
||||
fun onPasswordEncodingValidateListener(databaseUri: Uri?,
|
||||
masterPasswordChecked: Boolean,
|
||||
masterPassword: String?,
|
||||
keyFileChecked: Boolean,
|
||||
keyFile: Uri?)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
|
||||
private const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY"
|
||||
private const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY"
|
||||
private const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY"
|
||||
private const val KEY_FILE_URI_KEY = "KEY_FILE_URI_KEY"
|
||||
|
||||
fun getInstance(databaseUri: Uri,
|
||||
masterPasswordChecked: Boolean,
|
||||
masterPassword: String?,
|
||||
keyFileChecked: Boolean,
|
||||
keyFile: Uri?): SortDialogFragment {
|
||||
val fragment = SortDialogFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
putParcelable(DATABASE_URI_KEY, databaseUri)
|
||||
putBoolean(MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
||||
putString(MASTER_PASSWORD_KEY, masterPassword)
|
||||
putBoolean(KEY_FILE_CHECKED_KEY, keyFileChecked)
|
||||
putParcelable(KEY_FILE_URI_KEY, keyFile)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
|
||||
/**
|
||||
* Custom Dialog to confirm big file to upload
|
||||
*/
|
||||
class ReplaceFileDialogFragment : DialogFragment() {
|
||||
|
||||
private var mActionChooseListener: ActionChooseListener? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
// Verify that the host activity implements the callback interface
|
||||
try {
|
||||
mActionChooseListener = context as ActionChooseListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + ActionChooseListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mActionChooseListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
// Use the Builder class for convenient dialog construction
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setMessage(SpannableStringBuilder().apply {
|
||||
append(getString(R.string.warning_replace_file))
|
||||
append("\n\n")
|
||||
append(getString(R.string.warning_sure_add_file))
|
||||
})
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mActionChooseListener?.onValidateReplaceFile(
|
||||
arguments?.getParcelable(KEY_FILE_URI),
|
||||
arguments?.getParcelable(KEY_ENTRY_ATTACHMENT))
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
dismiss()
|
||||
}
|
||||
// Create the AlertDialog object and return it
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
interface ActionChooseListener {
|
||||
fun onValidateReplaceFile(attachmentToUploadUri: Uri?, attachment: Attachment?)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_FILE_URI = "KEY_FILE_URI"
|
||||
private const val KEY_ENTRY_ATTACHMENT = "KEY_ENTRY_ATTACHMENT"
|
||||
|
||||
fun build(attachmentToUploadUri: Uri,
|
||||
attachment: Attachment): ReplaceFileDialogFragment {
|
||||
val fragment = ReplaceFileDialogFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
putParcelable(KEY_FILE_URI, attachmentToUploadUri)
|
||||
putParcelable(KEY_ENTRY_ATTACHMENT, attachment)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,11 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mCreateOTPElementListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
||||
|
||||
@@ -54,6 +54,11 @@ class SortDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
@@ -25,6 +25,11 @@ class TimePickerFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
// Create a new instance of DatePickerDialog and return it
|
||||
return context?.let {
|
||||
|
||||
@@ -90,12 +90,12 @@ class UnavailableFeatureDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
if (apiName.isEmpty()) {
|
||||
val mapper = arrayOf("ANDROID BASE", "ANDROID BASE 1.1", "CUPCAKE", "DONUT", "ECLAIR", "ECLAIR_0_1", "ECLAIR_MR1", "FROYO", "GINGERBREAD", "GINGERBREAD_MR1", "HONEYCOMB", "HONEYCOMB_MR1", "HONEYCOMB_MR2", "ICE_CREAM_SANDWICH", "ICE_CREAM_SANDWICH_MR1", "JELLY_BEAN", "JELLY_BEAN", "JELLY_BEAN", "KITKAT", "KITKAT", "LOLLIPOOP", "LOLLIPOOP_MR1", "MARSHMALLOW", "NOUGAT", "NOUGAT", "OREO", "OREO")
|
||||
val mapper = arrayOf("ANDROID BASE", "ANDROID BASE 1.1", "CUPCAKE", "DONUT", "ECLAIR", "ECLAIR_0_1", "ECLAIR_MR1", "FROYO", "GINGERBREAD", "GINGERBREAD_MR1", "HONEYCOMB", "HONEYCOMB_MR1", "HONEYCOMB_MR2", "ICE_CREAM_SANDWICH", "ICE_CREAM_SANDWICH_MR1", "JELLY_BEAN", "JELLY_BEAN", "JELLY_BEAN", "KITKAT", "KITKAT", "LOLLIPOOP", "LOLLIPOOP_MR1", "MARSHMALLOW", "NOUGAT", "NOUGAT", "OREO", "OREO", "PIE", "", "")
|
||||
val index = apiNumber - 1
|
||||
apiName = if (index < mapper.size) mapper[index] else "UNKNOWN_VERSION"
|
||||
}
|
||||
if (version.isEmpty()) {
|
||||
val versions = arrayOf("1.0", "1.1", "1.5", "1.6", "2.0", "2.0.1", "2.1", "2.2.X", "2.3", "2.3.3", "3.0", "3.1", "3.2.0", "4.0.1", "4.0.3", "4.1.0", "4.2.0", "4.3.0", "4.4", "4.4", "5.0", "5.1", "6.0", "7.0", "7.1", "8.0.0", "8.1.0")
|
||||
val versions = arrayOf("1.0", "1.1", "1.5", "1.6", "2.0", "2.0.1", "2.1", "2.2.X", "2.3", "2.3.3", "3.0", "3.1", "3.2.0", "4.0.1", "4.0.3", "4.1.0", "4.2.0", "4.3.0", "4.4", "4.4", "5.0", "5.1", "6.0", "7.0", "7.1", "8.0.0", "8.1.0", "9", "10", "11")
|
||||
val index = apiNumber - 1
|
||||
version = if (index < versions.size) versions[index] else "UNKNOWN_VERSION"
|
||||
}
|
||||
|
||||
@@ -24,54 +24,186 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import java.io.Serializable
|
||||
|
||||
object EntrySelectionHelper {
|
||||
|
||||
private const val EXTRA_ENTRY_SELECTION_MODE = "com.kunzisoft.keepass.extra.ENTRY_SELECTION_MODE"
|
||||
private const val DEFAULT_ENTRY_SELECTION_MODE = false
|
||||
// Key to retrieve search in intent
|
||||
const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
||||
private const val KEY_SPECIAL_MODE = "com.kunzisoft.keepass.extra.SPECIAL_MODE"
|
||||
private const val KEY_TYPE_MODE = "com.kunzisoft.keepass.extra.TYPE_MODE"
|
||||
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
|
||||
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
|
||||
|
||||
fun startActivityForEntrySelectionResult(context: Context,
|
||||
fun startActivityForSearchModeResult(context: Context,
|
||||
intent: Intent,
|
||||
searchInfo: SearchInfo?) {
|
||||
addEntrySelectionModeExtraInIntent(intent)
|
||||
searchInfo?.let {
|
||||
intent.putExtra(KEY_SEARCH_INFO, it)
|
||||
}
|
||||
searchInfo: SearchInfo) {
|
||||
addSpecialModeInIntent(intent, SpecialMode.SEARCH)
|
||||
addSearchInfoInIntent(intent, searchInfo)
|
||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun addEntrySelectionModeExtraInIntent(intent: Intent) {
|
||||
intent.putExtra(EXTRA_ENTRY_SELECTION_MODE, true)
|
||||
fun startActivityForSaveModeResult(context: Context,
|
||||
intent: Intent,
|
||||
searchInfo: SearchInfo) {
|
||||
addSpecialModeInIntent(intent, SpecialMode.SAVE)
|
||||
addTypeModeInIntent(intent, TypeMode.DEFAULT)
|
||||
addSearchInfoInIntent(intent, searchInfo)
|
||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun removeEntrySelectionModeFromIntent(intent: Intent) {
|
||||
intent.removeExtra(EXTRA_ENTRY_SELECTION_MODE)
|
||||
fun startActivityForKeyboardSelectionModeResult(context: Context,
|
||||
intent: Intent,
|
||||
searchInfo: SearchInfo?) {
|
||||
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||
addTypeModeInIntent(intent, TypeMode.MAGIKEYBOARD)
|
||||
addSearchInfoInIntent(intent, searchInfo)
|
||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun retrieveEntrySelectionModeFromIntent(intent: Intent): Boolean {
|
||||
return intent.getBooleanExtra(EXTRA_ENTRY_SELECTION_MODE, DEFAULT_ENTRY_SELECTION_MODE)
|
||||
fun startActivityForRegistrationModeResult(context: Context,
|
||||
intent: Intent,
|
||||
registerInfo: RegisterInfo?) {
|
||||
addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
|
||||
// At the moment, only autofill for registration
|
||||
addTypeModeInIntent(intent, TypeMode.AUTOFILL)
|
||||
addRegisterInfoInIntent(intent, registerInfo)
|
||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun doEntrySelectionAction(intent: Intent,
|
||||
standardAction: () -> Unit,
|
||||
keyboardAction: () -> Unit,
|
||||
autofillAction: (assistStructure: AssistStructure) -> Unit) {
|
||||
fun addSearchInfoInIntent(intent: Intent, searchInfo: SearchInfo?) {
|
||||
searchInfo?.let {
|
||||
intent.putExtra(KEY_SEARCH_INFO, it)
|
||||
}
|
||||
}
|
||||
|
||||
fun retrieveSearchInfoFromIntent(intent: Intent): SearchInfo? {
|
||||
return intent.getParcelableExtra(KEY_SEARCH_INFO)
|
||||
}
|
||||
|
||||
fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
|
||||
registerInfo?.let {
|
||||
intent.putExtra(KEY_REGISTER_INFO, it)
|
||||
}
|
||||
}
|
||||
|
||||
fun retrieveRegisterInfoFromIntent(intent: Intent): RegisterInfo? {
|
||||
return intent.getParcelableExtra(KEY_REGISTER_INFO)
|
||||
}
|
||||
|
||||
fun removeInfoFromIntent(intent: Intent) {
|
||||
intent.removeExtra(KEY_SEARCH_INFO)
|
||||
intent.removeExtra(KEY_REGISTER_INFO)
|
||||
}
|
||||
|
||||
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
|
||||
intent.putExtra(KEY_SPECIAL_MODE, specialMode as Serializable)
|
||||
}
|
||||
|
||||
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (AutofillHelper.retrieveAssistStructure(intent) != null)
|
||||
return SpecialMode.SELECTION
|
||||
}
|
||||
return intent.getSerializableExtra(KEY_SPECIAL_MODE) as SpecialMode?
|
||||
?: SpecialMode.DEFAULT
|
||||
}
|
||||
|
||||
fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
|
||||
intent.putExtra(KEY_TYPE_MODE, typeMode as Serializable)
|
||||
}
|
||||
|
||||
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (AutofillHelper.retrieveAssistStructure(intent) != null)
|
||||
return TypeMode.AUTOFILL
|
||||
}
|
||||
return intent.getSerializableExtra(KEY_TYPE_MODE) as TypeMode? ?: TypeMode.DEFAULT
|
||||
}
|
||||
|
||||
fun removeModesFromIntent(intent: Intent) {
|
||||
intent.removeExtra(KEY_SPECIAL_MODE)
|
||||
intent.removeExtra(KEY_TYPE_MODE)
|
||||
}
|
||||
|
||||
fun doSpecialAction(intent: Intent,
|
||||
defaultAction: () -> Unit,
|
||||
searchAction: (searchInfo: SearchInfo) -> Unit,
|
||||
saveAction: (searchInfo: SearchInfo) -> Unit,
|
||||
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
||||
autofillSelectionAction: (searchInfo: SearchInfo?,
|
||||
assistStructure: AssistStructure) -> Unit,
|
||||
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
|
||||
|
||||
when (retrieveSpecialModeFromIntent(intent)) {
|
||||
SpecialMode.DEFAULT -> {
|
||||
removeModesFromIntent(intent)
|
||||
removeInfoFromIntent(intent)
|
||||
defaultAction.invoke()
|
||||
}
|
||||
SpecialMode.SEARCH -> {
|
||||
val searchInfo = retrieveSearchInfoFromIntent(intent)
|
||||
removeModesFromIntent(intent)
|
||||
removeInfoFromIntent(intent)
|
||||
if (searchInfo != null)
|
||||
searchAction.invoke(searchInfo)
|
||||
else {
|
||||
defaultAction.invoke()
|
||||
}
|
||||
}
|
||||
SpecialMode.SAVE -> {
|
||||
val searchInfo = retrieveSearchInfoFromIntent(intent)
|
||||
removeModesFromIntent(intent)
|
||||
removeInfoFromIntent(intent)
|
||||
if (searchInfo != null)
|
||||
saveAction.invoke(searchInfo)
|
||||
else {
|
||||
defaultAction.invoke()
|
||||
}
|
||||
}
|
||||
SpecialMode.SELECTION -> {
|
||||
val searchInfo: SearchInfo? = retrieveSearchInfoFromIntent(intent)
|
||||
var assistStructureInit = false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillHelper.retrieveAssistStructure(intent)?.let { assistStructure ->
|
||||
autofillAction.invoke(assistStructure)
|
||||
autofillSelectionAction.invoke(searchInfo, assistStructure)
|
||||
assistStructureInit = true
|
||||
}
|
||||
}
|
||||
if (!assistStructureInit) {
|
||||
if (intent.getBooleanExtra(EXTRA_ENTRY_SELECTION_MODE, DEFAULT_ENTRY_SELECTION_MODE)) {
|
||||
intent.removeExtra(EXTRA_ENTRY_SELECTION_MODE)
|
||||
keyboardAction.invoke()
|
||||
if (intent.getSerializableExtra(KEY_SPECIAL_MODE) != null) {
|
||||
when (retrieveTypeModeFromIntent(intent)) {
|
||||
TypeMode.DEFAULT -> {
|
||||
removeModesFromIntent(intent)
|
||||
if (searchInfo != null)
|
||||
searchAction.invoke(searchInfo)
|
||||
else
|
||||
defaultAction.invoke()
|
||||
}
|
||||
TypeMode.MAGIKEYBOARD -> keyboardSelectionAction.invoke(searchInfo)
|
||||
else -> {
|
||||
// In this case, error
|
||||
removeModesFromIntent(intent)
|
||||
removeInfoFromIntent(intent)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
standardAction.invoke()
|
||||
if (searchInfo != null)
|
||||
searchAction.invoke(searchInfo)
|
||||
else
|
||||
defaultAction.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
SpecialMode.REGISTRATION -> {
|
||||
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent)
|
||||
removeModesFromIntent(intent)
|
||||
removeInfoFromIntent(intent)
|
||||
autofillRegistrationAction.invoke(registerInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,19 +28,20 @@ import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
class OpenFileHelper {
|
||||
class SelectFileHelper {
|
||||
|
||||
private var activity: Activity? = null
|
||||
private var fragment: Fragment? = null
|
||||
|
||||
val openFileOnClickViewListener: OpenFileOnClickViewListener
|
||||
get() = OpenFileOnClickViewListener()
|
||||
val selectFileOnClickViewListener: SelectFileOnClickViewListener
|
||||
get() = SelectFileOnClickViewListener()
|
||||
|
||||
constructor(context: Activity) {
|
||||
this.activity = context
|
||||
@@ -52,7 +53,10 @@ class OpenFileHelper {
|
||||
this.fragment = context
|
||||
}
|
||||
|
||||
inner class OpenFileOnClickViewListener : View.OnClickListener, View.OnLongClickListener {
|
||||
inner class SelectFileOnClickViewListener :
|
||||
View.OnClickListener,
|
||||
View.OnLongClickListener,
|
||||
MenuItem.OnMenuItemClickListener {
|
||||
|
||||
private fun onAbstractClick(longClick: Boolean = false) {
|
||||
try {
|
||||
@@ -85,6 +89,11 @@ class OpenFileHelper {
|
||||
onAbstractClick(true)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
||||
onAbstractClick()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.kunzisoft.keepass.activities.helpers
|
||||
|
||||
enum class SpecialMode {
|
||||
DEFAULT,
|
||||
SEARCH,
|
||||
SAVE,
|
||||
SELECTION,
|
||||
REGISTRATION;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.kunzisoft.keepass.activities.helpers
|
||||
|
||||
enum class TypeMode {
|
||||
DEFAULT, MAGIKEYBOARD, AUTOFILL
|
||||
}
|
||||
@@ -25,7 +25,11 @@ 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
|
||||
@@ -43,7 +47,7 @@ abstract class LockingActivity : SpecialModeActivity() {
|
||||
// Force readOnly if Entry Selection mode
|
||||
protected var mReadOnly: Boolean
|
||||
get() {
|
||||
return mReadOnlyToSave || mSelectionMode
|
||||
return mReadOnlyToSave
|
||||
}
|
||||
set(value) {
|
||||
mReadOnlyToSave = value
|
||||
@@ -96,6 +100,15 @@ abstract class LockingActivity : SpecialModeActivity() {
|
||||
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
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package com.kunzisoft.keepass.activities.selection
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.helpers.TypeMode
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.SpecialModeView
|
||||
@@ -16,40 +17,128 @@ import com.kunzisoft.keepass.view.SpecialModeView
|
||||
*/
|
||||
abstract class SpecialModeActivity : StylishActivity() {
|
||||
|
||||
protected var mSelectionMode: Boolean = false
|
||||
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
|
||||
protected var mTypeMode: TypeMode = TypeMode.DEFAULT
|
||||
|
||||
protected var mAutofillSelection: Boolean = false
|
||||
private var mSpecialModeView: SpecialModeView? = null
|
||||
|
||||
private var specialModeView: SpecialModeView? = null
|
||||
override fun onBackPressed() {
|
||||
if (mSpecialMode != SpecialMode.DEFAULT)
|
||||
onCancelSpecialMode()
|
||||
else
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
/**
|
||||
* To call the regular onBackPressed() method in special mode
|
||||
*/
|
||||
protected fun onRegularBackPressed() {
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
/**
|
||||
* Intent sender uses special retains data in callback
|
||||
*/
|
||||
private fun isIntentSender(): Boolean {
|
||||
return (mSpecialMode == SpecialMode.SELECTION
|
||||
&& mTypeMode == TypeMode.AUTOFILL)
|
||||
/* TODO Registration callback #765
|
||||
|| (mSpecialMode == SpecialMode.REGISTRATION
|
||||
&& mTypeMode == TypeMode.AUTOFILL
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||
*/
|
||||
}
|
||||
|
||||
fun onLaunchActivitySpecialMode() {
|
||||
if (!isIntentSender()) {
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
open fun onValidateSpecialMode() {
|
||||
if (isIntentSender()) {
|
||||
super.finish()
|
||||
} else {
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
// To move the app in background
|
||||
moveTaskToBack(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun onCancelSpecialMode() {
|
||||
onBackPressed()
|
||||
if (isIntentSender()) {
|
||||
// To get the app caller, only for IntentSender
|
||||
super.onBackPressed()
|
||||
} else {
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
// To move the app in background
|
||||
moveTaskToBack(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun backToTheAppCaller() {
|
||||
if (isIntentSender()) {
|
||||
// To get the app caller, only for IntentSender
|
||||
super.onBackPressed()
|
||||
} else {
|
||||
// To move the app in background
|
||||
moveTaskToBack(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
||||
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
mSelectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(intent)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
mAutofillSelection = AutofillHelper.retrieveAssistStructure(intent) != null
|
||||
}
|
||||
|
||||
val searchInfo: SearchInfo? = intent.getParcelableExtra(EntrySelectionHelper.KEY_SEARCH_INFO)
|
||||
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
||||
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
||||
val searchInfo: SearchInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)?.searchInfo
|
||||
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
||||
|
||||
// To show the selection mode
|
||||
specialModeView = findViewById(R.id.special_mode_view)
|
||||
specialModeView?.apply {
|
||||
mSpecialModeView = findViewById(R.id.special_mode_view)
|
||||
mSpecialModeView?.apply {
|
||||
// Populate title
|
||||
val typeModeId = if (mAutofillSelection)
|
||||
R.string.autofill
|
||||
else
|
||||
R.string.magic_keyboard_title
|
||||
title = "${resources.getString(R.string.selection_mode)} (${getString(typeModeId)})"
|
||||
val selectionModeStringId = when (mSpecialMode) {
|
||||
SpecialMode.DEFAULT, // Not important because hidden
|
||||
SpecialMode.SEARCH -> R.string.search_mode
|
||||
SpecialMode.SAVE -> R.string.save_mode
|
||||
SpecialMode.SELECTION -> R.string.selection_mode
|
||||
SpecialMode.REGISTRATION -> R.string.registration_mode
|
||||
}
|
||||
val typeModeStringId = when (mTypeMode) {
|
||||
TypeMode.DEFAULT, // Not important because hidden
|
||||
TypeMode.MAGIKEYBOARD -> R.string.magic_keyboard_title
|
||||
TypeMode.AUTOFILL -> R.string.autofill
|
||||
}
|
||||
title = getString(selectionModeStringId)
|
||||
if (mTypeMode != TypeMode.DEFAULT)
|
||||
title = "$title (${getString(typeModeStringId)})"
|
||||
// Populate subtitle
|
||||
subtitle = searchInfo?.getName(resources)
|
||||
|
||||
// Show the toolbar or not
|
||||
visible = mSelectionMode
|
||||
visible = when (mSpecialMode) {
|
||||
SpecialMode.DEFAULT -> false
|
||||
SpecialMode.SEARCH -> true
|
||||
SpecialMode.SAVE -> true
|
||||
SpecialMode.SELECTION -> true
|
||||
SpecialMode.REGISTRATION -> true
|
||||
}
|
||||
|
||||
// Add back listener
|
||||
onCancelButtonClickListener = View.OnClickListener {
|
||||
@@ -58,7 +147,7 @@ abstract class SpecialModeActivity : StylishActivity() {
|
||||
|
||||
// Create menu
|
||||
menu.clear()
|
||||
if (mAutofillSelection) {
|
||||
if (mTypeMode == TypeMode.AUTOFILL) {
|
||||
menuInflater.inflate(R.menu.autofill, menu)
|
||||
setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
@@ -70,9 +159,15 @@ abstract class SpecialModeActivity : StylishActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// To hide home button from the regular toolbar in special mode
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun blockAutofill(searchInfo: SearchInfo?) {
|
||||
private fun blockAutofill(searchInfo: SearchInfo?) {
|
||||
val webDomain = searchInfo?.webDomain
|
||||
val applicationId = searchInfo?.applicationId
|
||||
if (webDomain != null) {
|
||||
|
||||
@@ -32,6 +32,18 @@ abstract class AnimatedItemsAdapter<Item, T: RecyclerView.ViewHolder>(val contex
|
||||
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
|
||||
}
|
||||
|
||||
open fun isEmpty(): Boolean {
|
||||
return itemsList.isEmpty()
|
||||
}
|
||||
|
||||
open fun contains(item: Item): Boolean {
|
||||
return itemsList.contains(item)
|
||||
}
|
||||
|
||||
open fun indexOf(item: Item): Int {
|
||||
return itemsList.indexOf(item)
|
||||
}
|
||||
|
||||
open fun putItem(item: Item) {
|
||||
val previousSize = itemsList.size
|
||||
if (itemsList.contains(item)) {
|
||||
@@ -46,13 +58,42 @@ abstract class AnimatedItemsAdapter<Item, T: RecyclerView.ViewHolder>(val contex
|
||||
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
|
||||
}
|
||||
|
||||
fun onBindDeleteButton(holder: T, deleteButton: View, item: Item, position: Int) {
|
||||
deleteButton.apply {
|
||||
visibility = View.VISIBLE
|
||||
if (mItemToRemove == item) {
|
||||
/**
|
||||
* Only replace [oldItem] by [newItem] if [oldItem] exists
|
||||
*/
|
||||
open fun replaceItem(oldItem: Item, newItem: Item) {
|
||||
if (itemsList.contains(oldItem)) {
|
||||
val index = itemsList.indexOf(oldItem)
|
||||
itemsList.removeAt(index)
|
||||
itemsList.add(index, newItem)
|
||||
notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only remove [item] if doesn't exists
|
||||
*/
|
||||
open fun removeItem(item: Item) {
|
||||
if (itemsList.contains(item)) {
|
||||
mItemToRemove = item
|
||||
notifyItemChanged(itemsList.indexOf(item))
|
||||
}
|
||||
}
|
||||
|
||||
protected fun performDeletion(holder: T, item: Item): Boolean {
|
||||
val effectivelyDeletionPerformed = mItemToRemove == item
|
||||
if (effectivelyDeletionPerformed) {
|
||||
holder.itemView.collapse(true) {
|
||||
deleteItem(item)
|
||||
}
|
||||
}
|
||||
return effectivelyDeletionPerformed
|
||||
}
|
||||
|
||||
protected fun onBindDeleteButton(holder: T, deleteButton: View, item: Item, position: Int) {
|
||||
deleteButton.apply {
|
||||
visibility = View.VISIBLE
|
||||
if (performDeletion(holder, item)) {
|
||||
setOnClickListener(null)
|
||||
} else {
|
||||
setOnClickListener {
|
||||
|
||||
@@ -20,39 +20,66 @@
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.TypedArray
|
||||
import android.graphics.Color
|
||||
import android.text.format.Formatter
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryAttachment
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
|
||||
class EntryAttachmentsItemsAdapter(context: Context, private val editable: Boolean)
|
||||
: AnimatedItemsAdapter<EntryAttachment, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
|
||||
|
||||
var onItemClickListener: ((item: EntryAttachment)->Unit)? = null
|
||||
class EntryAttachmentsItemsAdapter(context: Context)
|
||||
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
|
||||
|
||||
private val mDatabase = Database.getInstance()
|
||||
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
|
||||
|
||||
private var mTitleColor: Int
|
||||
|
||||
init {
|
||||
// Get the primary text color of the theme
|
||||
val typedValue = TypedValue()
|
||||
context.theme.resolveAttribute(android.R.attr.textColorPrimary, typedValue, true)
|
||||
val typedArray: TypedArray = context.obtainStyledAttributes(typedValue.data, intArrayOf(
|
||||
android.R.attr.textColor))
|
||||
mTitleColor = typedArray.getColor(0, -1)
|
||||
typedArray.recycle()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryBinariesViewHolder {
|
||||
return EntryBinariesViewHolder(inflater.inflate(R.layout.item_attachment, parent, false))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: EntryBinariesViewHolder, position: Int) {
|
||||
val entryAttachment = itemsList[position]
|
||||
val entryAttachmentState = itemsList[position]
|
||||
|
||||
holder.itemView.visibility = View.VISIBLE
|
||||
holder.binaryFileTitle.text = entryAttachment.name
|
||||
holder.binaryFileBroken.apply {
|
||||
setColorFilter(Color.RED)
|
||||
visibility = if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
holder.binaryFileTitle.text = entryAttachmentState.attachment.name
|
||||
if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) {
|
||||
holder.binaryFileTitle.setTextColor(Color.RED)
|
||||
} else {
|
||||
holder.binaryFileTitle.setTextColor(mTitleColor)
|
||||
}
|
||||
holder.binaryFileSize.text = Formatter.formatFileSize(context,
|
||||
entryAttachment.binaryAttachment.length())
|
||||
entryAttachmentState.attachment.binaryAttachment.length())
|
||||
holder.binaryFileCompression.apply {
|
||||
if (mDatabase.compressionAlgorithm == CompressionAlgorithm.GZip
|
||||
|| entryAttachment.binaryAttachment.isCompressed == true) {
|
||||
if (entryAttachmentState.attachment.binaryAttachment.isCompressed) {
|
||||
text = CompressionAlgorithm.GZip.getName(context.resources)
|
||||
visibility = View.VISIBLE
|
||||
} else {
|
||||
@@ -60,42 +87,61 @@ class EntryAttachmentsItemsAdapter(context: Context, private val editable: Boole
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
if (editable) {
|
||||
when (entryAttachmentState.streamDirection) {
|
||||
StreamDirection.UPLOAD -> {
|
||||
holder.binaryFileProgressIcon.isActivated = true
|
||||
when (entryAttachmentState.downloadState) {
|
||||
AttachmentState.START,
|
||||
AttachmentState.IN_PROGRESS -> {
|
||||
holder.binaryFileProgressContainer.visibility = View.VISIBLE
|
||||
holder.binaryFileProgress.apply {
|
||||
visibility = View.VISIBLE
|
||||
progress = entryAttachmentState.downloadProgression
|
||||
}
|
||||
holder.binaryFileDeleteButton.apply {
|
||||
visibility = View.GONE
|
||||
setOnClickListener(null)
|
||||
}
|
||||
}
|
||||
AttachmentState.NULL,
|
||||
AttachmentState.ERROR,
|
||||
AttachmentState.COMPLETE -> {
|
||||
holder.binaryFileProgressContainer.visibility = View.GONE
|
||||
holder.binaryFileProgress.visibility = View.GONE
|
||||
holder.binaryFileDeleteButton.apply {
|
||||
visibility = View.VISIBLE
|
||||
onBindDeleteButton(holder, this, entryAttachment, position)
|
||||
onBindDeleteButton(holder, this, entryAttachmentState, position)
|
||||
}
|
||||
} else {
|
||||
}
|
||||
}
|
||||
holder.itemView.setOnClickListener(null)
|
||||
}
|
||||
StreamDirection.DOWNLOAD -> {
|
||||
holder.binaryFileProgressIcon.isActivated = false
|
||||
holder.binaryFileProgressContainer.visibility = View.VISIBLE
|
||||
holder.binaryFileDeleteButton.visibility = View.GONE
|
||||
holder.binaryFileProgress.apply {
|
||||
visibility = when (entryAttachment.downloadState) {
|
||||
visibility = when (entryAttachmentState.downloadState) {
|
||||
AttachmentState.NULL, AttachmentState.COMPLETE, AttachmentState.ERROR -> View.GONE
|
||||
AttachmentState.START, AttachmentState.IN_PROGRESS -> View.VISIBLE
|
||||
}
|
||||
progress = entryAttachment.downloadProgression
|
||||
progress = entryAttachmentState.downloadProgression
|
||||
}
|
||||
holder.itemView.setOnClickListener {
|
||||
onItemClickListener?.invoke(entryAttachment)
|
||||
onItemClickListener?.invoke(entryAttachmentState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateProgress(entryAttachment: EntryAttachment) {
|
||||
val indexEntryAttachment = itemsList.indexOfLast { current -> current.name == entryAttachment.name }
|
||||
if (indexEntryAttachment != -1) {
|
||||
itemsList[indexEntryAttachment] = entryAttachment
|
||||
notifyItemChanged(indexEntryAttachment)
|
||||
}
|
||||
}
|
||||
|
||||
inner class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
var binaryFileBroken: ImageView = itemView.findViewById(R.id.item_attachment_broken)
|
||||
var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title)
|
||||
var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size)
|
||||
var binaryFileCompression: TextView = itemView.findViewById(R.id.item_attachment_compression)
|
||||
var binaryFileProgressContainer: View = itemView.findViewById(R.id.item_attachment_progress_container)
|
||||
var binaryFileProgressIcon: ImageView = itemView.findViewById(R.id.item_attachment_icon)
|
||||
var binaryFileProgress: ProgressBar = itemView.findViewById(R.id.item_attachment_progress)
|
||||
var binaryFileDeleteButton: View = itemView.findViewById(R.id.item_attachment_delete_button)
|
||||
}
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.model.Field
|
||||
import com.kunzisoft.keepass.model.FocusedEditField
|
||||
import com.kunzisoft.keepass.view.EditTextSelectable
|
||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||
|
||||
class EntryExtraFieldsItemsAdapter(context: Context)
|
||||
: AnimatedItemsAdapter<Field, EntryExtraFieldsItemsAdapter.EntryExtraFieldViewHolder>(context) {
|
||||
|
||||
var applyFontVisibility = false
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
private var mValueViewInputType: Int = 0
|
||||
private var mLastFocusedEditField = FocusedEditField()
|
||||
private var mLastFocusedTimestamp: Long = 0L
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryExtraFieldViewHolder {
|
||||
val view = EntryExtraFieldViewHolder(
|
||||
inflater.inflate(R.layout.item_entry_edit_extra_field, parent, false)
|
||||
)
|
||||
mValueViewInputType = view.extraFieldValue.inputType
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: EntryExtraFieldViewHolder, position: Int) {
|
||||
val extraField = itemsList[position]
|
||||
|
||||
holder.itemView.visibility = View.VISIBLE
|
||||
if (extraField.protectedValue.isProtected) {
|
||||
holder.extraFieldValueContainer.isPasswordVisibilityToggleEnabled = true
|
||||
holder.extraFieldValue.inputType = EditorInfo.TYPE_TEXT_VARIATION_PASSWORD or mValueViewInputType
|
||||
} else {
|
||||
holder.extraFieldValueContainer.isPasswordVisibilityToggleEnabled = false
|
||||
holder.extraFieldValue.inputType = mValueViewInputType
|
||||
}
|
||||
holder.extraFieldValueContainer.hint = extraField.name
|
||||
holder.extraFieldValue.apply {
|
||||
setText(extraField.protectedValue.toString())
|
||||
// To Fix focus in RecyclerView
|
||||
setOnFocusChangeListener { _, hasFocus ->
|
||||
if (hasFocus) {
|
||||
setFocusField(extraField, selectionStart, selectionEnd)
|
||||
} else {
|
||||
// request focus on last text focused
|
||||
if (focusedTimestampNotExpired()) {
|
||||
requestFocusField(this, extraField, false)
|
||||
} else {
|
||||
removeFocusField(extraField)
|
||||
}
|
||||
}
|
||||
}
|
||||
addOnSelectionChangedListener(object: EditTextSelectable.OnSelectionChangedListener {
|
||||
override fun onSelectionChanged(start: Int, end: Int) {
|
||||
mLastFocusedEditField.apply {
|
||||
cursorSelectionStart = start
|
||||
cursorSelectionEnd = end
|
||||
}
|
||||
}
|
||||
})
|
||||
requestFocusField(this, extraField, true)
|
||||
doOnTextChanged { text, _, _, _ ->
|
||||
extraField.protectedValue.stringValue = text.toString()
|
||||
}
|
||||
if (applyFontVisibility)
|
||||
applyFontVisibility()
|
||||
}
|
||||
holder.extraFieldDeleteButton.apply {
|
||||
onBindDeleteButton(holder, this, extraField, position)
|
||||
}
|
||||
}
|
||||
|
||||
fun assignItems(items: List<Field>, focusedEditField: FocusedEditField?) {
|
||||
focusedEditField?.let {
|
||||
setFocusField(it, true)
|
||||
}
|
||||
super.assignItems(items)
|
||||
}
|
||||
|
||||
override fun putItem(item: Field) {
|
||||
setFocusField(mLastFocusedEditField.apply {
|
||||
field = item
|
||||
cursorSelectionStart = -1
|
||||
cursorSelectionEnd = -1
|
||||
}, true)
|
||||
super.putItem(item)
|
||||
}
|
||||
|
||||
private fun setFocusField(field: Field,
|
||||
selectionStart: Int,
|
||||
selectionEnd: Int,
|
||||
force: Boolean = false) {
|
||||
mLastFocusedEditField.apply {
|
||||
this.field = field
|
||||
this.cursorSelectionStart = selectionStart
|
||||
this.cursorSelectionEnd = selectionEnd
|
||||
}
|
||||
setFocusField(mLastFocusedEditField, force)
|
||||
}
|
||||
|
||||
private fun setFocusField(field: FocusedEditField, force: Boolean = false) {
|
||||
mLastFocusedEditField = field
|
||||
mLastFocusedTimestamp = if (force) 0L else System.currentTimeMillis()
|
||||
}
|
||||
|
||||
private fun removeFocusField(field: Field? = null) {
|
||||
if (field == null || mLastFocusedEditField.field == field) {
|
||||
mLastFocusedEditField.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestFocusField(editText: EditText, field: Field, setSelection: Boolean) {
|
||||
if (field == mLastFocusedEditField.field) {
|
||||
editText.apply {
|
||||
post {
|
||||
if (setSelection) {
|
||||
setEditTextSelection(editText)
|
||||
}
|
||||
requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setEditTextSelection(editText: EditText) {
|
||||
try {
|
||||
var newCursorPositionStart = mLastFocusedEditField.cursorSelectionStart
|
||||
var newCursorPositionEnd = mLastFocusedEditField.cursorSelectionEnd
|
||||
// Cursor at end if 0 or less
|
||||
if (newCursorPositionStart < 0 || newCursorPositionEnd < 0) {
|
||||
newCursorPositionStart = (editText.text?:"").length
|
||||
newCursorPositionEnd = newCursorPositionStart
|
||||
}
|
||||
editText.setSelection(newCursorPositionStart, newCursorPositionEnd)
|
||||
} catch (ignoredException: Exception) {}
|
||||
}
|
||||
|
||||
private fun focusedTimestampNotExpired(): Boolean {
|
||||
return mLastFocusedTimestamp == 0L || (mLastFocusedTimestamp + FOCUS_TIMESTAMP) > System.currentTimeMillis()
|
||||
}
|
||||
|
||||
fun getFocusedField(): FocusedEditField {
|
||||
return mLastFocusedEditField
|
||||
}
|
||||
|
||||
class EntryExtraFieldViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
var extraFieldValueContainer: TextInputLayout = itemView.findViewById(R.id.entry_extra_field_value_container)
|
||||
var extraFieldValue: EditTextSelectable = itemView.findViewById(R.id.entry_extra_field_value)
|
||||
var extraFieldDeleteButton: View = itemView.findViewById(R.id.entry_extra_field_delete)
|
||||
}
|
||||
|
||||
companion object {
|
||||
// time to focus element when a keyboard appears
|
||||
private const val FOCUS_TIMESTAMP = 400L
|
||||
}
|
||||
}
|
||||
@@ -101,9 +101,9 @@ class FileDatabaseHistoryAdapter(context: Context)
|
||||
// Modification
|
||||
databaseFile.databaseLastModified?.let {
|
||||
holder.fileModification.text = it
|
||||
holder.fileModification.visibility = View.VISIBLE
|
||||
holder.fileModificationContainer.visibility = View.VISIBLE
|
||||
} ?: run {
|
||||
holder.fileModification.visibility = View.GONE
|
||||
holder.fileModificationContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
// Size
|
||||
@@ -234,7 +234,7 @@ class FileDatabaseHistoryAdapter(context: Context)
|
||||
this.saveAliasListener = listener
|
||||
}
|
||||
|
||||
inner class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
var fileContainer: ViewGroup = itemView.findViewById(R.id.file_container_basic_info)
|
||||
|
||||
@@ -250,6 +250,7 @@ class FileDatabaseHistoryAdapter(context: Context)
|
||||
var fileModifyButton: ImageView = itemView.findViewById(R.id.file_modify_button)
|
||||
var fileDeleteButton: ImageView = itemView.findViewById(R.id.file_delete_button)
|
||||
var filePath: TextView = itemView.findViewById(R.id.file_path)
|
||||
var fileModificationContainer: ViewGroup = itemView.findViewById(R.id.file_modification_container)
|
||||
var fileModification: TextView = itemView.findViewById(R.id.file_modification)
|
||||
var fileSize: TextView = itemView.findViewById(R.id.file_size)
|
||||
}
|
||||
|
||||
@@ -52,72 +52,69 @@ import java.util.*
|
||||
class NodeAdapter (private val context: Context)
|
||||
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
|
||||
|
||||
private var nodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
|
||||
private val nodeSortedListCallback: NodeSortedListCallback
|
||||
private val nodeSortedList: SortedList<Node>
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
|
||||
private val mNodeSortedListCallback: NodeSortedListCallback
|
||||
private val mNodeSortedList: SortedList<Node>
|
||||
private val mInflater: LayoutInflater = LayoutInflater.from(context)
|
||||
|
||||
private var calculateViewTypeTextSize = Array(2) { true} // number of view type
|
||||
private var textSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
||||
private var prefSizeMultiplier: Float = 0F
|
||||
private var subtextDefaultDimension: Float = 0F
|
||||
private var infoTextDefaultDimension: Float = 0F
|
||||
private var numberChildrenTextDefaultDimension: Float = 0F
|
||||
private var iconDefaultDimension: Float = 0F
|
||||
private var mCalculateViewTypeTextSize = Array(2) { true} // number of view type
|
||||
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
||||
private var mPrefSizeMultiplier: Float = 0F
|
||||
private var mSubtextDefaultDimension: Float = 0F
|
||||
private var mInfoTextDefaultDimension: Float = 0F
|
||||
private var mNumberChildrenTextDefaultDimension: Float = 0F
|
||||
private var mIconDefaultDimension: Float = 0F
|
||||
|
||||
private var showUserNames: Boolean = true
|
||||
private var showNumberEntries: Boolean = true
|
||||
private var entryFilters = arrayOf<Group.ChildFilter>()
|
||||
private var mShowUserNames: Boolean = true
|
||||
private var mShowNumberEntries: Boolean = true
|
||||
private var mEntryFilters = arrayOf<Group.ChildFilter>()
|
||||
|
||||
private var actionNodesList = LinkedList<Node>()
|
||||
private var nodeClickCallback: NodeClickCallback? = null
|
||||
private var mActionNodesList = LinkedList<Node>()
|
||||
private var mNodeClickCallback: NodeClickCallback? = null
|
||||
|
||||
private val mDatabase: Database
|
||||
|
||||
@ColorInt
|
||||
private val contentSelectionColor: Int
|
||||
private val mContentSelectionColor: Int
|
||||
@ColorInt
|
||||
private val iconGroupColor: Int
|
||||
private val mIconGroupColor: Int
|
||||
@ColorInt
|
||||
private val iconEntryColor: Int
|
||||
private val mIconEntryColor: Int
|
||||
|
||||
/**
|
||||
* Determine if the adapter contains or not any element
|
||||
* @return true if the list is empty
|
||||
*/
|
||||
val isEmpty: Boolean
|
||||
get() = nodeSortedList.size() <= 0
|
||||
get() = mNodeSortedList.size() <= 0
|
||||
|
||||
init {
|
||||
this.infoTextDefaultDimension = context.resources.getDimension(R.dimen.list_medium_size_default)
|
||||
this.subtextDefaultDimension = context.resources.getDimension(R.dimen.list_small_size_default)
|
||||
this.numberChildrenTextDefaultDimension = context.resources.getDimension(R.dimen.list_tiny_size_default)
|
||||
this.iconDefaultDimension = context.resources.getDimension(R.dimen.list_icon_size_default)
|
||||
this.mIconDefaultDimension = context.resources.getDimension(R.dimen.list_icon_size_default)
|
||||
|
||||
assignPreferences()
|
||||
|
||||
this.nodeSortedListCallback = NodeSortedListCallback()
|
||||
this.nodeSortedList = SortedList(Node::class.java, nodeSortedListCallback)
|
||||
this.mNodeSortedListCallback = NodeSortedListCallback()
|
||||
this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback)
|
||||
|
||||
// Database
|
||||
this.mDatabase = Database.getInstance()
|
||||
|
||||
// Color of content selection
|
||||
val taContentSelectionColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
||||
this.contentSelectionColor = taContentSelectionColor.getColor(0, Color.WHITE)
|
||||
this.mContentSelectionColor = taContentSelectionColor.getColor(0, Color.WHITE)
|
||||
taContentSelectionColor.recycle()
|
||||
// Retrieve the color to tint the icon
|
||||
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
||||
this.iconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
|
||||
this.mIconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
|
||||
taTextColorPrimary.recycle()
|
||||
// In two times to fix bug compilation
|
||||
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
this.iconEntryColor = taTextColor.getColor(0, Color.BLACK)
|
||||
this.mIconEntryColor = taTextColor.getColor(0, Color.BLACK)
|
||||
taTextColor.recycle()
|
||||
}
|
||||
|
||||
fun assignPreferences() {
|
||||
this.prefSizeMultiplier = PreferencesUtil.getListTextSize(context)
|
||||
this.mPrefSizeMultiplier = PreferencesUtil.getListTextSize(context)
|
||||
|
||||
notifyChangeSort(
|
||||
PreferencesUtil.getListSort(context),
|
||||
@@ -128,13 +125,13 @@ class NodeAdapter (private val context: Context)
|
||||
)
|
||||
)
|
||||
|
||||
this.showUserNames = PreferencesUtil.showUsernamesListEntries(context)
|
||||
this.showNumberEntries = PreferencesUtil.showNumberEntries(context)
|
||||
this.mShowUserNames = PreferencesUtil.showUsernamesListEntries(context)
|
||||
this.mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
|
||||
|
||||
this.entryFilters = Group.ChildFilter.getDefaults(context)
|
||||
this.mEntryFilters = Group.ChildFilter.getDefaults(context)
|
||||
|
||||
// Reinit textSize for all view type
|
||||
calculateViewTypeTextSize.forEachIndexed { index, _ -> calculateViewTypeTextSize[index] = true }
|
||||
mCalculateViewTypeTextSize.forEachIndexed { index, _ -> mCalculateViewTypeTextSize[index] = true }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,12 +139,12 @@ class NodeAdapter (private val context: Context)
|
||||
*/
|
||||
fun rebuildList(group: Group) {
|
||||
assignPreferences()
|
||||
nodeSortedList.replaceAll(group.getFilteredChildren(entryFilters))
|
||||
mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters))
|
||||
}
|
||||
|
||||
private inner class NodeSortedListCallback: SortedListAdapterCallback<Node>(this) {
|
||||
override fun compare(item1: Node, item2: Node): Int {
|
||||
return nodeComparator!!.compare(item1, item2)
|
||||
return mNodeComparator!!.compare(item1, item2)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
|
||||
@@ -162,7 +159,7 @@ class NodeAdapter (private val context: Context)
|
||||
}
|
||||
|
||||
fun contains(node: Node): Boolean {
|
||||
return nodeSortedList.indexOf(node) != SortedList.INVALID_POSITION
|
||||
return mNodeSortedList.indexOf(node) != SortedList.INVALID_POSITION
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,7 +167,7 @@ class NodeAdapter (private val context: Context)
|
||||
* @param node Node to add
|
||||
*/
|
||||
fun addNode(node: Node) {
|
||||
nodeSortedList.add(node)
|
||||
mNodeSortedList.add(node)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,7 +175,7 @@ class NodeAdapter (private val context: Context)
|
||||
* @param nodes Nodes to add
|
||||
*/
|
||||
fun addNodes(nodes: List<Node>) {
|
||||
nodeSortedList.addAll(nodes)
|
||||
mNodeSortedList.addAll(nodes)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,7 +183,7 @@ class NodeAdapter (private val context: Context)
|
||||
* @param node Node to delete
|
||||
*/
|
||||
fun removeNode(node: Node) {
|
||||
nodeSortedList.remove(node)
|
||||
mNodeSortedList.remove(node)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,7 +192,7 @@ class NodeAdapter (private val context: Context)
|
||||
*/
|
||||
fun removeNodes(nodes: List<Node>) {
|
||||
nodes.forEach { node ->
|
||||
nodeSortedList.remove(node)
|
||||
mNodeSortedList.remove(node)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,9 +200,9 @@ class NodeAdapter (private val context: Context)
|
||||
* Remove a node at [position] in the list
|
||||
*/
|
||||
fun removeNodeAt(position: Int) {
|
||||
nodeSortedList.removeItemAt(position)
|
||||
mNodeSortedList.removeItemAt(position)
|
||||
// Refresh all the next items
|
||||
notifyItemRangeChanged(position, nodeSortedList.size() - position)
|
||||
notifyItemRangeChanged(position, mNodeSortedList.size() - position)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -226,10 +223,10 @@ class NodeAdapter (private val context: Context)
|
||||
* @param newNode Node after the update
|
||||
*/
|
||||
fun updateNode(oldNode: Node, newNode: Node) {
|
||||
nodeSortedList.beginBatchedUpdates()
|
||||
nodeSortedList.remove(oldNode)
|
||||
nodeSortedList.add(newNode)
|
||||
nodeSortedList.endBatchedUpdates()
|
||||
mNodeSortedList.beginBatchedUpdates()
|
||||
mNodeSortedList.remove(oldNode)
|
||||
mNodeSortedList.add(newNode)
|
||||
mNodeSortedList.endBatchedUpdates()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -238,30 +235,30 @@ class NodeAdapter (private val context: Context)
|
||||
* @param newNodes Node after the update
|
||||
*/
|
||||
fun updateNodes(oldNodes: List<Node>, newNodes: List<Node>) {
|
||||
nodeSortedList.beginBatchedUpdates()
|
||||
mNodeSortedList.beginBatchedUpdates()
|
||||
oldNodes.forEach { oldNode ->
|
||||
nodeSortedList.remove(oldNode)
|
||||
mNodeSortedList.remove(oldNode)
|
||||
}
|
||||
nodeSortedList.addAll(newNodes)
|
||||
nodeSortedList.endBatchedUpdates()
|
||||
mNodeSortedList.addAll(newNodes)
|
||||
mNodeSortedList.endBatchedUpdates()
|
||||
}
|
||||
|
||||
fun notifyNodeChanged(node: Node) {
|
||||
notifyItemChanged(nodeSortedList.indexOf(node))
|
||||
notifyItemChanged(mNodeSortedList.indexOf(node))
|
||||
}
|
||||
|
||||
fun setActionNodes(actionNodes: List<Node>) {
|
||||
this.actionNodesList.apply {
|
||||
this.mActionNodesList.apply {
|
||||
clear()
|
||||
addAll(actionNodes)
|
||||
}
|
||||
}
|
||||
|
||||
fun unselectActionNodes() {
|
||||
actionNodesList.forEach {
|
||||
notifyItemChanged(nodeSortedList.indexOf(it))
|
||||
mActionNodesList.forEach {
|
||||
notifyItemChanged(mNodeSortedList.indexOf(it))
|
||||
}
|
||||
this.actionNodesList.apply {
|
||||
this.mActionNodesList.apply {
|
||||
clear()
|
||||
}
|
||||
}
|
||||
@@ -271,49 +268,55 @@ class NodeAdapter (private val context: Context)
|
||||
*/
|
||||
fun notifyChangeSort(sortNodeEnum: SortNodeEnum,
|
||||
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
||||
this.nodeComparator = sortNodeEnum.getNodeComparator(sortNodeParameters)
|
||||
this.mNodeComparator = sortNodeEnum.getNodeComparator(sortNodeParameters)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return nodeSortedList.get(position).type.ordinal
|
||||
return mNodeSortedList.get(position).type.ordinal
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NodeViewHolder {
|
||||
val view: View = if (viewType == Type.GROUP.ordinal) {
|
||||
inflater.inflate(R.layout.item_list_nodes_group, parent, false)
|
||||
mInflater.inflate(R.layout.item_list_nodes_group, parent, false)
|
||||
} else {
|
||||
inflater.inflate(R.layout.item_list_nodes_entry, parent, false)
|
||||
mInflater.inflate(R.layout.item_list_nodes_entry, parent, false)
|
||||
}
|
||||
return NodeViewHolder(view)
|
||||
val nodeViewHolder = NodeViewHolder(view)
|
||||
mInfoTextDefaultDimension = nodeViewHolder.text.textSize
|
||||
mSubtextDefaultDimension = nodeViewHolder.subText.textSize
|
||||
nodeViewHolder.numberChildren?.let {
|
||||
mNumberChildrenTextDefaultDimension = it.textSize
|
||||
}
|
||||
return nodeViewHolder
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: NodeViewHolder, position: Int) {
|
||||
val subNode = nodeSortedList.get(position)
|
||||
val subNode = mNodeSortedList.get(position)
|
||||
|
||||
// Node selection
|
||||
holder.container.isSelected = actionNodesList.contains(subNode)
|
||||
holder.container.isSelected = mActionNodesList.contains(subNode)
|
||||
|
||||
// Assign image
|
||||
val iconColor = if (holder.container.isSelected)
|
||||
contentSelectionColor
|
||||
mContentSelectionColor
|
||||
else when (subNode.type) {
|
||||
Type.GROUP -> iconGroupColor
|
||||
Type.ENTRY -> iconEntryColor
|
||||
Type.GROUP -> mIconGroupColor
|
||||
Type.ENTRY -> mIconEntryColor
|
||||
}
|
||||
holder.imageIdentifier?.setColorFilter(iconColor)
|
||||
holder.icon.apply {
|
||||
assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
|
||||
// Relative size of the icon
|
||||
layoutParams?.apply {
|
||||
height = (iconDefaultDimension * prefSizeMultiplier).toInt()
|
||||
width = (iconDefaultDimension * prefSizeMultiplier).toInt()
|
||||
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
|
||||
width = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
// Assign text
|
||||
holder.text.apply {
|
||||
text = subNode.title
|
||||
setTextSize(textSizeUnit, infoTextDefaultDimension, prefSizeMultiplier)
|
||||
setTextSize(mTextSizeUnit, mInfoTextDefaultDimension, mPrefSizeMultiplier)
|
||||
strikeOut(subNode.isCurrentlyExpires)
|
||||
}
|
||||
// Add subText with username
|
||||
@@ -331,24 +334,27 @@ class NodeAdapter (private val context: Context)
|
||||
holder.text.text = entry.getVisualTitle()
|
||||
holder.subText.apply {
|
||||
val username = entry.username
|
||||
if (showUserNames && username.isNotEmpty()) {
|
||||
if (mShowUserNames && username.isNotEmpty()) {
|
||||
visibility = View.VISIBLE
|
||||
text = username
|
||||
setTextSize(textSizeUnit, subtextDefaultDimension, prefSizeMultiplier)
|
||||
setTextSize(mTextSizeUnit, mSubtextDefaultDimension, mPrefSizeMultiplier)
|
||||
}
|
||||
}
|
||||
|
||||
holder.attachmentIcon?.visibility =
|
||||
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
||||
|
||||
mDatabase.stopManageEntry(entry)
|
||||
}
|
||||
|
||||
// Add number of entries in groups
|
||||
if (subNode.type == Type.GROUP) {
|
||||
if (showNumberEntries) {
|
||||
if (mShowNumberEntries) {
|
||||
holder.numberChildren?.apply {
|
||||
text = (subNode as Group)
|
||||
.getNumberOfChildEntries(entryFilters)
|
||||
.getNumberOfChildEntries(mEntryFilters)
|
||||
.toString()
|
||||
setTextSize(textSizeUnit, numberChildrenTextDefaultDimension, prefSizeMultiplier)
|
||||
setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier)
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
@@ -358,22 +364,22 @@ class NodeAdapter (private val context: Context)
|
||||
|
||||
// Assign click
|
||||
holder.container.setOnClickListener {
|
||||
nodeClickCallback?.onNodeClick(subNode)
|
||||
mNodeClickCallback?.onNodeClick(subNode)
|
||||
}
|
||||
holder.container.setOnLongClickListener {
|
||||
nodeClickCallback?.onNodeLongClick(subNode) ?: false
|
||||
mNodeClickCallback?.onNodeLongClick(subNode) ?: false
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return nodeSortedList.size()
|
||||
return mNodeSortedList.size()
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a listener when a node is clicked
|
||||
*/
|
||||
fun setOnNodeClickListener(nodeClickCallback: NodeClickCallback?) {
|
||||
this.nodeClickCallback = nodeClickCallback
|
||||
this.mNodeClickCallback = nodeClickCallback
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -391,6 +397,7 @@ class NodeAdapter (private val context: Context)
|
||||
var text: TextView = itemView.findViewById(R.id.node_text)
|
||||
var subText: TextView = itemView.findViewById(R.id.node_subtext)
|
||||
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
||||
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -100,6 +100,7 @@ class SearchEntryCursorAdapter(private val context: Context,
|
||||
} else {
|
||||
""
|
||||
}
|
||||
visibility = if (text.isEmpty()) View.GONE else View.VISIBLE
|
||||
strikeOut(currentEntry.isCurrentlyExpires)
|
||||
}
|
||||
}
|
||||
@@ -160,8 +161,8 @@ class SearchEntryCursorAdapter(private val context: Context,
|
||||
}
|
||||
|
||||
private class ViewHolder {
|
||||
internal var imageViewIcon: ImageView? = null
|
||||
internal var textViewTitle: TextView? = null
|
||||
internal var textViewSubTitle: TextView? = null
|
||||
var imageViewIcon: ImageView? = null
|
||||
var textViewTitle: TextView? = null
|
||||
var textViewSubTitle: TextView? = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.app
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
class App : MultiDexApplication() {
|
||||
|
||||
@@ -33,7 +34,7 @@ class App : MultiDexApplication() {
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
||||
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this))
|
||||
super.onTerminate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,19 +19,87 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.app.database
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.net.Uri
|
||||
import android.os.IBinder
|
||||
import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
||||
import java.util.*
|
||||
|
||||
class CipherDatabaseAction(applicationContext: Context) {
|
||||
class CipherDatabaseAction(context: Context) {
|
||||
|
||||
private val applicationContext = context.applicationContext
|
||||
private val cipherDatabaseDao =
|
||||
AppDatabase
|
||||
.getDatabase(applicationContext)
|
||||
.cipherDatabaseDao()
|
||||
|
||||
// Temp DAO to easily remove content if object no longer in memory
|
||||
private var useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
|
||||
|
||||
private val mIntentAdvancedUnlockService = Intent(applicationContext,
|
||||
AdvancedUnlockNotificationService::class.java)
|
||||
private var mBinder: AdvancedUnlockNotificationService.AdvancedUnlockBinder? = null
|
||||
private var mServiceConnection: ServiceConnection? = null
|
||||
|
||||
private var mDatabaseListeners = LinkedList<DatabaseListener>()
|
||||
|
||||
fun reloadPreferences() {
|
||||
useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun attachService(performedAction: () -> Unit) {
|
||||
// Check if a service is currently running else do nothing
|
||||
if (mBinder != null) {
|
||||
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) {
|
||||
mDatabaseListeners.add(listener)
|
||||
}
|
||||
|
||||
fun unregisterDatabaseListener(listener: DatabaseListener) {
|
||||
mDatabaseListeners.remove(listener)
|
||||
}
|
||||
|
||||
interface DatabaseListener {
|
||||
fun onDatabaseCleared()
|
||||
}
|
||||
|
||||
fun getCipherDatabase(databaseUri: Uri,
|
||||
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
|
||||
if (useTempDao) {
|
||||
attachService {
|
||||
cipherDatabaseResultListener.invoke(mBinder?.getCipherDatabase(databaseUri))
|
||||
}
|
||||
} else {
|
||||
IOActionTask(
|
||||
{
|
||||
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
|
||||
@@ -41,6 +109,7 @@ class CipherDatabaseAction(applicationContext: Context) {
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
}
|
||||
|
||||
fun containsCipherDatabase(databaseUri: Uri,
|
||||
contains: (Boolean) -> Unit) {
|
||||
@@ -51,10 +120,15 @@ class CipherDatabaseAction(applicationContext: Context) {
|
||||
|
||||
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
|
||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||
if (useTempDao) {
|
||||
attachService {
|
||||
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
|
||||
cipherDatabaseResultListener?.invoke()
|
||||
}
|
||||
} else {
|
||||
IOActionTask(
|
||||
{
|
||||
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
|
||||
|
||||
// Update values if element not yet in the database
|
||||
if (cipherDatabaseRetrieve == null) {
|
||||
cipherDatabaseDao.add(cipherDatabaseEntity)
|
||||
@@ -67,9 +141,16 @@ class CipherDatabaseAction(applicationContext: Context) {
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteByDatabaseUri(databaseUri: Uri,
|
||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||
if (useTempDao) {
|
||||
attachService {
|
||||
mBinder?.deleteByDatabaseUri(databaseUri)
|
||||
cipherDatabaseResultListener?.invoke()
|
||||
}
|
||||
} else {
|
||||
IOActionTask(
|
||||
{
|
||||
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
|
||||
@@ -79,8 +160,12 @@ class CipherDatabaseAction(applicationContext: Context) {
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAll() {
|
||||
attachService {
|
||||
mBinder?.deleteAll()
|
||||
}
|
||||
IOActionTask(
|
||||
{
|
||||
cipherDatabaseDao.deleteAll()
|
||||
|
||||
@@ -43,6 +43,11 @@ data class CipherDatabaseEntity(
|
||||
parcel.readString()!!,
|
||||
parcel.readString()!!)
|
||||
|
||||
fun replaceContent(copy: CipherDatabaseEntity) {
|
||||
this.encryptedValue = copy.encryptedValue
|
||||
this.specParameters = copy.specParameters
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(databaseUri)
|
||||
parcel.writeString(encryptedValue)
|
||||
|
||||
@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.app.database
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.model.DatabaseFile
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
||||
@@ -119,21 +120,29 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||
IOActionTask(
|
||||
{
|
||||
databaseFileToAddOrUpdate.databaseUri?.let { databaseUri ->
|
||||
// Try to get info in database first
|
||||
val fileDatabaseHistoryRetrieve = databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||
|
||||
// Complete alias if not exists
|
||||
val fileDatabaseHistory = FileDatabaseHistoryEntity(
|
||||
databaseUri.toString(),
|
||||
databaseFileToAddOrUpdate.databaseAlias ?: "",
|
||||
databaseFileToAddOrUpdate.databaseAlias
|
||||
?: fileDatabaseHistoryRetrieve?.databaseAlias
|
||||
?: "",
|
||||
databaseFileToAddOrUpdate.keyFileUri?.toString(),
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
|
||||
val fileDatabaseHistoryRetrieve = databaseFileHistoryDao.getByDatabaseUri(fileDatabaseHistory.databaseUri)
|
||||
|
||||
// Update values if history element not yet in the database
|
||||
try {
|
||||
if (fileDatabaseHistoryRetrieve == null) {
|
||||
databaseFileHistoryDao.add(fileDatabaseHistory)
|
||||
} else {
|
||||
databaseFileHistoryDao.update(fileDatabaseHistory)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to add or update database history", e)
|
||||
}
|
||||
|
||||
val fileDatabaseInfo = FileDatabaseInfo(applicationContext,
|
||||
fileDatabaseHistory.databaseUri)
|
||||
@@ -204,5 +213,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||
).execute()
|
||||
}
|
||||
|
||||
companion object : SingletonHolderParameter<FileDatabaseHistoryAction, Context>(::FileDatabaseHistoryAction)
|
||||
companion object : SingletonHolderParameter<FileDatabaseHistoryAction, Context>(::FileDatabaseHistoryAction) {
|
||||
private val TAG = FileDatabaseHistoryAction::class.java.name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
@@ -157,11 +157,9 @@ object AutofillHelper {
|
||||
intent: Intent,
|
||||
assistStructure: AssistStructure,
|
||||
searchInfo: SearchInfo?) {
|
||||
EntrySelectionHelper.addEntrySelectionModeExtraInIntent(intent)
|
||||
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||
intent.putExtra(ASSIST_STRUCTURE, assistStructure)
|
||||
searchInfo?.let {
|
||||
intent.putExtra(KEY_SEARCH_INFO, it)
|
||||
}
|
||||
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
|
||||
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,54 +23,75 @@ import android.os.Build
|
||||
import android.os.CancellationSignal
|
||||
import android.service.autofill.*
|
||||
import android.util.Log
|
||||
import android.view.autofill.AutofillId
|
||||
import android.widget.RemoteViews
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
class KeeAutofillService : AutofillService() {
|
||||
|
||||
var applicationIdBlocklist: Set<String>? = null
|
||||
var webDomainBlocklist: Set<String>? = null
|
||||
var askToSaveData: Boolean = false
|
||||
private var mLock = AtomicBoolean()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this)
|
||||
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this)
|
||||
askToSaveData = PreferencesUtil.askToSaveAutofillData(this) // TODO apply when changed
|
||||
}
|
||||
|
||||
override fun onFillRequest(request: FillRequest,
|
||||
cancellationSignal: CancellationSignal,
|
||||
callback: FillCallback) {
|
||||
val fillContexts = request.fillContexts
|
||||
val latestStructure = fillContexts[fillContexts.size - 1].structure
|
||||
|
||||
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
|
||||
|
||||
// Lock
|
||||
if (!mLock.get()) {
|
||||
mLock.set(true)
|
||||
// Check user's settings for authenticating Responses and Datasets.
|
||||
val latestStructure = request.fillContexts.last().structure
|
||||
StructureParser(latestStructure).parse()?.let { parseResult ->
|
||||
|
||||
// Build search info only if applicationId or webDomain are not blocked
|
||||
if (searchAllowedFor(parseResult.applicationId, applicationIdBlocklist)
|
||||
&& searchAllowedFor(parseResult.domain, webDomainBlocklist)) {
|
||||
if (autofillAllowedFor(parseResult.applicationId, applicationIdBlocklist)
|
||||
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
|
||||
val searchInfo = SearchInfo().apply {
|
||||
applicationId = parseResult.applicationId
|
||||
webDomain = parseResult.domain
|
||||
webDomain = parseResult.webDomain
|
||||
webScheme = parseResult.webScheme
|
||||
}
|
||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
|
||||
searchInfo.webDomain = webDomainWithoutSubDomain
|
||||
launchSelection(searchInfo, parseResult, callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchSelection(searchInfo: SearchInfo,
|
||||
parseResult: StructureParser.Result,
|
||||
callback: FillCallback) {
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
Database.getInstance(),
|
||||
searchInfo,
|
||||
{ items ->
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
AutofillHelper.addHeader(responseBuilder, packageName,
|
||||
parseResult.domain, parseResult.applicationId)
|
||||
parseResult.webDomain, parseResult.applicationId)
|
||||
items.forEach {
|
||||
responseBuilder.addDataset(AutofillHelper.buildDataset(this, it, parseResult))
|
||||
}
|
||||
@@ -86,8 +107,6 @@ class KeeAutofillService : AutofillService() {
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
||||
searchInfo: SearchInfo,
|
||||
@@ -96,12 +115,12 @@ class KeeAutofillService : AutofillService() {
|
||||
if (autofillIds.isNotEmpty()) {
|
||||
// If the entire Autofill Response is authenticated, AuthActivity is used
|
||||
// to generate Response.
|
||||
val sender = AutofillLauncherActivity.getAuthIntentSenderForResponse(this,
|
||||
val intentSender = AutofillLauncherActivity.getAuthIntentSenderForSelection(this,
|
||||
searchInfo)
|
||||
val responseBuilder = FillResponse.Builder()
|
||||
val remoteViewsUnlock: RemoteViews = if (!parseResult.domain.isNullOrEmpty()) {
|
||||
val remoteViewsUnlock: RemoteViews = if (!parseResult.webDomain.isNullOrEmpty()) {
|
||||
RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply {
|
||||
setTextViewText(R.id.autofill_web_domain_text, parseResult.domain)
|
||||
setTextViewText(R.id.autofill_web_domain_text, parseResult.webDomain)
|
||||
}
|
||||
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
||||
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
|
||||
@@ -110,15 +129,63 @@ class KeeAutofillService : AutofillService() {
|
||||
} else {
|
||||
RemoteViews(packageName, R.layout.item_autofill_unlock)
|
||||
}
|
||||
responseBuilder.setAuthentication(autofillIds, sender, remoteViewsUnlock)
|
||||
|
||||
// Tell to service the interest to save credentials
|
||||
if (askToSaveData) {
|
||||
var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC
|
||||
val info = ArrayList<AutofillId>()
|
||||
// Only if at least a password
|
||||
parseResult.passwordId?.let { passwordInfo ->
|
||||
parseResult.usernameId?.let { usernameInfo ->
|
||||
types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME
|
||||
info.add(usernameInfo)
|
||||
}
|
||||
types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD
|
||||
info.add(passwordInfo)
|
||||
}
|
||||
if (info.isNotEmpty()) {
|
||||
responseBuilder.setSaveInfo(
|
||||
SaveInfo.Builder(types, info.toTypedArray()).build()
|
||||
)
|
||||
}
|
||||
}
|
||||
// Build response
|
||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
|
||||
callback.onSuccess(responseBuilder.build())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
|
||||
// TODO Save autofill
|
||||
//callback.onFailure(getString(R.string.autofill_not_support_save));
|
||||
if (askToSaveData) {
|
||||
val latestStructure = request.fillContexts.last().structure
|
||||
StructureParser(latestStructure).parse(true)?.let { parseResult ->
|
||||
|
||||
if (autofillAllowedFor(parseResult.applicationId, applicationIdBlocklist)
|
||||
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
|
||||
Log.d(TAG, "autofill onSaveRequest password")
|
||||
|
||||
// Show UI to save data
|
||||
val registerInfo = RegisterInfo(SearchInfo().apply {
|
||||
applicationId = parseResult.applicationId
|
||||
webDomain = parseResult.webDomain
|
||||
webScheme = parseResult.webScheme
|
||||
},
|
||||
parseResult.usernameValue?.textValue?.toString(),
|
||||
parseResult.passwordValue?.textValue?.toString())
|
||||
// TODO Callback in each activity #765
|
||||
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,
|
||||
// registerInfo))
|
||||
//} else {
|
||||
AutofillLauncherActivity.launchForRegistration(this, registerInfo)
|
||||
callback.onSuccess()
|
||||
//}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
callback.onFailure("Saving form values is not allowed")
|
||||
}
|
||||
|
||||
override fun onConnected() {
|
||||
@@ -126,13 +193,14 @@ class KeeAutofillService : AutofillService() {
|
||||
}
|
||||
|
||||
override fun onDisconnected() {
|
||||
mLock.set(false)
|
||||
Log.d(TAG, "onDisconnected")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = KeeAutofillService::class.java.name
|
||||
|
||||
fun searchAllowedFor(element: String?, blockList: Set<String>?): Boolean {
|
||||
fun autofillAllowedFor(element: String?, blockList: Set<String>?): Boolean {
|
||||
element?.let { elementNotNull ->
|
||||
if (blockList?.any { appIdBlocked ->
|
||||
elementNotNull.contains(appIdBlocked)
|
||||
|
||||
@@ -25,6 +25,7 @@ import androidx.annotation.RequiresApi
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.autofill.AutofillId
|
||||
import android.view.autofill.AutofillValue
|
||||
import java.util.*
|
||||
|
||||
|
||||
@@ -34,14 +35,19 @@ import java.util.*
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
internal class StructureParser(private val structure: AssistStructure) {
|
||||
private var result: Result? = null
|
||||
private var usernameCandidate: AutofillId? = null
|
||||
|
||||
private var usernameNeeded = true
|
||||
|
||||
fun parse(): Result? {
|
||||
private var usernameCandidate: AutofillId? = null
|
||||
private var usernameValueCandidate: AutofillValue? = null
|
||||
|
||||
fun parse(saveValue: Boolean = false): Result? {
|
||||
try {
|
||||
result = Result()
|
||||
result?.apply {
|
||||
allowSaveValues = saveValue
|
||||
usernameCandidate = null
|
||||
usernameValueCandidate = null
|
||||
mainLoop@ for (i in 0 until structure.windowNodeCount) {
|
||||
val windowNode = structure.getWindowNodeAt(i)
|
||||
applicationId = windowNode.title.toString().split("/")[0]
|
||||
@@ -51,8 +57,12 @@ internal class StructureParser(private val structure: AssistStructure) {
|
||||
break@mainLoop
|
||||
}
|
||||
// If not explicit username field found, add the field just before password field.
|
||||
if (usernameId == null && passwordId != null && usernameCandidate != null)
|
||||
if (usernameId == null && passwordId != null && usernameCandidate != null) {
|
||||
usernameId = usernameCandidate
|
||||
if (allowSaveValues) {
|
||||
usernameValue = usernameValueCandidate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the result only if password field is retrieved
|
||||
@@ -68,11 +78,23 @@ internal class StructureParser(private val structure: AssistStructure) {
|
||||
|
||||
private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
|
||||
// Get the domain of a web app
|
||||
node.webDomain?.let {
|
||||
result?.domain = it
|
||||
Log.d(TAG, "Autofill domain: $it")
|
||||
node.webDomain?.let { webDomain ->
|
||||
if (webDomain.isNotEmpty()) {
|
||||
result?.webDomain = webDomain
|
||||
Log.d(TAG, "Autofill domain: $webDomain")
|
||||
}
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
node.webScheme?.let { webScheme ->
|
||||
if (webScheme.isNotEmpty()) {
|
||||
result?.webScheme = webScheme
|
||||
Log.d(TAG, "Autofill scheme: $webScheme")
|
||||
}
|
||||
}
|
||||
}
|
||||
val domainNotEmpty = result?.webDomain?.isNotEmpty() == true
|
||||
|
||||
var returnValue = false
|
||||
// Only parse visible nodes
|
||||
if (node.visibility == View.VISIBLE) {
|
||||
if (node.autofillId != null
|
||||
@@ -81,39 +103,41 @@ internal class StructureParser(private val structure: AssistStructure) {
|
||||
val hints = node.autofillHints
|
||||
if (hints != null && hints.isNotEmpty()) {
|
||||
if (parseNodeByAutofillHint(node))
|
||||
return true
|
||||
returnValue = true
|
||||
} else if (parseNodeByHtmlAttributes(node))
|
||||
return true
|
||||
returnValue = true
|
||||
else if (parseNodeByAndroidInput(node))
|
||||
return true
|
||||
returnValue = true
|
||||
}
|
||||
// Optimized return but only if domain not empty
|
||||
if (domainNotEmpty && returnValue)
|
||||
return true
|
||||
// Recursive method to process each node
|
||||
for (i in 0 until node.childCount) {
|
||||
if (parseViewNode(node.getChildAt(i)))
|
||||
returnValue = true
|
||||
if (domainNotEmpty && returnValue)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return returnValue
|
||||
}
|
||||
|
||||
private fun parseNodeByAutofillHint(node: AssistStructure.ViewNode): Boolean {
|
||||
val autofillId = node.autofillId
|
||||
node.autofillHints?.forEach {
|
||||
when {
|
||||
it.equals(View.AUTOFILL_HINT_USERNAME, true)
|
||||
|| it.equals(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|
||||
|| it.equals("email", true)
|
||||
|| it.equals(View.AUTOFILL_HINT_PHONE, true)
|
||||
|| it.contains("OrUsername", true)
|
||||
|| it.contains("OrEmailAddress", true)
|
||||
|| it.contains("OrEmail", true)
|
||||
|| it.contains("OrPhone", true)-> {
|
||||
it.contains(View.AUTOFILL_HINT_USERNAME, true)
|
||||
|| it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|
||||
|| it.contains("email", true)
|
||||
|| it.contains(View.AUTOFILL_HINT_PHONE, true)-> {
|
||||
result?.usernameId = autofillId
|
||||
result?.usernameValue = node.autofillValue
|
||||
Log.d(TAG, "Autofill username hint")
|
||||
}
|
||||
it.equals(View.AUTOFILL_HINT_PASSWORD, true)
|
||||
|| it.contains("password", true) -> {
|
||||
it.contains(View.AUTOFILL_HINT_PASSWORD, true) -> {
|
||||
result?.passwordId = autofillId
|
||||
result?.passwordValue = node.autofillValue
|
||||
Log.d(TAG, "Autofill password hint")
|
||||
// Username not needed in this case
|
||||
usernameNeeded = false
|
||||
@@ -143,14 +167,17 @@ internal class StructureParser(private val structure: AssistStructure) {
|
||||
when (pairAttribute.second.toLowerCase(Locale.ENGLISH)) {
|
||||
"tel", "email" -> {
|
||||
result?.usernameId = autofillId
|
||||
result?.usernameValue = node.autofillValue
|
||||
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||
}
|
||||
"text" -> {
|
||||
usernameCandidate = autofillId
|
||||
usernameValueCandidate = node.autofillValue
|
||||
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||
}
|
||||
"password" -> {
|
||||
result?.passwordId = autofillId
|
||||
result?.passwordValue = node.autofillValue
|
||||
Log.d(TAG, "Autofill password web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||
return true
|
||||
}
|
||||
@@ -185,6 +212,7 @@ internal class StructureParser(private val structure: AssistStructure) {
|
||||
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS) -> {
|
||||
result?.usernameId = autofillId
|
||||
result?.usernameValue = node.autofillValue
|
||||
Log.d(TAG, "Autofill username android text type: ${showHexInputType(inputType)}")
|
||||
}
|
||||
inputIsVariationType(inputType,
|
||||
@@ -192,6 +220,7 @@ internal class StructureParser(private val structure: AssistStructure) {
|
||||
InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> {
|
||||
usernameCandidate = autofillId
|
||||
usernameValueCandidate = node.autofillValue
|
||||
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
|
||||
}
|
||||
inputIsVariationType(inputType,
|
||||
@@ -199,6 +228,7 @@ internal class StructureParser(private val structure: AssistStructure) {
|
||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
|
||||
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) -> {
|
||||
result?.passwordId = autofillId
|
||||
result?.passwordValue = node.autofillValue
|
||||
Log.d(TAG, "Autofill password android text type: ${showHexInputType(inputType)}")
|
||||
usernameNeeded = false
|
||||
return true
|
||||
@@ -223,11 +253,13 @@ internal class StructureParser(private val structure: AssistStructure) {
|
||||
inputIsVariationType(inputType,
|
||||
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
|
||||
usernameCandidate = autofillId
|
||||
usernameValueCandidate = node.autofillValue
|
||||
Log.d(TAG, "Autofill usernale candidate android number type: ${showHexInputType(inputType)}")
|
||||
}
|
||||
inputIsVariationType(inputType,
|
||||
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {
|
||||
result?.passwordId = autofillId
|
||||
result?.passwordValue = node.autofillValue
|
||||
Log.d(TAG, "Autofill password android number type: ${showHexInputType(inputType)}")
|
||||
usernameNeeded = false
|
||||
return true
|
||||
@@ -244,7 +276,14 @@ internal class StructureParser(private val structure: AssistStructure) {
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
internal class Result {
|
||||
var applicationId: String? = null
|
||||
var domain: String? = null
|
||||
|
||||
var webDomain: String? = null
|
||||
set(value) {
|
||||
if (field == null)
|
||||
field = value
|
||||
}
|
||||
|
||||
var webScheme: String? = null
|
||||
set(value) {
|
||||
if (field == null)
|
||||
field = value
|
||||
@@ -272,6 +311,21 @@ internal class StructureParser(private val structure: AssistStructure) {
|
||||
}
|
||||
return all.toTypedArray()
|
||||
}
|
||||
|
||||
// Only in registration mode
|
||||
var allowSaveValues = false
|
||||
|
||||
var usernameValue: AutofillValue? = null
|
||||
set(value) {
|
||||
if (allowSaveValues && field == null)
|
||||
field = value
|
||||
}
|
||||
|
||||
var passwordValue: AutofillValue? = null
|
||||
set(value) {
|
||||
if (allowSaveValues && field == null)
|
||||
field = value
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -30,12 +30,12 @@ import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricConstants
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||
|
||||
@@ -51,13 +51,17 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
: BiometricUnlockDatabaseHelper.BiometricUnlockCallback {
|
||||
|
||||
private var biometricUnlockDatabaseHelper: BiometricUnlockDatabaseHelper? = null
|
||||
private var biometricMode: Mode = Mode.UNAVAILABLE
|
||||
private var biometricMode: Mode = Mode.BIOMETRIC_UNAVAILABLE
|
||||
|
||||
// Only to fix multiple fingerprint menu #332
|
||||
private var mAllowAdvancedUnlockMenu = false
|
||||
private var mAddBiometricMenuInProgress = false
|
||||
|
||||
/**
|
||||
* Manage setting to auto open biometric prompt
|
||||
*/
|
||||
private var biometricPromptAutoOpenPreference = PreferencesUtil.isBiometricPromptAutoOpenEnable(context)
|
||||
var isBiometricPromptAutoOpenEnable: Boolean = true
|
||||
private var biometricPromptAutoOpenPreference = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(context)
|
||||
var isBiometricPromptAutoOpenEnable: Boolean = false
|
||||
get() {
|
||||
return field && biometricPromptAutoOpenPreference
|
||||
}
|
||||
@@ -68,6 +72,12 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
|
||||
private var cipherDatabaseAction = CipherDatabaseAction.getInstance(context.applicationContext)
|
||||
|
||||
private val cipherDatabaseListener = object: CipherDatabaseAction.DatabaseListener {
|
||||
override fun onDatabaseCleared() {
|
||||
deleteEncryptedDatabaseKey()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
// Add a check listener to change fingerprint mode
|
||||
checkboxPasswordView?.setOnCheckedChangeListener { compoundButton, checked ->
|
||||
@@ -75,6 +85,10 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
// Add old listener to enable the button, only be call here because of onCheckedChange bug
|
||||
onCheckedPasswordChangeListener?.onCheckedChanged(compoundButton, checked)
|
||||
}
|
||||
cipherDatabaseAction.apply {
|
||||
reloadPreferences()
|
||||
registerDatabaseListener(cipherDatabaseListener)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,15 +96,23 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
*/
|
||||
fun checkBiometricAvailability() {
|
||||
|
||||
if (PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
|
||||
advancedUnlockInfoView?.setIconResource(R.drawable.bolt)
|
||||
} else if (PreferencesUtil.isBiometricUnlockEnable(context)) {
|
||||
advancedUnlockInfoView?.setIconResource(R.drawable.fingerprint)
|
||||
}
|
||||
|
||||
// biometric not supported (by API level or hardware) so keep option hidden
|
||||
// or manually disable
|
||||
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate()
|
||||
val biometricCanAuthenticate = BiometricUnlockDatabaseHelper.canAuthenticate(context)
|
||||
allowOpenBiometricPrompt = true
|
||||
|
||||
if (!PreferencesUtil.isBiometricUnlockEnable(context)
|
||||
if (!PreferencesUtil.isAdvancedUnlockEnable(context)
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
|
||||
toggleMode(Mode.UNAVAILABLE)
|
||||
toggleMode(Mode.BIOMETRIC_UNAVAILABLE)
|
||||
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED){
|
||||
toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
|
||||
} else {
|
||||
// biometric is available but not configured, show icon but in disabled state with some information
|
||||
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
|
||||
@@ -131,7 +153,7 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
private fun toggleMode(newBiometricMode: Mode) {
|
||||
if (newBiometricMode != biometricMode) {
|
||||
biometricMode = newBiometricMode
|
||||
initBiometricMode()
|
||||
initAdvancedUnlockMode()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,27 +171,34 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
override fun onAuthenticationFailed() {
|
||||
context.runOnUiThread {
|
||||
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
|
||||
setAdvancedUnlockedMessageView(R.string.biometric_not_recognized)
|
||||
setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
context.runOnUiThread {
|
||||
when (biometricMode) {
|
||||
Mode.UNAVAILABLE -> {}
|
||||
Mode.BIOMETRIC_NOT_CONFIGURED -> {}
|
||||
Mode.KEY_MANAGER_UNAVAILABLE -> {}
|
||||
Mode.WAIT_CREDENTIAL -> {}
|
||||
Mode.BIOMETRIC_UNAVAILABLE -> {
|
||||
}
|
||||
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> {
|
||||
}
|
||||
Mode.BIOMETRIC_NOT_CONFIGURED -> {
|
||||
}
|
||||
Mode.KEY_MANAGER_UNAVAILABLE -> {
|
||||
}
|
||||
Mode.WAIT_CREDENTIAL -> {
|
||||
}
|
||||
Mode.STORE_CREDENTIAL -> {
|
||||
// newly store the entered password in encrypted way
|
||||
biometricUnlockDatabaseHelper?.encryptData(passwordView?.text.toString())
|
||||
AdvancedUnlockNotificationService.startServiceForTimeout(context)
|
||||
}
|
||||
Mode.EXTRACT_CREDENTIAL -> {
|
||||
// retrieve the encrypted value from preferences
|
||||
cipherDatabaseAction.getCipherDatabase(databaseFileUri) {
|
||||
it?.encryptedValue?.let { value ->
|
||||
cipherDatabaseAction.getCipherDatabase(databaseFileUri) { cipherDatabase ->
|
||||
cipherDatabase?.encryptedValue?.let { value ->
|
||||
biometricUnlockDatabaseHelper?.decryptData(value)
|
||||
}
|
||||
} ?: deleteEncryptedDatabaseKey()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,24 +212,34 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
advancedUnlockInfoView?.setIconViewClickListener(false, null)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun openBiometricSetting() {
|
||||
advancedUnlockInfoView?.setIconViewClickListener(false) {
|
||||
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
|
||||
context.startActivity(Intent(Settings.ACTION_SETTINGS))
|
||||
}
|
||||
}
|
||||
|
||||
private fun initSecurityUpdateRequired() {
|
||||
showFingerPrintViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.biometric_security_update_required)
|
||||
|
||||
openBiometricSetting()
|
||||
}
|
||||
|
||||
private fun initNotConfigured() {
|
||||
showFingerPrintViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.configure_biometric)
|
||||
setAdvancedUnlockedMessageView("")
|
||||
|
||||
advancedUnlockInfoView?.setIconViewClickListener(false) {
|
||||
context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
|
||||
}
|
||||
openBiometricSetting()
|
||||
}
|
||||
|
||||
private fun initKeyManagerNotAvailable() {
|
||||
showFingerPrintViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
|
||||
setAdvancedUnlockedMessageView("")
|
||||
|
||||
advancedUnlockInfoView?.setIconViewClickListener(false) {
|
||||
context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
|
||||
}
|
||||
openBiometricSetting()
|
||||
}
|
||||
|
||||
private fun initWaitData() {
|
||||
@@ -209,81 +248,97 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
setAdvancedUnlockedMessageView("")
|
||||
|
||||
advancedUnlockInfoView?.setIconViewClickListener(false) {
|
||||
biometricAuthenticationCallback.onAuthenticationError(
|
||||
BiometricConstants.ERROR_UNABLE_TO_PROCESS
|
||||
, context.getString(R.string.credential_before_click_biometric_button))
|
||||
biometricAuthenticationCallback.onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
|
||||
context.getString(R.string.credential_before_click_advanced_unlock_button))
|
||||
}
|
||||
}
|
||||
|
||||
private fun openBiometricPrompt(biometricPrompt: BiometricPrompt?,
|
||||
cryptoObject: BiometricPrompt.CryptoObject,
|
||||
cryptoObject: BiometricPrompt.CryptoObject?,
|
||||
promptInfo: BiometricPrompt.PromptInfo) {
|
||||
context.runOnUiThread {
|
||||
if (allowOpenBiometricPrompt)
|
||||
biometricPrompt?.authenticate(promptInfo, cryptoObject)
|
||||
if (allowOpenBiometricPrompt) {
|
||||
if (biometricPrompt != null) {
|
||||
if (cryptoObject != null) {
|
||||
biometricPrompt.authenticate(promptInfo, cryptoObject)
|
||||
} else {
|
||||
setAdvancedUnlockedTitleView(R.string.crypto_object_not_initialized)
|
||||
}
|
||||
} else {
|
||||
setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEncryptData() {
|
||||
showFingerPrintViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.open_biometric_prompt_store_credential)
|
||||
setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_store_credential)
|
||||
setAdvancedUnlockedMessageView("")
|
||||
|
||||
biometricUnlockDatabaseHelper?.initEncryptData { biometricPrompt, cryptoObject, promptInfo ->
|
||||
|
||||
cryptoObject?.let { crypto ->
|
||||
// Set listener to open the biometric dialog and save credential
|
||||
advancedUnlockInfoView?.setIconViewClickListener { _ ->
|
||||
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
|
||||
openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun initDecryptData() {
|
||||
showFingerPrintViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.open_biometric_prompt_unlock_database)
|
||||
setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_unlock_database)
|
||||
setAdvancedUnlockedMessageView("")
|
||||
|
||||
if (biometricUnlockDatabaseHelper != null) {
|
||||
cipherDatabaseAction.getCipherDatabase(databaseFileUri) {
|
||||
cipherDatabaseAction.getCipherDatabase(databaseFileUri) { cipherDatabase ->
|
||||
cipherDatabase?.let {
|
||||
biometricUnlockDatabaseHelper?.initDecryptData(it.specParameters) { biometricPrompt, cryptoObject, promptInfo ->
|
||||
|
||||
it?.specParameters?.let { specs ->
|
||||
biometricUnlockDatabaseHelper?.initDecryptData(specs) { biometricPrompt, cryptoObject, promptInfo ->
|
||||
|
||||
cryptoObject?.let { crypto ->
|
||||
// Set listener to open the biometric dialog and check credential
|
||||
advancedUnlockInfoView?.setIconViewClickListener { _ ->
|
||||
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
|
||||
openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
|
||||
}
|
||||
|
||||
// Auto open the biometric prompt
|
||||
if (isBiometricPromptAutoOpenEnable) {
|
||||
isBiometricPromptAutoOpenEnable = false
|
||||
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
|
||||
}
|
||||
}
|
||||
|
||||
openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
|
||||
}
|
||||
}
|
||||
} ?: deleteEncryptedDatabaseKey()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun initBiometricMode() {
|
||||
fun initAdvancedUnlockMode() {
|
||||
mAllowAdvancedUnlockMenu = false
|
||||
when (biometricMode) {
|
||||
Mode.UNAVAILABLE -> initNotAvailable()
|
||||
Mode.BIOMETRIC_UNAVAILABLE -> initNotAvailable()
|
||||
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> initSecurityUpdateRequired()
|
||||
Mode.BIOMETRIC_NOT_CONFIGURED -> initNotConfigured()
|
||||
Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable()
|
||||
Mode.WAIT_CREDENTIAL -> initWaitData()
|
||||
Mode.STORE_CREDENTIAL -> initEncryptData()
|
||||
Mode.EXTRACT_CREDENTIAL -> initDecryptData()
|
||||
}
|
||||
|
||||
invalidateBiometricMenu()
|
||||
}
|
||||
|
||||
private fun invalidateBiometricMenu() {
|
||||
// Show fingerprint key deletion
|
||||
if (!mAddBiometricMenuInProgress) {
|
||||
mAddBiometricMenuInProgress = true
|
||||
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipher ->
|
||||
mAllowAdvancedUnlockMenu = containsCipher
|
||||
&& (biometricMode != Mode.BIOMETRIC_UNAVAILABLE
|
||||
&& biometricMode != Mode.KEY_MANAGER_UNAVAILABLE)
|
||||
mAddBiometricMenuInProgress = false
|
||||
context.invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
// Close the biometric prompt
|
||||
@@ -291,29 +346,22 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
biometricUnlockDatabaseHelper?.closeBiometricPrompt()
|
||||
// Restore the checked listener
|
||||
checkboxPasswordView?.setOnCheckedChangeListener(onCheckedPasswordChangeListener)
|
||||
cipherDatabaseAction.unregisterDatabaseListener(cipherDatabaseListener)
|
||||
}
|
||||
|
||||
// Only to fix multiple fingerprint menu #332
|
||||
private var addBiometricMenuInProgress = false
|
||||
fun inflateOptionsMenu(menuInflater: MenuInflater, menu: Menu) {
|
||||
if (!addBiometricMenuInProgress) {
|
||||
addBiometricMenuInProgress = true
|
||||
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) {
|
||||
if ((biometricMode != Mode.UNAVAILABLE && biometricMode != Mode.BIOMETRIC_NOT_CONFIGURED)
|
||||
&& it) {
|
||||
if (mAllowAdvancedUnlockMenu)
|
||||
menuInflater.inflate(R.menu.advanced_unlock, menu)
|
||||
addBiometricMenuInProgress = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteEntryKey() {
|
||||
biometricUnlockDatabaseHelper?.deleteEntryKey()
|
||||
cipherDatabaseAction.deleteByDatabaseUri(databaseFileUri)
|
||||
biometricMode = Mode.BIOMETRIC_NOT_CONFIGURED
|
||||
fun deleteEncryptedDatabaseKey() {
|
||||
allowOpenBiometricPrompt = false
|
||||
advancedUnlockInfoView?.setIconViewClickListener(false, null)
|
||||
biometricUnlockDatabaseHelper?.closeBiometricPrompt()
|
||||
cipherDatabaseAction.deleteByDatabaseUri(databaseFileUri) {
|
||||
checkBiometricAvailability()
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {
|
||||
loadDatabaseAfterRegisterCredentials.invoke(encryptedValue, ivSpec)
|
||||
@@ -325,13 +373,12 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
}
|
||||
|
||||
override fun onInvalidKeyException(e: Exception) {
|
||||
setAdvancedUnlockedMessageView(R.string.biometric_invalid_key)
|
||||
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
|
||||
}
|
||||
|
||||
override fun onBiometricException(e: Exception) {
|
||||
e.localizedMessage?.let {
|
||||
setAdvancedUnlockedMessageView(it)
|
||||
}
|
||||
val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: ""
|
||||
setAdvancedUnlockedMessageView(errorMessage)
|
||||
}
|
||||
|
||||
private fun showFingerPrintViews(show: Boolean) {
|
||||
@@ -359,7 +406,13 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||
}
|
||||
|
||||
enum class Mode {
|
||||
UNAVAILABLE, BIOMETRIC_NOT_CONFIGURED, KEY_MANAGER_UNAVAILABLE, WAIT_CREDENTIAL, STORE_CREDENTIAL, EXTRACT_CREDENTIAL
|
||||
BIOMETRIC_UNAVAILABLE,
|
||||
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
|
||||
BIOMETRIC_NOT_CONFIGURED,
|
||||
KEY_MANAGER_UNAVAILABLE,
|
||||
WAIT_CREDENTIAL,
|
||||
STORE_CREDENTIAL,
|
||||
EXTRACT_CREDENTIAL
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -29,9 +29,11 @@ import android.util.Base64
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricManager.Authenticators.*
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import java.security.KeyStore
|
||||
import java.security.UnrecoverableKeyException
|
||||
import java.util.concurrent.Executors
|
||||
@@ -56,31 +58,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
||||
var authenticationCallback: BiometricPrompt.AuthenticationCallback? = null
|
||||
var biometricUnlockCallback: BiometricUnlockCallback? = null
|
||||
|
||||
private val promptInfoStoreCredential = BiometricPrompt.PromptInfo.Builder().apply {
|
||||
setTitle(context.getString(R.string.biometric_prompt_store_credential_title))
|
||||
setDescription(context.getString(R.string.biometric_prompt_store_credential_message))
|
||||
setConfirmationRequired(true)
|
||||
// TODO device credential #102 #152
|
||||
/*
|
||||
if (keyguardManager?.isDeviceSecure == true)
|
||||
setDeviceCredentialAllowed(true)
|
||||
else
|
||||
*/
|
||||
setNegativeButtonText(context.getString(android.R.string.cancel))
|
||||
}.build()
|
||||
|
||||
private val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply {
|
||||
setTitle(context.getString(R.string.biometric_prompt_extract_credential_title))
|
||||
//setDescription(context.getString(R.string.biometric_prompt_extract_credential_message))
|
||||
setConfirmationRequired(false)
|
||||
// TODO device credential #102 #152
|
||||
/*
|
||||
if (keyguardManager?.isDeviceSecure == true)
|
||||
setDeviceCredentialAllowed(true)
|
||||
else
|
||||
*/
|
||||
setNegativeButtonText(context.getString(android.R.string.cancel))
|
||||
}.build()
|
||||
private val deviceCredentialUnlockEnable = PreferencesUtil.isDeviceCredentialUnlockEnable(context)
|
||||
|
||||
val isKeyManagerInitialized: Boolean
|
||||
get() {
|
||||
@@ -91,12 +69,8 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
||||
}
|
||||
|
||||
init {
|
||||
if (BiometricManager.from(context).canAuthenticate() != BiometricManager.BIOMETRIC_SUCCESS) {
|
||||
// really not much to do when no fingerprint support found
|
||||
isKeyManagerInit = false
|
||||
} else {
|
||||
if (allowInitKeyStore(context)) {
|
||||
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager?
|
||||
|
||||
try {
|
||||
this.keyStore = KeyStore.getInstance(BIOMETRIC_KEYSTORE)
|
||||
this.keyGenerator = KeyGenerator.getInstance(BIOMETRIC_KEY_ALGORITHM, BIOMETRIC_KEYSTORE)
|
||||
@@ -113,6 +87,9 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
||||
isKeyManagerInit = false
|
||||
biometricUnlockCallback?.onBiometricException(e)
|
||||
}
|
||||
} else {
|
||||
// really not much to do when no fingerprint support found
|
||||
isKeyManagerInit = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +115,12 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
||||
// Require the user to authenticate with a fingerprint to authorize every use
|
||||
// of the key
|
||||
.setUserAuthenticationRequired(true)
|
||||
.apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& deviceCredentialUnlockEnable) {
|
||||
setUserAuthenticationParameters(0, KeyProperties.AUTH_DEVICE_CREDENTIAL)
|
||||
}
|
||||
}
|
||||
.build())
|
||||
keyGenerator?.generateKey()
|
||||
}
|
||||
@@ -163,13 +146,27 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
// TODO if (keyguardManager?.isDeviceSecure == true) {
|
||||
getSecretKey()?.let { secretKey ->
|
||||
cipher?.init(Cipher.ENCRYPT_MODE, secretKey)
|
||||
|
||||
initBiometricPrompt()
|
||||
actionIfCypherInit.invoke(biometricPrompt, cryptoObject, promptInfoStoreCredential)
|
||||
}
|
||||
|
||||
val promptInfoStoreCredential = BiometricPrompt.PromptInfo.Builder().apply {
|
||||
setTitle(context.getString(R.string.advanced_unlock_prompt_store_credential_title))
|
||||
setDescription(context.getString(R.string.advanced_unlock_prompt_store_credential_message))
|
||||
setConfirmationRequired(true)
|
||||
if (deviceCredentialUnlockEnable) {
|
||||
setAllowedAuthenticators(DEVICE_CREDENTIAL)
|
||||
} else {
|
||||
setNegativeButtonText(context.getString(android.R.string.cancel))
|
||||
}
|
||||
}.build()
|
||||
|
||||
actionIfCypherInit.invoke(biometricPrompt,
|
||||
cryptoObject,
|
||||
promptInfoStoreCredential)
|
||||
}
|
||||
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
||||
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
|
||||
biometricUnlockCallback?.onInvalidKeyException(unrecoverableKeyException)
|
||||
@@ -195,10 +192,10 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
||||
val ivSpecValue = Base64.encodeToString(spec.iv, Base64.NO_WRAP)
|
||||
biometricUnlockCallback?.handleEncryptedResult(encryptedBase64, ivSpecValue)
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
val exception = Exception(context.getString(R.string.keystore_not_accessible), e)
|
||||
Log.e(TAG, "Unable to encrypt data", e)
|
||||
biometricUnlockCallback?.onBiometricException(e)
|
||||
biometricUnlockCallback?.onBiometricException(exception)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,6 +207,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
// TODO if (keyguardManager?.isDeviceSecure == true) {
|
||||
// important to restore spec here that was used for decryption
|
||||
val iv = Base64.decode(ivSpecValue, Base64.NO_WRAP)
|
||||
val spec = IvParameterSpec(iv)
|
||||
@@ -218,12 +216,25 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
||||
cipher?.init(Cipher.DECRYPT_MODE, secretKey, spec)
|
||||
|
||||
initBiometricPrompt()
|
||||
actionIfCypherInit.invoke(biometricPrompt, cryptoObject, promptInfoExtractCredential)
|
||||
}
|
||||
|
||||
val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply {
|
||||
setTitle(context.getString(R.string.advanced_unlock_prompt_extract_credential_title))
|
||||
//setDescription(context.getString(R.string.biometric_prompt_extract_credential_message))
|
||||
setConfirmationRequired(false)
|
||||
if (deviceCredentialUnlockEnable) {
|
||||
setAllowedAuthenticators(DEVICE_CREDENTIAL)
|
||||
} else {
|
||||
setNegativeButtonText(context.getString(android.R.string.cancel))
|
||||
}
|
||||
}.build()
|
||||
|
||||
actionIfCypherInit.invoke(biometricPrompt,
|
||||
cryptoObject,
|
||||
promptInfoExtractCredential)
|
||||
}
|
||||
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
||||
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
|
||||
deleteEntryKey()
|
||||
deleteKeystoreKey()
|
||||
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
||||
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
|
||||
biometricUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
||||
@@ -247,12 +258,13 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
||||
Log.e(TAG, "Unable to decrypt data", badPaddingException)
|
||||
biometricUnlockCallback?.onInvalidKeyException(badPaddingException)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to decrypt data", e)
|
||||
biometricUnlockCallback?.onBiometricException(e)
|
||||
val exception = Exception(context.getString(R.string.keystore_not_accessible), e)
|
||||
Log.e(TAG, "Unable to decrypt data", exception)
|
||||
biometricUnlockCallback?.onBiometricException(exception)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteEntryKey() {
|
||||
fun deleteKeystoreKey() {
|
||||
try {
|
||||
keyStore?.load(null)
|
||||
keyStore?.deleteEntry(BIOMETRIC_KEYSTORE_KEY)
|
||||
@@ -295,9 +307,79 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
||||
private const val BIOMETRIC_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
|
||||
private const val BIOMETRIC_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
fun canAuthenticate(context: Context): Int {
|
||||
return try {
|
||||
BiometricManager.from(context).canAuthenticate(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
|
||||
BIOMETRIC_STRONG or DEVICE_CREDENTIAL
|
||||
} else {
|
||||
BIOMETRIC_STRONG
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
|
||||
try {
|
||||
BiometricManager.from(context).canAuthenticate(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
|
||||
BIOMETRIC_WEAK or DEVICE_CREDENTIAL
|
||||
} else {
|
||||
BIOMETRIC_WEAK
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
|
||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
fun allowInitKeyStore(context: Context): Boolean {
|
||||
val biometricCanAuthenticate = canAuthenticate(context)
|
||||
return ( biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|
||||
)
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
fun biometricUnlockSupported(context: Context): Boolean {
|
||||
val biometricCanAuthenticate = try {
|
||||
BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
|
||||
try {
|
||||
BiometricManager.from(context).canAuthenticate(BIOMETRIC_WEAK)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
|
||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
|
||||
}
|
||||
}
|
||||
return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
|
||||
)
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.R)
|
||||
fun deviceCredentialUnlockSupported(context: Context): Boolean {
|
||||
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL)
|
||||
return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove entry key in keystore
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
fun deleteEntryKeyInKeystoreForBiometric(context: FragmentActivity,
|
||||
biometricCallback: BiometricUnlockErrorCallback) {
|
||||
BiometricUnlockDatabaseHelper(context).apply {
|
||||
@@ -315,7 +397,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
||||
biometricCallback.onBiometricException(e)
|
||||
}
|
||||
}
|
||||
deleteEntryKey()
|
||||
deleteKeystoreKey()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ class AesKdf : KdfEngine() {
|
||||
}
|
||||
}
|
||||
|
||||
override val defaultKeyRounds: Long = 6000L
|
||||
override val defaultKeyRounds: Long = 500000L
|
||||
|
||||
override fun getName(resources: Resources): String {
|
||||
return resources.getString(R.string.kdf_AES)
|
||||
|
||||
@@ -19,17 +19,23 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.crypto.keyDerivation
|
||||
|
||||
import com.kunzisoft.keepass.stream.LittleEndianDataInputStream
|
||||
import com.kunzisoft.keepass.stream.LittleEndianDataOutputStream
|
||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
||||
import com.kunzisoft.keepass.stream.uuidTo16Bytes
|
||||
import com.kunzisoft.keepass.utils.VariantDictionary
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
class KdfParameters(val uuid: UUID) : VariantDictionary() {
|
||||
class KdfParameters: VariantDictionary {
|
||||
|
||||
val uuid: UUID
|
||||
|
||||
constructor(uuid: UUID): super() {
|
||||
this.uuid = uuid
|
||||
}
|
||||
|
||||
constructor(uuid: UUID, d: VariantDictionary): super(d) {
|
||||
this.uuid = uuid
|
||||
}
|
||||
|
||||
fun setParamUUID() {
|
||||
setByteArray(PARAM_UUID, uuidTo16Bytes(uuid))
|
||||
@@ -41,25 +47,17 @@ class KdfParameters(val uuid: UUID) : VariantDictionary() {
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun deserialize(data: ByteArray): KdfParameters? {
|
||||
val inputStream = LittleEndianDataInputStream(ByteArrayInputStream(data))
|
||||
val dictionary = deserialize(inputStream)
|
||||
val dictionary = VariantDictionary.deserialize(data)
|
||||
|
||||
val uuidBytes = dictionary.getByteArray(PARAM_UUID) ?: return null
|
||||
val uuid = bytes16ToUuid(uuidBytes)
|
||||
|
||||
val kdfParameters = KdfParameters(uuid)
|
||||
kdfParameters.copyTo(dictionary)
|
||||
return kdfParameters
|
||||
return KdfParameters(uuid, dictionary)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun serialize(kdfParameters: KdfParameters): ByteArray {
|
||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||
val outputStream = LittleEndianDataOutputStream(byteArrayOutputStream)
|
||||
|
||||
serialize(kdfParameters, outputStream)
|
||||
|
||||
return byteArrayOutputStream.toByteArray()
|
||||
return VariantDictionary.serialize(kdfParameters)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ import android.util.Log
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.utils.closeDatabase
|
||||
|
||||
class CreateDatabaseRunnable(context: Context,
|
||||
private val mDatabase: Database,
|
||||
@@ -45,7 +47,7 @@ class CreateDatabaseRunnable(context: Context,
|
||||
createData(mDatabaseUri, databaseName, rootName)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
mDatabase.closeAndClear(context.applicationContext.filesDir)
|
||||
mDatabase.closeAndClear(UriUtil.getBinaryDir(context))
|
||||
setError(e)
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.utils.closeDatabase
|
||||
|
||||
class LoadDatabaseRunnable(private val context: Context,
|
||||
private val mDatabase: Database,
|
||||
@@ -45,7 +47,7 @@ class LoadDatabaseRunnable(private val context: Context,
|
||||
|
||||
override fun onStartRun() {
|
||||
// Clear before we load
|
||||
mDatabase.closeAndClear(context.applicationContext.filesDir)
|
||||
mDatabase.closeAndClear(UriUtil.getBinaryDir(context))
|
||||
}
|
||||
|
||||
override fun onActionRun() {
|
||||
@@ -53,7 +55,7 @@ class LoadDatabaseRunnable(private val context: Context,
|
||||
mDatabase.loadData(mUri, mPass, mKey,
|
||||
mReadonly,
|
||||
context.contentResolver,
|
||||
context.applicationContext.filesDir,
|
||||
UriUtil.getBinaryDir(context),
|
||||
mFixDuplicateUUID,
|
||||
progressTaskUpdater)
|
||||
}
|
||||
@@ -81,7 +83,7 @@ class LoadDatabaseRunnable(private val context: Context,
|
||||
// Register the current time to init the lock timer
|
||||
PreferencesUtil.saveCurrentTime(context)
|
||||
} else {
|
||||
mDatabase.closeAndClear(context.applicationContext.filesDir)
|
||||
mDatabase.closeAndClear(UriUtil.getBinaryDir(context))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import android.content.*
|
||||
import android.content.Context.BIND_ABOVE_CLIENT
|
||||
import android.content.Context.BIND_NOT_FOREGROUND
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
@@ -46,6 +45,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK
|
||||
@@ -169,6 +169,10 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
||||
serviceConnection = null
|
||||
}
|
||||
|
||||
fun isBinded(): Boolean {
|
||||
return mBinder != null
|
||||
}
|
||||
|
||||
fun registerProgressTask() {
|
||||
stopDialog()
|
||||
|
||||
@@ -463,6 +467,13 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
||||
, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseRemoveUnlinkedData(save: Boolean) {
|
||||
start(Bundle().apply {
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
|
||||
newMaxHistoryItems: Int,
|
||||
save: Boolean) {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
|
||||
class RemoveUnlinkedDataDatabaseRunnable (
|
||||
context: Context,
|
||||
database: Database,
|
||||
saveDatabase: Boolean)
|
||||
: SaveDatabaseRunnable(context, database, saveDatabase) {
|
||||
|
||||
override fun onActionRun() {
|
||||
try {
|
||||
database.removeUnlinkedAttachments()
|
||||
} catch (e: Exception) {
|
||||
setError(e)
|
||||
}
|
||||
|
||||
super.onActionRun()
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ class DeleteEntryHistoryDatabaseRunnable (
|
||||
|
||||
override fun onStartRun() {
|
||||
try {
|
||||
mainEntry.removeEntryFromHistory(entryHistoryPosition)
|
||||
database.removeEntryHistory(mainEntry, entryHistoryPosition)
|
||||
} catch (e: Exception) {
|
||||
setError(e)
|
||||
}
|
||||
|
||||
@@ -64,6 +64,10 @@ class DeleteNodesRunnable(context: Context,
|
||||
} else {
|
||||
database.deleteEntry(currentNode)
|
||||
}
|
||||
// Remove the oldest attachments
|
||||
currentNode.getAttachments(database.binaryPool).forEach {
|
||||
database.removeAttachmentIfNotUsed(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
package com.kunzisoft.keepass.database.action.node
|
||||
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
@@ -40,16 +41,34 @@ class UpdateEntryRunnable constructor(
|
||||
// WARNING : Re attribute parent removed in entry edit activity to save memory
|
||||
mNewEntry.addParentFrom(mOldEntry)
|
||||
|
||||
// Build oldest attachments
|
||||
val oldEntryAttachments = mOldEntry.getAttachments(database.binaryPool, true)
|
||||
val newEntryAttachments = mNewEntry.getAttachments(database.binaryPool, true)
|
||||
val attachmentsToRemove = ArrayList<Attachment>(oldEntryAttachments)
|
||||
// Not use equals because only check name
|
||||
newEntryAttachments.forEach { newAttachment ->
|
||||
oldEntryAttachments.forEach { oldAttachment ->
|
||||
if (oldAttachment.name == newAttachment.name
|
||||
&& oldAttachment.binaryAttachment == newAttachment.binaryAttachment)
|
||||
attachmentsToRemove.remove(oldAttachment)
|
||||
}
|
||||
}
|
||||
|
||||
// Update entry with new values
|
||||
mOldEntry.updateWith(mNewEntry)
|
||||
mNewEntry.touch(modified = true, touchParents = true)
|
||||
|
||||
// Create an entry history (an entry history don't have history)
|
||||
mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false))
|
||||
database.removeOldestEntryHistory(mOldEntry)
|
||||
database.removeOldestEntryHistory(mOldEntry, database.binaryPool)
|
||||
|
||||
// Only change data in index
|
||||
database.updateEntry(mOldEntry)
|
||||
|
||||
// Remove oldest attachments
|
||||
attachmentsToRemove.forEach {
|
||||
database.removeAttachmentIfNotUsed(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun nodeFinish(): ActionNodesValues {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
||||
|
||||
data class Attachment(var name: String,
|
||||
var binaryAttachment: BinaryAttachment) : Parcelable {
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readString() ?: "",
|
||||
parcel.readParcelable(BinaryAttachment::class.java.classLoader) ?: BinaryAttachment()
|
||||
)
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(name)
|
||||
parcel.writeParcelable(binaryAttachment, flags)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "$name at $binaryAttachment"
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Attachment) return false
|
||||
|
||||
if (name != other.name) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return name.hashCode()
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<Attachment> {
|
||||
override fun createFromParcel(parcel: Parcel): Attachment {
|
||||
return Attachment(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<Attachment?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,9 +25,7 @@ import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.element.database.*
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||
@@ -52,6 +50,7 @@ import com.kunzisoft.keepass.utils.SingletonHolder
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
|
||||
class Database {
|
||||
@@ -157,6 +156,17 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
fun compressionForNewEntry(): Boolean {
|
||||
if (mDatabaseKDB != null)
|
||||
return false
|
||||
// Default compression not necessary if stored in header
|
||||
mDatabaseKDBX?.let {
|
||||
return it.compressionAlgorithm == CompressionAlgorithm.GZip
|
||||
&& it.kdbxVersion.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun updateDataBinaryCompression(oldCompression: CompressionAlgorithm,
|
||||
newCompression: CompressionAlgorithm) {
|
||||
mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression)
|
||||
@@ -268,14 +278,14 @@ class Database {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if RecycleBin is available or not for this version of database
|
||||
* @return true if RecycleBin available
|
||||
* Determine if a configurable RecycleBin is available or not for this version of database
|
||||
* @return true if a configurable RecycleBin available
|
||||
*/
|
||||
val allowRecycleBin: Boolean
|
||||
val allowConfigurableRecycleBin: Boolean
|
||||
get() = mDatabaseKDBX != null
|
||||
|
||||
var isRecycleBinEnabled: Boolean
|
||||
// TODO #394 isRecycleBinEnabled mDatabaseKDB
|
||||
// Backup is always enabled in KDB database
|
||||
get() = mDatabaseKDB != null || mDatabaseKDBX?.isRecycleBinEnabled ?: false
|
||||
set(value) {
|
||||
mDatabaseKDBX?.isRecycleBinEnabled = value
|
||||
@@ -293,12 +303,12 @@ class Database {
|
||||
}
|
||||
|
||||
fun ensureRecycleBinExists(resources: Resources) {
|
||||
mDatabaseKDB?.ensureRecycleBinExists()
|
||||
mDatabaseKDB?.ensureBackupExists()
|
||||
mDatabaseKDBX?.ensureRecycleBinExists(resources)
|
||||
}
|
||||
|
||||
fun removeRecycleBin() {
|
||||
// TODO #394 delete backup mDatabaseKDB?.removeRecycleBin()
|
||||
// Don't allow remove backup in KDB
|
||||
mDatabaseKDBX?.removeRecycleBin()
|
||||
}
|
||||
|
||||
@@ -421,6 +431,7 @@ class Database {
|
||||
searchInPasswords = false
|
||||
searchInUrls = true
|
||||
searchInNotes = true
|
||||
searchInOTP = false
|
||||
searchInOther = true
|
||||
searchInUUIDs = false
|
||||
searchInTags = false
|
||||
@@ -428,6 +439,38 @@ class Database {
|
||||
}, omitBackup, max)
|
||||
}
|
||||
|
||||
val binaryPool: BinaryPool
|
||||
get() {
|
||||
return mDatabaseKDBX?.binaryPool ?: BinaryPool()
|
||||
}
|
||||
|
||||
val allowMultipleAttachments: Boolean
|
||||
get() {
|
||||
if (mDatabaseKDB != null)
|
||||
return false
|
||||
if (mDatabaseKDBX != null)
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
fun buildNewBinary(cacheDirectory: File,
|
||||
compressed: Boolean = false,
|
||||
protected: Boolean = false): BinaryAttachment? {
|
||||
return mDatabaseKDB?.buildNewBinary(cacheDirectory)
|
||||
?: mDatabaseKDBX?.buildNewBinary(cacheDirectory, compressed, protected)
|
||||
}
|
||||
|
||||
fun removeAttachmentIfNotUsed(attachment: Attachment) {
|
||||
// No need in KDB database because unique attachment by entry
|
||||
// Don't clear to fix upload multiple times
|
||||
mDatabaseKDBX?.removeUnlinkedAttachment(attachment.binaryAttachment, false)
|
||||
}
|
||||
|
||||
fun removeUnlinkedAttachments() {
|
||||
// No check in database KDB because unique attachment by entry
|
||||
mDatabaseKDBX?.removeUnlinkedAttachments(true)
|
||||
}
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
fun saveData(contentResolver: ContentResolver) {
|
||||
try {
|
||||
@@ -473,7 +516,7 @@ class Database {
|
||||
} else {
|
||||
var outputStream: OutputStream? = null
|
||||
try {
|
||||
outputStream = contentResolver.openOutputStream(uri)
|
||||
outputStream = contentResolver.openOutputStream(uri, "rwt")
|
||||
outputStream?.let { definedOutputStream ->
|
||||
val databaseOutput = mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
|
||||
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, definedOutputStream) }
|
||||
@@ -718,7 +761,7 @@ class Database {
|
||||
fun canRecycle(entry: Entry): Boolean {
|
||||
var canRecycle: Boolean? = null
|
||||
entry.entryKDB?.let {
|
||||
canRecycle = mDatabaseKDB?.canRecycle()
|
||||
canRecycle = mDatabaseKDB?.canRecycle(it)
|
||||
}
|
||||
entry.entryKDBX?.let {
|
||||
canRecycle = mDatabaseKDBX?.canRecycle(it)
|
||||
@@ -729,7 +772,7 @@ class Database {
|
||||
fun canRecycle(group: Group): Boolean {
|
||||
var canRecycle: Boolean? = null
|
||||
group.groupKDB?.let {
|
||||
canRecycle = mDatabaseKDB?.canRecycle()
|
||||
canRecycle = mDatabaseKDB?.canRecycle(it)
|
||||
}
|
||||
group.groupKDBX?.let {
|
||||
canRecycle = mDatabaseKDBX?.canRecycle(it)
|
||||
@@ -781,18 +824,25 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
fun startManageEntry(entry: Entry) {
|
||||
fun startManageEntry(entry: Entry?) {
|
||||
mDatabaseKDBX?.let {
|
||||
entry.startToManageFieldReferences(it)
|
||||
entry?.startToManageFieldReferences(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun stopManageEntry(entry: Entry) {
|
||||
fun stopManageEntry(entry: Entry?) {
|
||||
mDatabaseKDBX?.let {
|
||||
entry.stopToManageFieldReferences()
|
||||
entry?.stopToManageFieldReferences()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if database allows custom field
|
||||
*/
|
||||
fun allowEntryCustomFields(): Boolean {
|
||||
return mDatabaseKDBX != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove oldest history for each entry if more than max items or max memory
|
||||
*/
|
||||
@@ -800,7 +850,7 @@ class Database {
|
||||
rootGroup?.doForEachChildAndForIt(
|
||||
object : NodeHandler<Entry>() {
|
||||
override fun operate(node: Entry): Boolean {
|
||||
removeOldestEntryHistory(node)
|
||||
removeOldestEntryHistory(node, binaryPool)
|
||||
return true
|
||||
}
|
||||
},
|
||||
@@ -808,34 +858,19 @@ class Database {
|
||||
override fun operate(node: Group): Boolean {
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun removeEachEntryHistory() {
|
||||
rootGroup?.doForEachChildAndForIt(
|
||||
object : NodeHandler<Entry>() {
|
||||
override fun operate(node: Entry): Boolean {
|
||||
node.removeAllHistory()
|
||||
return true
|
||||
}
|
||||
},
|
||||
object : NodeHandler<Group>() {
|
||||
override fun operate(node: Group): Boolean {
|
||||
return true
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove oldest history if more than max items or max memory
|
||||
*/
|
||||
fun removeOldestEntryHistory(entry: Entry) {
|
||||
fun removeOldestEntryHistory(entry: Entry, binaryPool: BinaryPool) {
|
||||
mDatabaseKDBX?.let {
|
||||
|
||||
val maxItems = historyMaxItems
|
||||
if (maxItems >= 0) {
|
||||
while (entry.getHistory().size > maxItems) {
|
||||
entry.removeOldestEntryFromHistory()
|
||||
removeOldestEntryHistory(entry)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -844,11 +879,10 @@ class Database {
|
||||
while (true) {
|
||||
var historySize: Long = 0
|
||||
for (entryHistory in entry.getHistory()) {
|
||||
historySize += entryHistory.getSize()
|
||||
historySize += entryHistory.getSize(binaryPool)
|
||||
}
|
||||
|
||||
if (historySize > maxSize) {
|
||||
entry.removeOldestEntryFromHistory()
|
||||
removeOldestEntryHistory(entry)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
@@ -857,6 +891,22 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeOldestEntryHistory(entry: Entry) {
|
||||
entry.removeOldestEntryFromHistory()?.let {
|
||||
it.getAttachments(binaryPool, false).forEach { attachmentToRemove ->
|
||||
removeAttachmentIfNotUsed(attachmentToRemove)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeEntryHistory(entry: Entry, entryHistoryPosition: Int) {
|
||||
entry.removeEntryFromHistory(entryHistoryPosition)?.let {
|
||||
it.getAttachments(binaryPool, false).forEach { attachmentToRemove ->
|
||||
removeAttachmentIfNotUsed(attachmentToRemove)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object : SingletonHolder<Database>(::Database) {
|
||||
|
||||
private val TAG = Database::class.java.name
|
||||
|
||||
@@ -23,6 +23,8 @@ import android.content.res.Resources
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import org.joda.time.Duration
|
||||
import org.joda.time.Instant
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@@ -95,6 +97,7 @@ class DateInstant : Parcelable {
|
||||
companion object {
|
||||
|
||||
val NEVER_EXPIRE = neverExpire
|
||||
val IN_ONE_MONTH = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate())
|
||||
private val dateFormat = SimpleDateFormat.getDateTimeInstance()
|
||||
|
||||
private val neverExpire: DateInstant
|
||||
|
||||
@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.database.element
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.database.BinaryPool
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
@@ -32,8 +33,6 @@ import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.model.EntryAttachment
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.Field
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
@@ -284,73 +283,88 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve custom fields to show, key is the label, value is the value of field (protected or not)
|
||||
* Retrieve extra fields to show, key is the label, value is the value of field (protected or not)
|
||||
* @return Map of label/value
|
||||
*/
|
||||
val customFields: HashMap<String, ProtectedString>
|
||||
get() = entryKDBX?.customFields ?: HashMap()
|
||||
|
||||
/**
|
||||
* To redefine if version of entry allow custom field,
|
||||
* @return true if entry allows custom field
|
||||
*/
|
||||
fun allowCustomFields(): Boolean {
|
||||
return entryKDBX?.allowCustomFields() ?: false
|
||||
fun getExtraFields(): List<Field> {
|
||||
val extraFields = ArrayList<Field>()
|
||||
entryKDBX?.let {
|
||||
for (field in it.customFields) {
|
||||
extraFields.add(Field(field.key, field.value))
|
||||
}
|
||||
|
||||
fun removeAllFields() {
|
||||
entryKDBX?.removeAllFields()
|
||||
}
|
||||
return extraFields
|
||||
}
|
||||
|
||||
/**
|
||||
* Update or add an extra field to the list (standard or custom)
|
||||
* @param label Label of field, must be unique
|
||||
* @param value Value of field
|
||||
*/
|
||||
fun putExtraField(label: String, value: ProtectedString) {
|
||||
entryKDBX?.putExtraField(label, value)
|
||||
fun putExtraField(field: Field) {
|
||||
entryKDBX?.putExtraField(field.name, field.protectedValue)
|
||||
}
|
||||
|
||||
private fun addExtraFields(fields: List<Field>) {
|
||||
fields.forEach {
|
||||
putExtraField(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeAllFields() {
|
||||
entryKDBX?.removeAllFields()
|
||||
}
|
||||
|
||||
fun getOtpElement(): OtpElement? {
|
||||
entryKDBX?.let {
|
||||
return OtpEntryFields.parseFields { key ->
|
||||
customFields[key]?.toString()
|
||||
it.customFields[key]?.toString()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun startToManageFieldReferences(db: DatabaseKDBX) {
|
||||
entryKDBX?.startToManageFieldReferences(db)
|
||||
fun startToManageFieldReferences(database: DatabaseKDBX) {
|
||||
entryKDBX?.startToManageFieldReferences(database)
|
||||
}
|
||||
|
||||
fun stopToManageFieldReferences() {
|
||||
entryKDBX?.stopToManageFieldReferences()
|
||||
}
|
||||
|
||||
fun getAttachments(): ArrayList<EntryAttachment> {
|
||||
val attachments = ArrayList<EntryAttachment>()
|
||||
|
||||
entryKDB?.binaryData?.let { binaryKDB ->
|
||||
attachments.add(EntryAttachment(entryKDB?.binaryDescription ?: "", binaryKDB))
|
||||
fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List<Attachment> {
|
||||
val attachments = ArrayList<Attachment>()
|
||||
entryKDB?.getAttachment()?.let {
|
||||
attachments.add(it)
|
||||
}
|
||||
|
||||
entryKDBX?.binaries?.let { binariesKDBX ->
|
||||
for ((key, value) in binariesKDBX) {
|
||||
attachments.add(EntryAttachment(key, value))
|
||||
entryKDBX?.getAttachments(binaryPool, inHistory)?.let {
|
||||
attachments.addAll(it)
|
||||
}
|
||||
}
|
||||
|
||||
return attachments
|
||||
}
|
||||
|
||||
fun removeAttachment(attachment: EntryAttachment) {
|
||||
entryKDB?.apply {
|
||||
if (binaryDescription == attachment.name
|
||||
&& binaryData == attachment.binaryAttachment) {
|
||||
binaryDescription = ""
|
||||
binaryData = null
|
||||
fun containsAttachment(): Boolean {
|
||||
return entryKDB?.containsAttachment() == true
|
||||
|| entryKDBX?.containsAttachment() == true
|
||||
}
|
||||
|
||||
private fun addAttachments(binaryPool: BinaryPool, attachments: List<Attachment>) {
|
||||
attachments.forEach {
|
||||
putAttachment(it, binaryPool)
|
||||
}
|
||||
}
|
||||
|
||||
entryKDBX?.removeProtectedBinary(attachment.name)
|
||||
private fun removeAttachment(attachment: Attachment) {
|
||||
entryKDB?.removeAttachment(attachment)
|
||||
entryKDBX?.removeAttachment(attachment)
|
||||
}
|
||||
|
||||
private fun removeAllAttachments() {
|
||||
entryKDB?.removeAttachment()
|
||||
entryKDBX?.removeAttachments()
|
||||
}
|
||||
|
||||
private fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) {
|
||||
entryKDB?.putAttachment(attachment)
|
||||
entryKDBX?.putAttachment(attachment, binaryPool)
|
||||
}
|
||||
|
||||
fun getHistory(): ArrayList<Entry> {
|
||||
@@ -368,20 +382,22 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
}
|
||||
}
|
||||
|
||||
fun removeEntryFromHistory(position: Int) {
|
||||
entryKDBX?.removeEntryFromHistory(position)
|
||||
fun removeEntryFromHistory(position: Int): Entry? {
|
||||
entryKDBX?.removeEntryFromHistory(position)?.let {
|
||||
return Entry(it)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun removeAllHistory() {
|
||||
entryKDBX?.removeAllHistory()
|
||||
fun removeOldestEntryFromHistory(): Entry? {
|
||||
entryKDBX?.removeOldestEntryFromHistory()?.let {
|
||||
return Entry(it)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun removeOldestEntryFromHistory() {
|
||||
entryKDBX?.removeOldestEntryFromHistory()
|
||||
}
|
||||
|
||||
fun getSize(): Long {
|
||||
return entryKDBX?.size ?: 0L
|
||||
fun getSize(binaryPool: BinaryPool): Long {
|
||||
return entryKDBX?.getSize(binaryPool) ?: 0L
|
||||
}
|
||||
|
||||
fun containsCustomData(): Boolean {
|
||||
@@ -395,8 +411,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
*/
|
||||
|
||||
/**
|
||||
* Retrieve generated entry info,
|
||||
* Remove parameter fields and add auto generated elements in auto custom fields
|
||||
* Retrieve generated entry info.
|
||||
* 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 {
|
||||
val entryInfo = EntryInfo()
|
||||
@@ -404,26 +420,57 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
database?.stopManageEntry(this)
|
||||
else
|
||||
database?.startManageEntry(this)
|
||||
|
||||
entryInfo.id = nodeId.toString()
|
||||
entryInfo.title = title
|
||||
entryInfo.icon = icon
|
||||
entryInfo.username = username
|
||||
entryInfo.password = password
|
||||
entryInfo.expires = expires
|
||||
entryInfo.expiryTime = expiryTime
|
||||
entryInfo.url = url
|
||||
entryInfo.notes = notes
|
||||
for (entry in customFields.entries) {
|
||||
entryInfo.customFields.add(
|
||||
Field(entry.key, entry.value))
|
||||
}
|
||||
entryInfo.customFields = getExtraFields()
|
||||
// Add otpElement to generate token
|
||||
entryInfo.otpModel = getOtpElement()?.otpModel
|
||||
if (!raw) {
|
||||
// Replace parameter fields by generated OTP fields
|
||||
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
|
||||
}
|
||||
database?.binaryPool?.let { binaryPool ->
|
||||
entryInfo.attachments = getAttachments(binaryPool)
|
||||
}
|
||||
|
||||
if (!raw)
|
||||
database?.stopManageEntry(this)
|
||||
return entryInfo
|
||||
}
|
||||
|
||||
fun setEntryInfo(database: Database?, newEntryInfo: EntryInfo) {
|
||||
database?.startManageEntry(this)
|
||||
|
||||
removeAllFields()
|
||||
removeAllAttachments()
|
||||
// NodeId stay as is
|
||||
title = newEntryInfo.title
|
||||
icon = newEntryInfo.icon
|
||||
username = newEntryInfo.username
|
||||
password = newEntryInfo.password
|
||||
expires = newEntryInfo.expires
|
||||
expiryTime = newEntryInfo.expiryTime
|
||||
url = newEntryInfo.url
|
||||
notes = newEntryInfo.notes
|
||||
addExtraFields(newEntryInfo.customFields)
|
||||
database?.binaryPool?.let { binaryPool ->
|
||||
addAttachments(binaryPool, newEntryInfo.attachments)
|
||||
}
|
||||
// Update date time
|
||||
lastAccessTime = DateInstant()
|
||||
lastModificationTime = DateInstant()
|
||||
|
||||
database?.stopManageEntry(this)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
@@ -453,5 +500,12 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
}
|
||||
|
||||
const val PMS_TAN_ENTRY = "<TAN>"
|
||||
|
||||
/**
|
||||
* True if [field] name is not a standard field name
|
||||
*/
|
||||
fun newExtraFieldNameAllowed(field: Field): Boolean {
|
||||
return EntryKDBX.newCustomNameAllowed(field.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,8 @@
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element.security
|
||||
package com.kunzisoft.keepass.database.element.database
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.stream.readBytes
|
||||
@@ -30,68 +28,83 @@ import java.util.zip.GZIPOutputStream
|
||||
|
||||
class BinaryAttachment : Parcelable {
|
||||
|
||||
var isCompressed: Boolean? = null
|
||||
private var dataFile: File? = null
|
||||
var isCompressed: Boolean = false
|
||||
private set
|
||||
var isProtected: Boolean = false
|
||||
private set
|
||||
private var dataFile: File? = null
|
||||
var isCorrupted: Boolean = false
|
||||
|
||||
fun length(): Long {
|
||||
if (dataFile != null)
|
||||
return dataFile!!.length()
|
||||
return 0
|
||||
return dataFile?.length() ?: 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty protected binary
|
||||
*/
|
||||
constructor() {
|
||||
this.isCompressed = null
|
||||
this.isProtected = false
|
||||
this.dataFile = null
|
||||
}
|
||||
constructor()
|
||||
|
||||
constructor(dataFile: File, enableProtection: Boolean = false, compressed: Boolean? = null) {
|
||||
this.isCompressed = compressed
|
||||
this.isProtected = enableProtection
|
||||
constructor(dataFile: File, compressed: Boolean = false, protected: Boolean = false) {
|
||||
this.dataFile = dataFile
|
||||
this.isCompressed = compressed
|
||||
this.isProtected = protected
|
||||
}
|
||||
|
||||
private constructor(parcel: Parcel) {
|
||||
val compressedByte = parcel.readByte().toInt()
|
||||
isCompressed = if (compressedByte == 2) null else compressedByte != 0
|
||||
isProtected = parcel.readByte().toInt() != 0
|
||||
parcel.readString()?.let {
|
||||
dataFile = File(it)
|
||||
}
|
||||
isCompressed = parcel.readByte().toInt() != 0
|
||||
isProtected = parcel.readByte().toInt() != 0
|
||||
isCorrupted = parcel.readByte().toInt() != 0
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getInputDataStream(): InputStream {
|
||||
return when {
|
||||
dataFile != null -> FileInputStream(dataFile!!)
|
||||
length() > 0 -> FileInputStream(dataFile!!)
|
||||
else -> ByteArrayInputStream(ByteArray(0))
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getUnGzipInputDataStream(): InputStream {
|
||||
return if (isCompressed)
|
||||
GZIPInputStream(getInputDataStream())
|
||||
else
|
||||
getInputDataStream()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getOutputDataStream(): OutputStream {
|
||||
return when {
|
||||
dataFile != null -> FileOutputStream(dataFile!!)
|
||||
else -> throw IOException("Unable to write in an unknown file")
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getGzipOutputDataStream(): OutputStream {
|
||||
return if (isCompressed) {
|
||||
GZIPOutputStream(getOutputDataStream())
|
||||
} else {
|
||||
getOutputDataStream()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
||||
dataFile?.let { concreteDataFile ->
|
||||
// To compress, create a new binary with file
|
||||
if (isCompressed != true) {
|
||||
if (!isCompressed) {
|
||||
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
||||
var outputStream: GZIPOutputStream? = null
|
||||
var inputStream: InputStream? = null
|
||||
try {
|
||||
outputStream = GZIPOutputStream(FileOutputStream(fileBinaryCompress))
|
||||
inputStream = getInputDataStream()
|
||||
GZIPOutputStream(FileOutputStream(fileBinaryCompress)).use { outputStream ->
|
||||
getInputDataStream().use { inputStream ->
|
||||
inputStream.readBytes(bufferSize) { buffer ->
|
||||
outputStream.write(buffer)
|
||||
}
|
||||
} finally {
|
||||
inputStream?.close()
|
||||
outputStream?.close()
|
||||
|
||||
}
|
||||
}
|
||||
// Remove unGzip file
|
||||
if (concreteDataFile.delete()) {
|
||||
if (fileBinaryCompress.renameTo(concreteDataFile)) {
|
||||
@@ -102,25 +115,19 @@ class BinaryAttachment : Parcelable {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
||||
dataFile?.let { concreteDataFile ->
|
||||
if (isCompressed != false) {
|
||||
if (isCompressed) {
|
||||
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
||||
var outputStream: FileOutputStream? = null
|
||||
var inputStream: GZIPInputStream? = null
|
||||
try {
|
||||
outputStream = FileOutputStream(fileBinaryDecompress)
|
||||
inputStream = GZIPInputStream(getInputDataStream())
|
||||
FileOutputStream(fileBinaryDecompress).use { outputStream ->
|
||||
getUnGzipInputDataStream().use { inputStream ->
|
||||
inputStream.readBytes(bufferSize) { buffer ->
|
||||
outputStream.write(buffer)
|
||||
}
|
||||
} finally {
|
||||
inputStream?.close()
|
||||
outputStream?.close()
|
||||
|
||||
}
|
||||
}
|
||||
// Remove gzip file
|
||||
if (concreteDataFile.delete()) {
|
||||
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
|
||||
@@ -131,33 +138,6 @@ class BinaryAttachment : Parcelable {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun download(createdFileUri: Uri,
|
||||
contentResolver: ContentResolver,
|
||||
bufferSize: Int = DEFAULT_BUFFER_SIZE,
|
||||
update: ((percent: Int)->Unit)? = null) {
|
||||
|
||||
var dataDownloaded = 0
|
||||
contentResolver.openOutputStream(createdFileUri).use { outputStream ->
|
||||
outputStream?.let { fileOutputStream ->
|
||||
if (isCompressed == true) {
|
||||
GZIPInputStream(getInputDataStream())
|
||||
} else {
|
||||
getInputDataStream()
|
||||
}.use { inputStream ->
|
||||
inputStream.readBytes(bufferSize) { buffer ->
|
||||
fileOutputStream.write(buffer)
|
||||
dataDownloaded += buffer.size
|
||||
try {
|
||||
val percentDownload = (100 * dataDownloaded / length()).toInt()
|
||||
update?.invoke(percentDownload)
|
||||
} catch (e: Exception) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun clear() {
|
||||
@@ -179,26 +159,33 @@ class BinaryAttachment : Parcelable {
|
||||
|
||||
return isCompressed == other.isCompressed
|
||||
&& isProtected == other.isProtected
|
||||
&& isCorrupted == other.isCorrupted
|
||||
&& sameData
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
|
||||
var result = 0
|
||||
result = 31 * result + if (isCompressed == null) 2 else if (isCompressed!!) 1 else 0
|
||||
result = 31 * result + if (isCompressed) 1 else 0
|
||||
result = 31 * result + if (isProtected) 1 else 0
|
||||
result = 31 * result + if (isCorrupted) 1 else 0
|
||||
result = 31 * result + dataFile!!.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return dataFile.toString()
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
dest.writeByte((if (isCompressed == null) 2 else if (isCompressed!!) 1 else 0).toByte())
|
||||
dest.writeByte((if (isProtected) 1 else 0).toByte())
|
||||
dest.writeString(dataFile?.absolutePath)
|
||||
dest.writeByte((if (isCompressed) 1 else 0).toByte())
|
||||
dest.writeByte((if (isProtected) 1 else 0).toByte())
|
||||
dest.writeByte((if (isCorrupted) 1 else 0).toByte())
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -19,52 +19,137 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element.database
|
||||
|
||||
import android.util.SparseArray
|
||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
||||
import java.io.IOException
|
||||
|
||||
class BinaryPool {
|
||||
private val pool = SparseArray<BinaryAttachment>()
|
||||
private val pool = LinkedHashMap<Int, BinaryAttachment>()
|
||||
|
||||
/**
|
||||
* To get a binary by the pool key (ref attribute in entry)
|
||||
*/
|
||||
operator fun get(key: Int): BinaryAttachment? {
|
||||
return pool[key]
|
||||
}
|
||||
|
||||
fun put(key: Int, value: BinaryAttachment) {
|
||||
pool.put(key, value)
|
||||
/**
|
||||
* To linked a binary with a pool key, if the pool key doesn't exists, create an unused one
|
||||
*/
|
||||
fun put(key: Int?, value: BinaryAttachment) {
|
||||
if (key == null)
|
||||
put(value)
|
||||
else
|
||||
pool[key] = value
|
||||
}
|
||||
|
||||
fun doForEachBinary(action: (key: Int, binary: BinaryAttachment) -> Unit) {
|
||||
for (i in 0 until pool.size()) {
|
||||
action.invoke(i, pool.get(pool.keyAt(i)))
|
||||
/**
|
||||
* To put a [binaryAttachment] in the pool,
|
||||
* if already exists, replace the current one,
|
||||
* else add it with a new key
|
||||
*/
|
||||
fun put(binaryAttachment: BinaryAttachment): Int {
|
||||
var key = findKey(binaryAttachment)
|
||||
if (key == null) {
|
||||
key = findUnusedKey()
|
||||
}
|
||||
pool[key] = binaryAttachment
|
||||
return key
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a binary from the pool, the file is not deleted
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun clear() {
|
||||
doForEachBinary { _, binary ->
|
||||
binary.clear()
|
||||
fun remove(binaryAttachment: BinaryAttachment) {
|
||||
findKey(binaryAttachment)?.let {
|
||||
pool.remove(it)
|
||||
}
|
||||
pool.clear()
|
||||
// Don't clear attachment here because a file can be used in many BinaryAttachment
|
||||
}
|
||||
|
||||
fun add(fileBinary: BinaryAttachment) {
|
||||
if (findKey(fileBinary) == null) {
|
||||
pool.put(findUnusedKey(), fileBinary)
|
||||
}
|
||||
}
|
||||
|
||||
fun findUnusedKey(): Int {
|
||||
var unusedKey = pool.size()
|
||||
while (get(unusedKey) != null)
|
||||
/**
|
||||
* Utility method to find an unused key in the pool
|
||||
*/
|
||||
private fun findUnusedKey(): Int {
|
||||
var unusedKey = 0
|
||||
while (pool[unusedKey] != null)
|
||||
unusedKey++
|
||||
return unusedKey
|
||||
}
|
||||
|
||||
fun findKey(pb: BinaryAttachment): Int? {
|
||||
for (i in 0 until pool.size()) {
|
||||
if (pool.get(pool.keyAt(i)) == pb) return i
|
||||
/**
|
||||
* Return key of [binaryAttachmentToRetrieve] or null if not found
|
||||
*/
|
||||
private fun findKey(binaryAttachmentToRetrieve: BinaryAttachment): Int? {
|
||||
val contains = pool.containsValue(binaryAttachmentToRetrieve)
|
||||
return if (!contains)
|
||||
null
|
||||
else {
|
||||
for ((key, binary) in pool) {
|
||||
if (binary == binaryAttachmentToRetrieve) {
|
||||
return key
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to order binaries and solve index problem in database v4
|
||||
*/
|
||||
private fun orderedBinaries(): List<KeyBinary> {
|
||||
val keyBinaryList = ArrayList<KeyBinary>()
|
||||
for ((key, binary) in pool) {
|
||||
keyBinaryList.add(KeyBinary(key, binary))
|
||||
}
|
||||
return keyBinaryList
|
||||
}
|
||||
|
||||
/**
|
||||
* To register a binary with a ref corresponding to an ordered index
|
||||
*/
|
||||
fun getBinaryIndexFromKey(key: Int): Int? {
|
||||
val index = orderedBinaries().indexOfFirst { it.key == key }
|
||||
return if (index < 0)
|
||||
null
|
||||
else
|
||||
index
|
||||
}
|
||||
|
||||
/**
|
||||
* Different from doForEach, provide an ordered index to each binary
|
||||
*/
|
||||
fun doForEachOrderedBinary(action: (index: Int, keyBinary: KeyBinary) -> Unit) {
|
||||
orderedBinaries().forEachIndexed(action)
|
||||
}
|
||||
|
||||
/**
|
||||
* To do an action on each binary in the pool
|
||||
*/
|
||||
fun doForEachBinary(action: (binary: BinaryAttachment) -> Unit) {
|
||||
pool.values.forEach { action.invoke(it) }
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun clear() {
|
||||
doForEachBinary {
|
||||
it.clear()
|
||||
}
|
||||
pool.clear()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val stringBuffer = StringBuffer()
|
||||
for ((key, value) in pool) {
|
||||
if (stringBuffer.isNotEmpty())
|
||||
stringBuffer.append(", {$key:$value}")
|
||||
else
|
||||
stringBuffer.append("{$key:$value}")
|
||||
}
|
||||
return stringBuffer.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility data class to order binaries
|
||||
*/
|
||||
data class KeyBinary(val key: Int, val binary: BinaryAttachment)
|
||||
}
|
||||
|
||||
@@ -26,8 +26,10 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.stream.NullOutputStream
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.security.DigestOutputStream
|
||||
@@ -38,10 +40,12 @@ import kotlin.collections.ArrayList
|
||||
|
||||
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
|
||||
var backupGroupId: Int = BACKUP_FOLDER_UNDEFINED_ID
|
||||
private var backupGroupId: Int = BACKUP_FOLDER_UNDEFINED_ID
|
||||
|
||||
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
|
||||
|
||||
private var binaryIncrement = 0
|
||||
|
||||
override val version: String
|
||||
get() = "KeePass 1"
|
||||
|
||||
@@ -57,7 +61,12 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
|
||||
// Retrieve backup group in index
|
||||
val backupGroup: GroupKDB?
|
||||
get() = if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID) null else getGroupById(backupGroupId)
|
||||
get() {
|
||||
return if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID)
|
||||
null
|
||||
else
|
||||
getGroupById(backupGroupId)
|
||||
}
|
||||
|
||||
override val kdfEngine: KdfEngine?
|
||||
get() = kdfListV3[0]
|
||||
@@ -177,6 +186,13 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
override fun isInRecycleBin(group: GroupKDB): Boolean {
|
||||
var currentGroup: GroupKDB? = group
|
||||
|
||||
// Init backup group variable
|
||||
if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID)
|
||||
findBackupGroupId()
|
||||
|
||||
if (backupGroup == null)
|
||||
return false
|
||||
|
||||
if (currentGroup == backupGroup)
|
||||
return true
|
||||
|
||||
@@ -191,17 +207,21 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the recycle bin tree exists, if enabled and create it
|
||||
* if it doesn't exist
|
||||
*/
|
||||
fun ensureRecycleBinExists() {
|
||||
private fun findBackupGroupId() {
|
||||
rootGroups.forEach { currentGroup ->
|
||||
if (currentGroup.level == 0
|
||||
&& currentGroup.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)) {
|
||||
backupGroupId = currentGroup.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the backup tree exists if enabled, and create it
|
||||
* if it doesn't exist
|
||||
*/
|
||||
fun ensureBackupExists() {
|
||||
findBackupGroupId()
|
||||
|
||||
if (backupGroup == null) {
|
||||
// Create recycle bin
|
||||
@@ -219,21 +239,25 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
* @param node Node to remove
|
||||
* @return true if node can be recycle, false elsewhere
|
||||
*/
|
||||
// TODO #394 Backup KDB
|
||||
// fun canRecycle(node: NodeVersioned<*, GroupKDB, EntryKDB>): Boolean {
|
||||
fun canRecycle(): Boolean {
|
||||
fun canRecycle(node: NodeVersioned<*, GroupKDB, EntryKDB>): Boolean {
|
||||
if (backupGroup == null)
|
||||
ensureBackupExists()
|
||||
if (node == backupGroup)
|
||||
return false
|
||||
backupGroup?.let {
|
||||
if (node.isContainedIn(it))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun recycle(group: GroupKDB) {
|
||||
ensureRecycleBinExists()
|
||||
removeGroupFrom(group, group.parent)
|
||||
addGroupTo(group, backupGroup)
|
||||
group.afterAssignNewParent()
|
||||
}
|
||||
|
||||
fun recycle(entry: EntryKDB) {
|
||||
ensureRecycleBinExists()
|
||||
removeEntryFrom(entry, entry.parent)
|
||||
addEntryTo(entry, backupGroup)
|
||||
entry.afterAssignNewParent()
|
||||
@@ -249,6 +273,13 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
addEntryTo(entry, origParent)
|
||||
}
|
||||
|
||||
fun buildNewBinary(cacheDirectory: File): BinaryAttachment {
|
||||
// Generate an unique new file
|
||||
val fileInCache = File(cacheDirectory, binaryIncrement.toString())
|
||||
binaryIncrement++
|
||||
return BinaryAttachment(fileInCache)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TYPE = DatabaseKDB::class.java
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
|
||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.DeletedObject
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
||||
@@ -46,6 +47,7 @@ import com.kunzisoft.keepass.utils.UnsignedInt
|
||||
import com.kunzisoft.keepass.utils.VariantDictionary
|
||||
import org.w3c.dom.Node
|
||||
import org.w3c.dom.Text
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
@@ -105,6 +107,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
val customData = HashMap<String, String>()
|
||||
|
||||
var binaryPool = BinaryPool()
|
||||
private var binaryIncrement = 0 // Unique id (don't use current time because CPU too fast)
|
||||
|
||||
var localizedAppName = "KeePassDX"
|
||||
|
||||
@@ -173,33 +176,51 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
|
||||
fun changeBinaryCompression(oldCompression: CompressionAlgorithm,
|
||||
newCompression: CompressionAlgorithm) {
|
||||
binaryPool.doForEachBinary { key, binary ->
|
||||
|
||||
try {
|
||||
when (oldCompression) {
|
||||
CompressionAlgorithm.None -> {
|
||||
when (newCompression) {
|
||||
CompressionAlgorithm.None -> {
|
||||
}
|
||||
CompressionAlgorithm.None -> {}
|
||||
CompressionAlgorithm.GZip -> {
|
||||
// To compress, create a new binary with file
|
||||
binary.compress(BUFFER_SIZE_BYTES)
|
||||
// Only in databaseV3.1, in databaseV4 the header is zipped during the save
|
||||
if (kdbxVersion.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong()) {
|
||||
compressAllBinaries()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CompressionAlgorithm.GZip -> {
|
||||
// In databaseV4 the header is zipped during the save, so not necessary here
|
||||
if (kdbxVersion.toKotlinLong() >= FILE_VERSION_32_4.toKotlinLong()) {
|
||||
decompressAllBinaries()
|
||||
} else {
|
||||
when (newCompression) {
|
||||
CompressionAlgorithm.None -> {
|
||||
// To decompress, create a new binary with file
|
||||
binary.decompress(BUFFER_SIZE_BYTES)
|
||||
decompressAllBinaries()
|
||||
}
|
||||
CompressionAlgorithm.GZip -> {
|
||||
CompressionAlgorithm.GZip -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun compressAllBinaries() {
|
||||
binaryPool.doForEachBinary { binary ->
|
||||
try {
|
||||
// To compress, create a new binary with file
|
||||
binary.compress(BUFFER_SIZE_BYTES)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to change compression for $key")
|
||||
Log.e(TAG, "Unable to compress $binary", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun decompressAllBinaries() {
|
||||
binaryPool.doForEachBinary { binary ->
|
||||
try {
|
||||
binary.decompress(BUFFER_SIZE_BYTES)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to decompress $binary", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -536,6 +557,60 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
return publicCustomData.size() > 0
|
||||
}
|
||||
|
||||
fun buildNewBinary(cacheDirectory: File,
|
||||
compression: Boolean,
|
||||
protection: Boolean,
|
||||
binaryPoolId: Int? = null): BinaryAttachment {
|
||||
// New file with current time
|
||||
val fileInCache = File(cacheDirectory, binaryIncrement.toString())
|
||||
binaryIncrement++
|
||||
val binaryAttachment = BinaryAttachment(fileInCache, compression, protection)
|
||||
// add attachment to pool
|
||||
binaryPool.put(binaryPoolId, binaryAttachment)
|
||||
return binaryAttachment
|
||||
}
|
||||
|
||||
fun removeUnlinkedAttachment(binary: BinaryAttachment, clear: Boolean) {
|
||||
val listBinaries = ArrayList<BinaryAttachment>()
|
||||
listBinaries.add(binary)
|
||||
removeUnlinkedAttachments(listBinaries, clear)
|
||||
}
|
||||
|
||||
fun removeUnlinkedAttachments(clear: Boolean) {
|
||||
removeUnlinkedAttachments(emptyList(), clear)
|
||||
}
|
||||
|
||||
private fun removeUnlinkedAttachments(binaries: List<BinaryAttachment>, clear: Boolean) {
|
||||
// Build binaries to remove with all binaries known
|
||||
val binariesToRemove = ArrayList<BinaryAttachment>()
|
||||
if (binaries.isEmpty()) {
|
||||
binaryPool.doForEachBinary { binary ->
|
||||
binariesToRemove.add(binary)
|
||||
}
|
||||
} else {
|
||||
binariesToRemove.addAll(binaries)
|
||||
}
|
||||
// Remove binaries from the list
|
||||
rootGroup?.doForEachChild(object : NodeHandler<EntryKDBX>() {
|
||||
override fun operate(node: EntryKDBX): Boolean {
|
||||
node.getAttachments(binaryPool, true).forEach {
|
||||
binariesToRemove.remove(it.binaryAttachment)
|
||||
}
|
||||
return binariesToRemove.isNotEmpty()
|
||||
}
|
||||
}, null)
|
||||
// Effective removing
|
||||
binariesToRemove.forEach {
|
||||
try {
|
||||
binaryPool.remove(it)
|
||||
if (clear)
|
||||
it.clear()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Unable to clean binaries", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
||||
if (password == null)
|
||||
return true
|
||||
|
||||
@@ -27,7 +27,6 @@ import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.KeyFileEmptyDatabaseException
|
||||
import java.io.*
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
@@ -136,7 +135,6 @@ abstract class DatabaseVersioned<
|
||||
}
|
||||
|
||||
when (keyData.size.toLong()) {
|
||||
0L -> throw KeyFileEmptyDatabaseException()
|
||||
32L -> return keyData
|
||||
64L -> try {
|
||||
return hexStringToByteArray(String(keyData))
|
||||
|
||||
@@ -26,8 +26,10 @@ import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.NodeKDBInterface
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
||||
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
/**
|
||||
* Structure containing information about one entry.
|
||||
@@ -135,6 +137,29 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
||||
override val type: Type
|
||||
get() = Type.ENTRY
|
||||
|
||||
fun getAttachment(): Attachment? {
|
||||
val binary = binaryData
|
||||
return if (binary != null)
|
||||
Attachment(binaryDescription, binary)
|
||||
else null
|
||||
}
|
||||
|
||||
fun containsAttachment(): Boolean {
|
||||
return binaryData != null
|
||||
}
|
||||
|
||||
fun putAttachment(attachment: Attachment) {
|
||||
this.binaryDescription = attachment.name
|
||||
this.binaryData = attachment.binaryAttachment
|
||||
}
|
||||
|
||||
fun removeAttachment(attachment: Attachment? = null) {
|
||||
if (attachment == null || this.binaryDescription == attachment.name) {
|
||||
this.binaryDescription = ""
|
||||
this.binaryData = null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/** Size of byte buffer needed to hold this struct. */
|
||||
|
||||
@@ -21,7 +21,9 @@ package com.kunzisoft.keepass.database.element.entry
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.database.BinaryPool
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
@@ -31,11 +33,11 @@ import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.utils.ParcelableUtil
|
||||
import com.kunzisoft.keepass.utils.UnsignedLong
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.LinkedHashMap
|
||||
|
||||
class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
|
||||
@@ -59,9 +61,9 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
super.icon = value
|
||||
}
|
||||
var iconCustom = IconImageCustom.UNKNOWN_ICON
|
||||
private var customData = LinkedHashMap<String, String>()
|
||||
var customData = LinkedHashMap<String, String>()
|
||||
var fields = LinkedHashMap<String, ProtectedString>()
|
||||
var binaries = LinkedHashMap<String, BinaryAttachment>()
|
||||
var binaries = LinkedHashMap<String, Int>() // Map<Label, PoolId>
|
||||
var foregroundColor = ""
|
||||
var backgroundColor = ""
|
||||
var overrideURL = ""
|
||||
@@ -70,8 +72,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
var additional = ""
|
||||
var tags = ""
|
||||
|
||||
val size: Long
|
||||
get() {
|
||||
fun getSize(binaryPool: BinaryPool): Long {
|
||||
var size = FIXED_LENGTH_SIZE
|
||||
|
||||
for (entry in fields.entries) {
|
||||
@@ -79,10 +80,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
size += entry.value.length().toLong()
|
||||
}
|
||||
|
||||
for ((key, value) in binaries) {
|
||||
size += key.length.toLong()
|
||||
size += value.length()
|
||||
}
|
||||
size += getAttachmentsSize(binaryPool)
|
||||
|
||||
size += autoType.defaultSequence.length.toLong()
|
||||
for ((key, value) in autoType.entrySet()) {
|
||||
@@ -91,7 +89,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
}
|
||||
|
||||
for (entry in history) {
|
||||
size += entry.size
|
||||
size += entry.getSize(binaryPool)
|
||||
}
|
||||
|
||||
size += overrideURL.length.toLong()
|
||||
@@ -110,7 +108,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
|
||||
customData = ParcelableUtil.readStringParcelableMap(parcel)
|
||||
fields = ParcelableUtil.readStringParcelableMap(parcel, ProtectedString::class.java)
|
||||
binaries = ParcelableUtil.readStringParcelableMap(parcel, BinaryAttachment::class.java)
|
||||
binaries = ParcelableUtil.readStringIntMap(parcel)
|
||||
foregroundColor = parcel.readString() ?: foregroundColor
|
||||
backgroundColor = parcel.readString() ?: backgroundColor
|
||||
overrideURL = parcel.readString() ?: overrideURL
|
||||
@@ -128,7 +126,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
dest.writeParcelable(locationChanged, flags)
|
||||
ParcelableUtil.writeStringParcelableMap(dest, customData)
|
||||
ParcelableUtil.writeStringParcelableMap(dest, flags, fields)
|
||||
ParcelableUtil.writeStringParcelableMap(dest, flags, binaries)
|
||||
ParcelableUtil.writeStringIntMap(dest, binaries)
|
||||
dest.writeString(foregroundColor)
|
||||
dest.writeString(backgroundColor)
|
||||
dest.writeString(overrideURL)
|
||||
@@ -167,8 +165,8 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
tags = source.tags
|
||||
}
|
||||
|
||||
fun startToManageFieldReferences(db: DatabaseKDBX) {
|
||||
this.mDatabase = db
|
||||
fun startToManageFieldReferences(database: DatabaseKDBX) {
|
||||
this.mDatabase = database
|
||||
this.mDecodeRef = true
|
||||
}
|
||||
|
||||
@@ -272,10 +270,6 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
return field
|
||||
}
|
||||
|
||||
fun allowCustomFields(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
fun removeAllFields() {
|
||||
fields.clear()
|
||||
}
|
||||
@@ -284,16 +278,47 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
fields[label] = value
|
||||
}
|
||||
|
||||
fun putProtectedBinary(key: String, value: BinaryAttachment) {
|
||||
binaries[key] = value
|
||||
/**
|
||||
* It's a list because history labels can be defined multiple times
|
||||
*/
|
||||
fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List<Attachment> {
|
||||
val entryAttachmentList = ArrayList<Attachment>()
|
||||
for ((label, poolId) in binaries) {
|
||||
binaryPool[poolId]?.let { binary ->
|
||||
entryAttachmentList.add(Attachment(label, binary))
|
||||
}
|
||||
}
|
||||
if (inHistory) {
|
||||
history.forEach {
|
||||
entryAttachmentList.addAll(it.getAttachments(binaryPool, false))
|
||||
}
|
||||
}
|
||||
return entryAttachmentList
|
||||
}
|
||||
|
||||
fun removeProtectedBinary(name: String) {
|
||||
binaries.remove(name)
|
||||
fun containsAttachment(): Boolean {
|
||||
return binaries.isNotEmpty()
|
||||
}
|
||||
|
||||
fun sizeOfHistory(): Int {
|
||||
return history.size
|
||||
fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) {
|
||||
binaries[attachment.name] = binaryPool.put(attachment.binaryAttachment)
|
||||
}
|
||||
|
||||
fun removeAttachment(attachment: Attachment) {
|
||||
binaries.remove(attachment.name)
|
||||
}
|
||||
|
||||
fun removeAttachments() {
|
||||
binaries.clear()
|
||||
}
|
||||
|
||||
private fun getAttachmentsSize(binaryPool: BinaryPool): Long {
|
||||
var size = 0L
|
||||
for ((label, poolId) in binaries) {
|
||||
size += label.length.toLong()
|
||||
size += binaryPool[poolId]?.length() ?: 0
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
override fun putCustomData(key: String, value: String) {
|
||||
@@ -308,15 +333,11 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
history.add(entry)
|
||||
}
|
||||
|
||||
fun removeEntryFromHistory(position: Int) {
|
||||
history.removeAt(position)
|
||||
fun removeEntryFromHistory(position: Int): EntryKDBX? {
|
||||
return history.removeAt(position)
|
||||
}
|
||||
|
||||
fun removeAllHistory() {
|
||||
history.clear()
|
||||
}
|
||||
|
||||
fun removeOldestEntryFromHistory() {
|
||||
fun removeOldestEntryFromHistory(): EntryKDBX? {
|
||||
var min: Date? = null
|
||||
var index = -1
|
||||
|
||||
@@ -329,9 +350,9 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
}
|
||||
}
|
||||
|
||||
if (index != -1) {
|
||||
return if (index != -1) {
|
||||
history.removeAt(index)
|
||||
}
|
||||
} else null
|
||||
}
|
||||
|
||||
override fun touch(modified: Boolean, touchParents: Boolean) {
|
||||
@@ -348,6 +369,14 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
const val STR_URL = "URL"
|
||||
const val STR_NOTES = "Notes"
|
||||
|
||||
fun newCustomNameAllowed(name: String): Boolean {
|
||||
return !(name.equals(STR_TITLE, true)
|
||||
|| name.equals(STR_USERNAME, true)
|
||||
|| name.equals(STR_PASSWORD, true)
|
||||
|| name.equals(STR_URL, true)
|
||||
|| name.equals(STR_NOTES, true))
|
||||
}
|
||||
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<EntryKDBX> = object : Parcelable.Creator<EntryKDBX> {
|
||||
override fun createFromParcel(parcel: Parcel): EntryKDBX {
|
||||
|
||||
@@ -116,13 +116,6 @@ class InvalidCredentialsDatabaseException : LoadDatabaseException {
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class KeyFileEmptyDatabaseException : LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.keyfile_is_empty
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class NoMemoryDatabaseException: LoadDatabaseException {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.error_out_of_memory
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.kunzisoft.keepass.database.exception.VersionDatabaseException
|
||||
import com.kunzisoft.keepass.stream.*
|
||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||
import com.kunzisoft.keepass.utils.UnsignedLong
|
||||
import com.kunzisoft.keepass.utils.VariantDictionary
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
@@ -192,10 +193,11 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldID == PwDbHeaderV4Fields.EndOfHeader)
|
||||
return true
|
||||
|
||||
if (fieldData != null)
|
||||
when (fieldID) {
|
||||
PwDbHeaderV4Fields.EndOfHeader -> return true
|
||||
|
||||
PwDbHeaderV4Fields.CipherID -> setCipher(fieldData)
|
||||
|
||||
PwDbHeaderV4Fields.CompressionFlags -> setCompressionFlags(fieldData)
|
||||
@@ -220,10 +222,8 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
|
||||
|
||||
PwDbHeaderV4Fields.KdfParameters -> databaseV4.kdfParameters = KdfParameters.deserialize(fieldData)
|
||||
|
||||
PwDbHeaderV4Fields.PublicCustomData -> {
|
||||
databaseV4.publicCustomData = KdfParameters.deserialize(fieldData)!! // TODO verify
|
||||
throw IOException("Invalid header type: $fieldID")
|
||||
}
|
||||
PwDbHeaderV4Fields.PublicCustomData -> databaseV4.publicCustomData = VariantDictionary.deserialize(fieldData)
|
||||
|
||||
else -> throw IOException("Invalid header type: $fieldID")
|
||||
}
|
||||
|
||||
|
||||
@@ -27,14 +27,12 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.exception.*
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeader
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
||||
import com.kunzisoft.keepass.stream.*
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import org.joda.time.Instant
|
||||
import java.io.*
|
||||
import java.security.*
|
||||
import java.util.*
|
||||
@@ -282,11 +280,9 @@ class DatabaseInputKDB(cacheDirectory: File,
|
||||
0x000E -> {
|
||||
newEntry?.let { entry ->
|
||||
if (fieldSize > 0) {
|
||||
// Generate an unique new file with timestamp
|
||||
val binaryFile = File(cacheDirectory,
|
||||
Instant.now().millis.toString())
|
||||
entry.binaryData = BinaryAttachment(binaryFile)
|
||||
BufferedOutputStream(FileOutputStream(binaryFile)).use { outputStream ->
|
||||
val binaryAttachment = mDatabaseToOpen.buildNewBinary(cacheDirectory)
|
||||
entry.binaryData = binaryAttachment
|
||||
BufferedOutputStream(binaryAttachment.getOutputDataStream()).use { outputStream ->
|
||||
cipherInputStream.readBytes(fieldSize,
|
||||
DatabaseKDB.BUFFER_SIZE_BYTES) { buffer ->
|
||||
outputStream.write(buffer)
|
||||
|
||||
@@ -20,12 +20,14 @@
|
||||
package com.kunzisoft.keepass.database.file.input
|
||||
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||
import com.kunzisoft.keepass.crypto.StreamCipherFactory
|
||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.DeletedObject
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
|
||||
@@ -35,7 +37,7 @@ import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
||||
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.database.exception.*
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||
@@ -49,12 +51,14 @@ import org.bouncycastle.crypto.StreamCipher
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
import org.xmlpull.v1.XmlPullParserFactory
|
||||
import java.io.*
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.nio.charset.Charset
|
||||
import java.text.ParseException
|
||||
import java.util.*
|
||||
import java.util.zip.GZIPInputStream
|
||||
import java.util.zip.GZIPOutputStream
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.CipherInputStream
|
||||
import kotlin.math.min
|
||||
@@ -68,9 +72,6 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
|
||||
private var hashOfHeader: ByteArray? = null
|
||||
|
||||
private val unusedCacheFileName: String
|
||||
get() = mDatabase.binaryPool.findUnusedKey().toString()
|
||||
|
||||
private var readNextNode = true
|
||||
private val ctxGroups = Stack<GroupKDBX>()
|
||||
private var ctxGroup: GroupKDBX? = null
|
||||
@@ -233,9 +234,11 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
|
||||
var data = ByteArray(0)
|
||||
if (size > 0) {
|
||||
if (fieldId != DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
|
||||
if (fieldId != DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary) {
|
||||
// TODO OOM here
|
||||
data = dataInputStream.readBytes(size)
|
||||
}
|
||||
}
|
||||
|
||||
var result = true
|
||||
when (fieldId) {
|
||||
@@ -249,18 +252,16 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
header.innerRandomStreamKey = data
|
||||
}
|
||||
DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary -> {
|
||||
val flag = dataInputStream.readBytes(1)[0].toInt() != 0
|
||||
val protectedFlag = flag && DatabaseHeaderKDBX.KdbxBinaryFlags.Protected.toInt() != DatabaseHeaderKDBX.KdbxBinaryFlags.None.toInt()
|
||||
val byteLength = size - 1
|
||||
// Read in a file
|
||||
val file = File(cacheDirectory, unusedCacheFileName)
|
||||
FileOutputStream(file).use { outputStream ->
|
||||
val protectedFlag = dataInputStream.read().toByte() == DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
||||
val byteLength = size - 1
|
||||
// No compression at this level
|
||||
val protectedBinary = mDatabase.buildNewBinary(cacheDirectory, false, protectedFlag)
|
||||
protectedBinary.getOutputDataStream().use { outputStream ->
|
||||
dataInputStream.readBytes(byteLength, DatabaseKDBX.BUFFER_SIZE_BYTES) { buffer ->
|
||||
outputStream.write(buffer)
|
||||
}
|
||||
}
|
||||
val protectedBinary = BinaryAttachment(file, protectedFlag)
|
||||
mDatabase.binaryPool.add(protectedBinary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,14 +444,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
}
|
||||
|
||||
KdbContext.Binaries -> if (name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) {
|
||||
val key = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrId)
|
||||
if (key != null) {
|
||||
val pbData = readBinary(xpp)
|
||||
val id = Integer.parseInt(key)
|
||||
mDatabase.binaryPool.put(id, pbData!!)
|
||||
} else {
|
||||
readUnknown(xpp)
|
||||
}
|
||||
readBinary(xpp)
|
||||
} else {
|
||||
readUnknown(xpp)
|
||||
}
|
||||
@@ -749,8 +743,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
if (entryInHistory) {
|
||||
ctxEntry = ctxHistoryBase
|
||||
return KdbContext.EntryHistory
|
||||
}
|
||||
else if (ctxEntry != null) {
|
||||
} else if (ctxEntry != null) {
|
||||
// Add entry to the index only when close the XML element
|
||||
mDatabase.addEntryIndex(ctxEntry!!)
|
||||
}
|
||||
@@ -766,8 +759,9 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
|
||||
return KdbContext.Entry
|
||||
} else if (ctx == KdbContext.EntryBinary && name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) {
|
||||
if (ctxBinaryName != null && ctxBinaryValue != null)
|
||||
ctxEntry?.putProtectedBinary(ctxBinaryName!!, ctxBinaryValue!!)
|
||||
if (ctxBinaryName != null && ctxBinaryValue != null) {
|
||||
ctxEntry?.putAttachment(Attachment(ctxBinaryName!!, ctxBinaryValue!!), mDatabase.binaryPool)
|
||||
}
|
||||
ctxBinaryName = null
|
||||
ctxBinaryValue = null
|
||||
|
||||
@@ -885,9 +879,14 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
if (encoded.isEmpty()) {
|
||||
return DatabaseVersioned.UUID_ZERO
|
||||
}
|
||||
val buf = Base64.decode(encoded, BASE_64_FLAG)
|
||||
|
||||
return bytes16ToUuid(buf)
|
||||
return try {
|
||||
val buf = Base64.decode(encoded, BASE_64_FLAG)
|
||||
bytes16ToUuid(buf)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to read base 64 UUID, create a random one", e)
|
||||
UUID.randomUUID()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
@@ -947,17 +946,36 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
|
||||
// Reference Id to a binary already present in binary pool
|
||||
val ref = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrRef)
|
||||
if (ref != null) {
|
||||
xpp.next() // Consume end tag
|
||||
// New id to a binary
|
||||
val key = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrId)
|
||||
|
||||
return when {
|
||||
ref != null -> {
|
||||
xpp.next() // Consume end tag
|
||||
val id = Integer.parseInt(ref)
|
||||
return mDatabase.binaryPool[id]
|
||||
// A ref is not necessarily an index in Database V3.1
|
||||
var binaryRetrieve = mDatabase.binaryPool[id]
|
||||
// Create empty binary if not retrieved in pool
|
||||
if (binaryRetrieve == null) {
|
||||
binaryRetrieve = mDatabase.buildNewBinary(cacheDirectory,
|
||||
compression = false, protection = true, binaryPoolId = id)
|
||||
}
|
||||
return binaryRetrieve
|
||||
}
|
||||
key != null -> {
|
||||
createBinary(key.toIntOrNull(), xpp)
|
||||
}
|
||||
else -> {
|
||||
// New binary to retrieve
|
||||
createBinary(null, xpp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// New binary to retrieve
|
||||
else {
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
private fun createBinary(binaryId: Int?, xpp: XmlPullParser): BinaryAttachment? {
|
||||
var compressed = false
|
||||
var protected = false
|
||||
var protected = true
|
||||
|
||||
if (xpp.attributeCount > 0) {
|
||||
val compress = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrCompressed)
|
||||
@@ -973,22 +991,22 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
|
||||
val base64 = readString(xpp)
|
||||
if (base64.isEmpty())
|
||||
return BinaryAttachment()
|
||||
val data = Base64.decode(base64, BASE_64_FLAG)
|
||||
return null
|
||||
|
||||
val file = File(cacheDirectory, unusedCacheFileName)
|
||||
return FileOutputStream(file).use { outputStream ->
|
||||
// Force compression in this specific case
|
||||
if (mDatabase.compressionAlgorithm == CompressionAlgorithm.GZip
|
||||
&& !compressed) {
|
||||
GZIPOutputStream(outputStream).write(data)
|
||||
BinaryAttachment(file, protected, true)
|
||||
} else {
|
||||
outputStream.write(data)
|
||||
BinaryAttachment(file, protected, compressed)
|
||||
}
|
||||
// Build the new binary and compress
|
||||
val binaryAttachment = mDatabase.buildNewBinary(cacheDirectory, compressed, protected, binaryId)
|
||||
try {
|
||||
binaryAttachment.getOutputDataStream().use { outputStream ->
|
||||
outputStream.write(Base64.decode(base64, BASE_64_FLAG))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to read base 64 attachment", e)
|
||||
binaryAttachment.isCorrupted = true
|
||||
binaryAttachment.getOutputDataStream().use { outputStream ->
|
||||
outputStream.write(base64.toByteArray())
|
||||
}
|
||||
}
|
||||
return binaryAttachment
|
||||
}
|
||||
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
@@ -1045,6 +1063,8 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = DatabaseInputKDBX::class.java.name
|
||||
|
||||
private val DEFAULT_HISTORY_DAYS = UnsignedInt(365)
|
||||
|
||||
@Throws(XmlPullParserException::class)
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BU
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||
import com.kunzisoft.keepass.stream.LittleEndianDataOutputStream
|
||||
import com.kunzisoft.keepass.stream.readBytes
|
||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import kotlin.experimental.or
|
||||
@@ -36,33 +37,40 @@ class DatabaseInnerHeaderOutputKDBX(private val database: DatabaseKDBX,
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun output() {
|
||||
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomStreamID.toInt())
|
||||
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomStreamID)
|
||||
dataOutputStream.writeInt(4)
|
||||
if (header.innerRandomStream == null)
|
||||
throw IOException("Can't write innerRandomStream")
|
||||
dataOutputStream.writeInt(header.innerRandomStream!!.id.toKotlinInt())
|
||||
dataOutputStream.writeUInt(header.innerRandomStream!!.id)
|
||||
|
||||
val streamKeySize = header.innerRandomStreamKey.size
|
||||
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey.toInt())
|
||||
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.InnerRandomstreamKey)
|
||||
dataOutputStream.writeInt(streamKeySize)
|
||||
dataOutputStream.write(header.innerRandomStreamKey)
|
||||
|
||||
database.binaryPool.doForEachBinary { _, protectedBinary ->
|
||||
database.binaryPool.doForEachOrderedBinary { _, keyBinary ->
|
||||
val protectedBinary = keyBinary.binary
|
||||
// Force decompression to add binary in header
|
||||
protectedBinary.decompress()
|
||||
// Write type binary
|
||||
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
|
||||
// Write size
|
||||
dataOutputStream.writeUInt(UnsignedInt.fromKotlinLong(protectedBinary.length() + 1))
|
||||
// Write protected flag
|
||||
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
|
||||
if (protectedBinary.isProtected) {
|
||||
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
|
||||
}
|
||||
dataOutputStream.writeByte(flag)
|
||||
|
||||
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary.toInt())
|
||||
dataOutputStream.writeInt(protectedBinary.length().toInt() + 1) // TODO verify
|
||||
dataOutputStream.write(flag.toInt())
|
||||
|
||||
protectedBinary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||
protectedBinary.getInputDataStream().use { inputStream ->
|
||||
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||
dataOutputStream.write(buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader.toInt())
|
||||
dataOutputStream.writeByte(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader)
|
||||
dataOutputStream.writeInt(0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
|
||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
||||
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
||||
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||
@@ -55,7 +55,6 @@ import java.io.OutputStream
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import java.util.zip.GZIPInputStream
|
||||
import java.util.zip.GZIPOutputStream
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.CipherOutputStream
|
||||
@@ -360,6 +359,9 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
|
||||
writeFields(entry.fields)
|
||||
writeEntryBinaries(entry.binaries)
|
||||
if (entry.containsCustomData()) {
|
||||
writeCustomData(entry.customData)
|
||||
}
|
||||
writeAutoType(entry.autoType)
|
||||
|
||||
if (!isHistory) {
|
||||
@@ -422,7 +424,6 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
private fun writeBinary(binary : BinaryAttachment) {
|
||||
val binaryLength = binary.length()
|
||||
if (binaryLength > 0) {
|
||||
|
||||
if (binary.isProtected) {
|
||||
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
|
||||
|
||||
@@ -433,21 +434,11 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
xml.text(charArray, 0, charArray.size)
|
||||
}
|
||||
} else {
|
||||
// Force binary compression from database (compression was harmonized during import)
|
||||
if (mDatabaseKDBX.compressionAlgorithm === CompressionAlgorithm.GZip) {
|
||||
if (binary.isCompressed) {
|
||||
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
|
||||
}
|
||||
|
||||
// Force decompression in this specific case
|
||||
val binaryInputStream = if (mDatabaseKDBX.compressionAlgorithm == CompressionAlgorithm.None
|
||||
&& binary.isCompressed == true) {
|
||||
GZIPInputStream(binary.getInputDataStream())
|
||||
} else {
|
||||
binary.getInputDataStream()
|
||||
}
|
||||
|
||||
// Write the XML
|
||||
binaryInputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||
binary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||
val charArray = String(Base64.encode(buffer, BASE_64_FLAG)).toCharArray()
|
||||
xml.text(charArray, 0, charArray.size)
|
||||
}
|
||||
@@ -459,10 +450,11 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
private fun writeMetaBinaries() {
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemBinaries)
|
||||
|
||||
mDatabaseKDBX.binaryPool.doForEachBinary { key, binary ->
|
||||
// Use indexes because necessarily in DatabaseV4 (binary header ref is the order)
|
||||
mDatabaseKDBX.binaryPool.doForEachOrderedBinary { index, keyBinary ->
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
|
||||
xml.attribute(null, DatabaseKDBXXML.AttrId, key.toString())
|
||||
writeBinary(binary)
|
||||
xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString())
|
||||
writeBinary(keyBinary.binary)
|
||||
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
|
||||
}
|
||||
|
||||
@@ -559,25 +551,24 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
private fun writeEntryBinaries(binaries: Map<String, BinaryAttachment>) {
|
||||
for ((key, binary) in binaries) {
|
||||
private fun writeEntryBinaries(binaries: LinkedHashMap<String, Int>) {
|
||||
for ((label, poolId) in binaries) {
|
||||
// Retrieve the right index with the poolId, don't use ref because of header in DatabaseV4
|
||||
mDatabaseKDBX.binaryPool.getBinaryIndexFromKey(poolId)?.toString()?.let { indexString ->
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemKey)
|
||||
xml.text(safeXmlString(key))
|
||||
xml.text(safeXmlString(label))
|
||||
xml.endTag(null, DatabaseKDBXXML.ElemKey)
|
||||
|
||||
xml.startTag(null, DatabaseKDBXXML.ElemValue)
|
||||
val ref = mDatabaseKDBX.binaryPool.findKey(binary)
|
||||
if (ref != null) {
|
||||
xml.attribute(null, DatabaseKDBXXML.AttrRef, ref.toString())
|
||||
} else {
|
||||
writeBinary(binary)
|
||||
}
|
||||
// Use only pool data in Meta to save binaries
|
||||
xml.attribute(null, DatabaseKDBXXML.AttrRef, indexString)
|
||||
xml.endTag(null, DatabaseKDBXXML.ElemValue)
|
||||
|
||||
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
private fun writeDeletedObjects(value: List<DeletedObject>) {
|
||||
|
||||
@@ -28,14 +28,13 @@ import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorK
|
||||
import com.kunzisoft.keepass.database.search.iterator.EntrySearchStringIteratorKDBX
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.model.getSearchString
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
|
||||
class SearchHelper {
|
||||
|
||||
companion object {
|
||||
const val MAX_SEARCH_ENTRY = 6
|
||||
const val MAX_SEARCH_ENTRY = 10
|
||||
|
||||
/**
|
||||
* Utility method to perform actions if item is found or not after an auto search in [database]
|
||||
@@ -53,7 +52,7 @@ class SearchHelper {
|
||||
&& !searchInfo.containsOnlyNullValues()) {
|
||||
// If search provide results
|
||||
database.createVirtualGroupFromSearchInfo(
|
||||
searchInfo.getSearchString(context),
|
||||
searchInfo.toString(),
|
||||
PreferencesUtil.omitBackup(context),
|
||||
MAX_SEARCH_ENTRY
|
||||
)?.let { searchGroup ->
|
||||
|
||||
@@ -33,6 +33,7 @@ class SearchParameters {
|
||||
var searchInUrls = true
|
||||
var searchInGroupNames = false
|
||||
var searchInNotes = true
|
||||
var searchInOTP = false
|
||||
var searchInOther = true
|
||||
var searchInUUIDs = false
|
||||
var searchInTags = true
|
||||
@@ -54,6 +55,7 @@ class SearchParameters {
|
||||
this.searchInUrls = source.searchInUrls
|
||||
this.searchInGroupNames = source.searchInGroupNames
|
||||
this.searchInNotes = source.searchInNotes
|
||||
this.searchInOTP = source.searchInOTP
|
||||
this.searchInOther = source.searchInOther
|
||||
this.searchInUUIDs = source.searchInUUIDs
|
||||
this.searchInTags = source.searchInTags
|
||||
@@ -69,6 +71,7 @@ class SearchParameters {
|
||||
searchInUrls = false
|
||||
searchInGroupNames = false
|
||||
searchInNotes = false
|
||||
searchInOTP = false
|
||||
searchInOther = false
|
||||
searchInUUIDs = false
|
||||
searchInTags = false
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.database.search.iterator
|
||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import java.util.*
|
||||
import kotlin.collections.Map.Entry
|
||||
|
||||
@@ -75,6 +76,7 @@ class EntrySearchStringIteratorKDBX(
|
||||
EntryKDBX.STR_PASSWORD -> mSearchParameters.searchInPasswords
|
||||
EntryKDBX.STR_URL -> mSearchParameters.searchInUrls
|
||||
EntryKDBX.STR_NOTES -> mSearchParameters.searchInNotes
|
||||
OtpEntryFields.OTP_FIELD -> mSearchParameters.searchInOTP
|
||||
else -> mSearchParameters.searchInOther
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,9 +95,9 @@ open class Education(val activity: Activity) {
|
||||
R.string.education_entry_edit_key,
|
||||
R.string.education_password_generator_key,
|
||||
R.string.education_entry_new_field_key,
|
||||
R.string.education_add_attachment_key,
|
||||
R.string.education_setup_OTP_key)
|
||||
|
||||
|
||||
/**
|
||||
* Get preferences bundle for education
|
||||
*/
|
||||
@@ -272,6 +272,18 @@ open class Education(val activity: Activity) {
|
||||
context.resources.getBoolean(R.bool.education_entry_new_field_default))
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the explanatory view of the new attachment button in an entry has already been displayed.
|
||||
*
|
||||
* @param context The context to open the SharedPreferences
|
||||
* @return boolean value of education_add_attachment_key key
|
||||
*/
|
||||
fun isEducationAddAttachmentPerformed(context: Context): Boolean {
|
||||
val prefs = getEducationSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.education_add_attachment_key),
|
||||
context.resources.getBoolean(R.bool.education_add_attachment_default))
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the explanatory view to setup OTP has already been displayed.
|
||||
*
|
||||
|
||||
@@ -29,6 +29,10 @@ import com.kunzisoft.keepass.R
|
||||
class EntryEditActivityEducation(activity: Activity)
|
||||
: Education(activity) {
|
||||
|
||||
/**
|
||||
* Check and display learning views
|
||||
* Displays the explanation for the password generator
|
||||
*/
|
||||
fun checkAndPerformedGeneratePasswordEducation(educationView: View,
|
||||
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
|
||||
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
|
||||
@@ -56,7 +60,7 @@ class EntryEditActivityEducation(activity: Activity)
|
||||
|
||||
/**
|
||||
* Check and display learning views
|
||||
* Displays the explanation for the icon selection, the password generator and for a new field
|
||||
* Displays the explanation to create a new field
|
||||
*/
|
||||
fun checkAndPerformedEntryNewFieldEducation(educationView: View,
|
||||
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
|
||||
@@ -83,6 +87,35 @@ class EntryEditActivityEducation(activity: Activity)
|
||||
R.string.education_entry_new_field_key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and display learning views
|
||||
* Displays the explanation for to upload attachment
|
||||
*/
|
||||
fun checkAndPerformedAttachmentEducation(educationView: View,
|
||||
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
|
||||
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
|
||||
return checkAndPerformedEducation(!isEducationAddAttachmentPerformed(activity),
|
||||
TapTarget.forView(educationView,
|
||||
activity.getString(R.string.education_add_attachment_title),
|
||||
activity.getString(R.string.education_add_attachment_summary))
|
||||
.textColorInt(Color.WHITE)
|
||||
.tintTarget(true)
|
||||
.cancelable(true),
|
||||
object : TapTargetView.Listener() {
|
||||
override fun onTargetClick(view: TapTargetView) {
|
||||
super.onTargetClick(view)
|
||||
onEducationViewClick?.invoke(view)
|
||||
}
|
||||
|
||||
override fun onOuterCircleClick(view: TapTargetView?) {
|
||||
super.onOuterCircleClick(view)
|
||||
view?.dismiss(false)
|
||||
onOuterViewClick?.invoke(view)
|
||||
}
|
||||
},
|
||||
R.string.education_add_attachment_key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and display learning views
|
||||
* Displays the explanation to setup OTP
|
||||
|
||||
@@ -86,8 +86,8 @@ class PasswordActivityEducation(activity: Activity)
|
||||
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
|
||||
return checkAndPerformedEducation(!isEducationBiometricPerformed(activity),
|
||||
TapTarget.forView(educationView,
|
||||
activity.getString(R.string.education_biometric_title),
|
||||
activity.getString(R.string.education_biometric_summary))
|
||||
activity.getString(R.string.education_advanced_unlock_title),
|
||||
activity.getString(R.string.education_advanced_unlock_summary))
|
||||
.textColorInt(Color.WHITE)
|
||||
.tintTarget(false)
|
||||
.cancelable(true),
|
||||
|
||||
@@ -42,6 +42,7 @@ import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.Field
|
||||
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
|
||||
@@ -184,7 +185,6 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun switchToPreviousKeyboard() {
|
||||
var imeManager: InputMethodManager? = null
|
||||
try {
|
||||
@@ -246,6 +246,13 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
}
|
||||
actionGoAutomatically()
|
||||
}
|
||||
KEY_OTP -> {
|
||||
if (entryInfoKey != null) {
|
||||
currentInputConnection.commitText(
|
||||
entryInfoKey!!.getGeneratedFieldValue(OTP_TOKEN_FIELD), 1)
|
||||
}
|
||||
actionGoAutomatically()
|
||||
}
|
||||
KEY_URL -> {
|
||||
if (entryInfoKey != null) {
|
||||
currentInputConnection.commitText(entryInfoKey!!.url, 1)
|
||||
@@ -255,7 +262,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
KEY_FIELDS -> {
|
||||
if (entryInfoKey != null) {
|
||||
fieldsAdapter?.apply {
|
||||
setFields(entryInfoKey!!.customFields)
|
||||
setFields(entryInfoKey!!.customFields.filter { it.name != OTP_TOKEN_FIELD})
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -327,6 +334,7 @@ class MagikIME : InputMethodService(), KeyboardView.OnKeyboardActionListener {
|
||||
private const val KEY_ENTRY = 620
|
||||
private const val KEY_USERNAME = 500
|
||||
private const val KEY_PASSWORD = 510
|
||||
private const val KEY_OTP = 515
|
||||
private const val KEY_URL = 520
|
||||
private const val KEY_FIELDS = 530
|
||||
|
||||
|
||||
@@ -21,24 +21,25 @@ package com.kunzisoft.keepass.model
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
||||
import com.kunzisoft.keepass.utils.readEnum
|
||||
import com.kunzisoft.keepass.utils.writeEnum
|
||||
|
||||
data class EntryAttachment(var name: String,
|
||||
var binaryAttachment: BinaryAttachment,
|
||||
data class EntryAttachmentState(var attachment: Attachment,
|
||||
var streamDirection: StreamDirection,
|
||||
var downloadState: AttachmentState = AttachmentState.NULL,
|
||||
var downloadProgression: Int = 0) : Parcelable {
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readString() ?: "",
|
||||
parcel.readParcelable(BinaryAttachment::class.java.classLoader) ?: BinaryAttachment(),
|
||||
parcel.readParcelable(Attachment::class.java.classLoader) ?: Attachment("", BinaryAttachment()),
|
||||
parcel.readEnum<StreamDirection>() ?: StreamDirection.DOWNLOAD,
|
||||
parcel.readEnum<AttachmentState>() ?: AttachmentState.NULL,
|
||||
parcel.readInt())
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(name)
|
||||
parcel.writeParcelable(binaryAttachment, flags)
|
||||
parcel.writeParcelable(attachment, flags)
|
||||
parcel.writeEnum(streamDirection)
|
||||
parcel.writeEnum(downloadState)
|
||||
parcel.writeInt(downloadProgression)
|
||||
}
|
||||
@@ -49,26 +50,23 @@ data class EntryAttachment(var name: String,
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is EntryAttachment) return false
|
||||
if (other !is EntryAttachmentState) return false
|
||||
|
||||
if (name != other.name) return false
|
||||
if (binaryAttachment != other.binaryAttachment) return false
|
||||
if (attachment != other.attachment) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = name.hashCode()
|
||||
result = 31 * result + binaryAttachment.hashCode()
|
||||
return result
|
||||
return attachment.hashCode()
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<EntryAttachment> {
|
||||
override fun createFromParcel(parcel: Parcel): EntryAttachment {
|
||||
return EntryAttachment(parcel)
|
||||
companion object CREATOR : Parcelable.Creator<EntryAttachmentState> {
|
||||
override fun createFromParcel(parcel: Parcel): EntryAttachmentState {
|
||||
return EntryAttachmentState(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<EntryAttachment?> {
|
||||
override fun newArray(size: Int): Array<EntryAttachmentState?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
@@ -21,21 +21,30 @@ package com.kunzisoft.keepass.model
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class EntryInfo : Parcelable {
|
||||
|
||||
var id: String = ""
|
||||
var title: String = ""
|
||||
var icon: IconImage? = null
|
||||
var icon: IconImage = IconImageStandard()
|
||||
var username: String = ""
|
||||
var password: String = ""
|
||||
var expires: Boolean = false
|
||||
var expiryTime: DateInstant = DateInstant.IN_ONE_MONTH
|
||||
var url: String = ""
|
||||
var notes: String = ""
|
||||
var customFields: MutableList<Field> = ArrayList()
|
||||
var customFields: List<Field> = ArrayList()
|
||||
var attachments: List<Attachment> = ArrayList()
|
||||
var otpModel: OtpModel? = null
|
||||
|
||||
constructor()
|
||||
@@ -43,12 +52,15 @@ class EntryInfo : Parcelable {
|
||||
private constructor(parcel: Parcel) {
|
||||
id = parcel.readString() ?: id
|
||||
title = parcel.readString() ?: title
|
||||
icon = parcel.readParcelable(IconImage::class.java.classLoader)
|
||||
icon = parcel.readParcelable(IconImage::class.java.classLoader) ?: icon
|
||||
username = parcel.readString() ?: username
|
||||
password = parcel.readString() ?: password
|
||||
expires = parcel.readInt() != 0
|
||||
expiryTime = parcel.readParcelable(DateInstant::class.java.classLoader) ?: expiryTime
|
||||
url = parcel.readString() ?: url
|
||||
notes = parcel.readString() ?: notes
|
||||
parcel.readList(customFields as List<Field>, Field::class.java.classLoader)
|
||||
parcel.readList(customFields, Field::class.java.classLoader)
|
||||
parcel.readList(attachments, Attachment::class.java.classLoader)
|
||||
otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel
|
||||
}
|
||||
|
||||
@@ -62,9 +74,12 @@ class EntryInfo : Parcelable {
|
||||
parcel.writeParcelable(icon, flags)
|
||||
parcel.writeString(username)
|
||||
parcel.writeString(password)
|
||||
parcel.writeInt(if (expires) 1 else 0)
|
||||
parcel.writeParcelable(expiryTime, flags)
|
||||
parcel.writeString(url)
|
||||
parcel.writeString(notes)
|
||||
parcel.writeArray(customFields.toTypedArray())
|
||||
parcel.writeArray(attachments.toTypedArray())
|
||||
parcel.writeParcelable(otpModel, flags)
|
||||
}
|
||||
|
||||
@@ -81,16 +96,83 @@ class EntryInfo : Parcelable {
|
||||
}
|
||||
|
||||
fun getGeneratedFieldValue(label: String): String {
|
||||
otpModel?.let {
|
||||
if (label == OTP_TOKEN_FIELD) {
|
||||
otpModel?.let {
|
||||
return OtpElement(it).token
|
||||
}
|
||||
}
|
||||
return customFields.lastOrNull { it.name == label }?.protectedValue?.toString() ?: ""
|
||||
}
|
||||
|
||||
private fun addUniqueField(field: Field, number: Int = 0) {
|
||||
var sameName = false
|
||||
var sameValue = false
|
||||
val suffix = if (number > 0) "_$number" else ""
|
||||
customFields.forEach { currentField ->
|
||||
// Not write the same data again
|
||||
if (currentField.protectedValue.stringValue == field.protectedValue.stringValue) {
|
||||
sameValue = true
|
||||
return
|
||||
}
|
||||
// Same name but new value, create a new suffix
|
||||
if (currentField.name == field.name + suffix) {
|
||||
sameName = true
|
||||
addUniqueField(field, number + 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (!sameName && !sameValue)
|
||||
(customFields as ArrayList<Field>).add(Field(field.name + suffix, field.protectedValue))
|
||||
}
|
||||
|
||||
fun saveSearchInfo(database: Database?, searchInfo: SearchInfo) {
|
||||
searchInfo.otpString?.let { otpString ->
|
||||
// Replace the OTP field
|
||||
OtpEntryFields.parseOTPUri(otpString)?.let { otpElement ->
|
||||
if (title.isEmpty())
|
||||
title = otpElement.issuer
|
||||
if (username.isEmpty())
|
||||
username = otpElement.name
|
||||
// Add OTP field
|
||||
val mutableCustomFields = customFields as ArrayList<Field>
|
||||
val otpField = OtpEntryFields.buildOtpField(otpElement, null, null)
|
||||
if (mutableCustomFields.contains(otpField)) {
|
||||
mutableCustomFields.remove(otpField)
|
||||
}
|
||||
mutableCustomFields.add(otpField)
|
||||
}
|
||||
} ?: searchInfo.webDomain?.let { webDomain ->
|
||||
// If unable to save web domain in custom field or URL not populated, save in URL
|
||||
val scheme = searchInfo.webScheme
|
||||
val webScheme = if (scheme.isNullOrEmpty()) "http" else scheme
|
||||
val webDomainToStore = "$webScheme://$webDomain"
|
||||
if (database?.allowEntryCustomFields() != true || url.isEmpty()) {
|
||||
url = webDomainToStore
|
||||
}
|
||||
else if (url != webDomainToStore){
|
||||
// Save web domain in custom field
|
||||
addUniqueField(Field(WEB_DOMAIN_FIELD_NAME,
|
||||
ProtectedString(false, webDomainToStore)),
|
||||
1 // Start to one because URL is a standard field name
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
// Save application id in custom field
|
||||
if (database?.allowEntryCustomFields() == true) {
|
||||
searchInfo.applicationId?.let { applicationId ->
|
||||
addUniqueField(Field(APPLICATION_ID_FIELD_NAME,
|
||||
ProtectedString(false, applicationId))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val WEB_DOMAIN_FIELD_NAME = "URL"
|
||||
const val APPLICATION_ID_FIELD_NAME = "AndroidApp"
|
||||
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<EntryInfo> = object : Parcelable.Creator<EntryInfo> {
|
||||
override fun createFromParcel(parcel: Parcel): EntryInfo {
|
||||
|
||||
@@ -33,6 +33,11 @@ class Field : Parcelable {
|
||||
this.protectedValue = value
|
||||
}
|
||||
|
||||
constructor(fieldToCopy: Field) {
|
||||
this.name = fieldToCopy.name
|
||||
this.protectedValue = fieldToCopy.protectedValue
|
||||
}
|
||||
|
||||
constructor(parcel: Parcel) {
|
||||
this.name = parcel.readString() ?: name
|
||||
this.protectedValue = parcel.readParcelable(ProtectedString::class.java.classLoader) ?: protectedValue
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user