mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
977 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
32cd998c2a | ||
|
|
691fc6335e | ||
|
|
b0025d1416 | ||
|
|
2467d8b0e7 | ||
|
|
28993c53e7 | ||
|
|
efdea870f0 | ||
|
|
b2995ec862 | ||
|
|
2bcc84dbb2 | ||
|
|
70cc98ce33 | ||
|
|
6e055f398d | ||
|
|
9f6234f032 | ||
|
|
6135544b72 | ||
|
|
5f32ec218b | ||
|
|
3b9a884db2 | ||
|
|
ada6b85868 | ||
|
|
2f8a4f447c | ||
|
|
9bd6499271 | ||
|
|
76fcc919ef | ||
|
|
a382297edf | ||
|
|
4987dfe4f6 | ||
|
|
d1a2e50b8d | ||
|
|
b9e44b6166 | ||
|
|
af601edc94 | ||
|
|
6640dcf9cd | ||
|
|
9b2734ed38 | ||
|
|
afcfce056f | ||
|
|
8f3af2f27b | ||
|
|
706aae47b3 | ||
|
|
d494295b21 | ||
|
|
c1600a253b | ||
|
|
a3bc29ad8f | ||
|
|
83225ed157 | ||
|
|
f13f6dc01f | ||
|
|
2d2489443a | ||
|
|
0e5b7fbfa2 | ||
|
|
d16a8068f7 | ||
|
|
c5ef11febc | ||
|
|
027f31447a | ||
|
|
41d1a4e5fb | ||
|
|
924e3191cb | ||
|
|
ebdc6b8fd9 | ||
|
|
5c05128cd7 | ||
|
|
5bf5685a12 | ||
|
|
f7391cb4c4 | ||
|
|
bad7bd8884 | ||
|
|
e0524a1656 | ||
|
|
f59af9baa3 | ||
|
|
b36890ca82 | ||
|
|
22703af08b | ||
|
|
c4108269b3 | ||
|
|
94c9d090cf | ||
|
|
28b9235a43 | ||
|
|
3aebae5e15 | ||
|
|
2631cb75d6 | ||
|
|
07b8b4156f | ||
|
|
7978967c1a | ||
|
|
3f24ff4de3 | ||
|
|
7253dd82a6 | ||
|
|
2b8eb3ae7e | ||
|
|
429eae71cd | ||
|
|
e5c552defb | ||
|
|
5c950a2e2c | ||
|
|
577ff78189 | ||
|
|
3f3cde05f7 | ||
|
|
b48f2c3276 | ||
|
|
8e818846f0 | ||
|
|
1554e37f8c | ||
|
|
affaabd011 | ||
|
|
f34a8f991c | ||
|
|
c2b14d610b | ||
|
|
02693d0cbb | ||
|
|
23155279ab | ||
|
|
2d23f7403d | ||
|
|
ae411c6fd5 | ||
|
|
ab8d6075a9 | ||
|
|
bc5ae29a67 | ||
|
|
1a8aabc30c | ||
|
|
8c51d7f713 | ||
|
|
8a1485e7ce | ||
|
|
614145431a | ||
|
|
db25f1999f | ||
|
|
4ed231b9bb | ||
|
|
25a5342c11 | ||
|
|
c7202e3ca9 | ||
|
|
89c2e94cea | ||
|
|
3dc46771b5 | ||
|
|
0eac4d4d7f | ||
|
|
a0ceb788db | ||
|
|
98fb36d03a | ||
|
|
a670006517 | ||
|
|
9cdbe67cd4 | ||
|
|
bbe8af452c | ||
|
|
f5edf28ce1 | ||
|
|
8fc30f590b | ||
|
|
e578f23ebe | ||
|
|
99c488fc9e | ||
|
|
f6a710660d | ||
|
|
a61744bb65 | ||
|
|
17c3078c24 | ||
|
|
5fa7731b56 | ||
|
|
c8e2be4d8c | ||
|
|
e3db613a07 | ||
|
|
0f7839027f | ||
|
|
31b322a108 | ||
|
|
b3c0494618 | ||
|
|
78ddb0533d | ||
|
|
da2158e7f2 | ||
|
|
d2a1efb6e7 | ||
|
|
98a880db2d | ||
|
|
cb82ef8703 | ||
|
|
d56246767b | ||
|
|
bb477984aa | ||
|
|
5a3e650e02 | ||
|
|
3c96dd2fac | ||
|
|
da051e3ff3 | ||
|
|
d15b6323c2 | ||
|
|
ec5f8fe4a4 | ||
|
|
71d84d76f8 | ||
|
|
d33ed52ec2 | ||
|
|
3a970544bb | ||
|
|
792ac3a2e8 | ||
|
|
60bbc27401 | ||
|
|
cf7cbcb6e6 | ||
|
|
c126a8eba9 | ||
|
|
66a60d0357 | ||
|
|
1acdadd027 | ||
|
|
200be9dadd | ||
|
|
a73e2872a4 | ||
|
|
ce6f7729c5 | ||
|
|
c285411371 | ||
|
|
46394c600e | ||
|
|
bea9cb3248 | ||
|
|
1087dcd714 | ||
|
|
0b63029b7e | ||
|
|
9679d24414 | ||
|
|
7947fd53e5 | ||
|
|
8dedf75565 | ||
|
|
b5b7c12b49 | ||
|
|
51f4e3cc3a | ||
|
|
24b0315d2e | ||
|
|
be446220eb | ||
|
|
a3ef2d332e | ||
|
|
ba6fe576e3 | ||
|
|
abcef38102 | ||
|
|
d5780b2f30 | ||
|
|
f7e498a0a2 | ||
|
|
51ac7ca2de | ||
|
|
c94535f6b5 | ||
|
|
07457ae368 | ||
|
|
4767fff08c | ||
|
|
f0c3498ecc | ||
|
|
1eca52d0fe | ||
|
|
7fbac9ad2f | ||
|
|
6fb80ed50b | ||
|
|
47f63ac81b | ||
|
|
42cc0b28ba | ||
|
|
993806f781 | ||
|
|
8a5af33aaa | ||
|
|
a974e36e9e | ||
|
|
17bcb2b39e | ||
|
|
cddf02d0c1 | ||
|
|
75ff7ece37 | ||
|
|
ec2b407a20 | ||
|
|
1dc9d78e54 | ||
|
|
5742a75c9d | ||
|
|
b5e9ad6d7e | ||
|
|
6393025219 | ||
|
|
9ab3e289bc | ||
|
|
6454474886 | ||
|
|
c5720a7a03 | ||
|
|
41b15adc6d | ||
|
|
05b962e718 | ||
|
|
1f01ca7b85 | ||
|
|
5d3b4fa5ec | ||
|
|
754d2b2dd3 | ||
|
|
ffad62e3dc | ||
|
|
1c11e16565 | ||
|
|
edc12990b4 | ||
|
|
12ea234d18 | ||
|
|
0461206a61 | ||
|
|
663f9e3962 | ||
|
|
34ee948c8e | ||
|
|
1bb9c2e4fe | ||
|
|
8ab18ce5cc | ||
|
|
71be16826e | ||
|
|
926c09d9df | ||
|
|
66c065ae7f | ||
|
|
083ed7775c | ||
|
|
5185452495 | ||
|
|
fa15f226ab | ||
|
|
fea4da2a33 | ||
|
|
055c933f4b | ||
|
|
9bcb867748 | ||
|
|
26e3c03f5f | ||
|
|
c195c3b2d1 | ||
|
|
f9e0aacfeb | ||
|
|
37fef66647 | ||
|
|
9f99b67563 | ||
|
|
fe902648ad | ||
|
|
13b933cd0b | ||
|
|
1d3b1d1d80 | ||
|
|
67a612af3a | ||
|
|
a891683806 | ||
|
|
440a72fc42 | ||
|
|
696d2e5197 | ||
|
|
2b17d56fc7 | ||
|
|
a410ef5d9f | ||
|
|
fe94769541 | ||
|
|
c63ae9c00c | ||
|
|
d5ece8d007 | ||
|
|
692a971dc0 | ||
|
|
05b8370cc0 | ||
|
|
b6111b35a2 | ||
|
|
4d72687628 | ||
|
|
8f125983ce | ||
|
|
4279825caa | ||
|
|
77ae3a4623 | ||
|
|
4c222dbc54 | ||
|
|
4e0f93ee8a | ||
|
|
e99f3e6627 | ||
|
|
f73877c34a | ||
|
|
abd3f12cae | ||
|
|
00117f5b7b | ||
|
|
d7d728f93e | ||
|
|
dc9217c4ec | ||
|
|
95acb13b93 | ||
|
|
234cc00d9f | ||
|
|
d6ba164799 | ||
|
|
910aa03dc8 | ||
|
|
a3e4a4c873 | ||
|
|
2d7c843447 | ||
|
|
e342b45473 | ||
|
|
f354bccd58 | ||
|
|
98073134db | ||
|
|
360666b00b | ||
|
|
ce4c807870 | ||
|
|
f2783bdac8 | ||
|
|
875ed16c3b | ||
|
|
383ba56d1f | ||
|
|
45eb54e624 | ||
|
|
5aff4e2ed6 | ||
|
|
e73e47dd94 | ||
|
|
1c8ac5efbc | ||
|
|
90fa5e1ecd | ||
|
|
348994917b | ||
|
|
60dbea1027 | ||
|
|
dae19bbccf | ||
|
|
c81f83887e | ||
|
|
04e555dde9 | ||
|
|
be94518e31 | ||
|
|
66bec1e08c | ||
|
|
f61ce10716 | ||
|
|
b1b92b2995 | ||
|
|
bd9f2c4757 | ||
|
|
a3ead2153e | ||
|
|
e12f008b92 | ||
|
|
d064ece0ff | ||
|
|
379fbf68b1 | ||
|
|
83783c1a88 | ||
|
|
c7e46205b3 | ||
|
|
0c61f0ded2 | ||
|
|
49e2ec0498 | ||
|
|
fb0a74c101 | ||
|
|
245a7ddfe9 | ||
|
|
ca8874c2e1 | ||
|
|
dbcd7c8e03 | ||
|
|
9cce63659e | ||
|
|
be0bbab0c8 | ||
|
|
7b6d3698c4 | ||
|
|
56daca8b4f | ||
|
|
ed382d102e | ||
|
|
745de2502e | ||
|
|
503a4b1374 | ||
|
|
0b71b2d659 | ||
|
|
2ad244df94 | ||
|
|
d0da4f03a6 | ||
|
|
ab0cd4152a | ||
|
|
93d04bfe60 | ||
|
|
b1ccb40bd3 | ||
|
|
d177001ea8 | ||
|
|
2f921897c7 | ||
|
|
1a0f7146ce | ||
|
|
dff2386594 | ||
|
|
55cc782cc6 | ||
|
|
6903099873 | ||
|
|
6e2d84be33 | ||
|
|
9e542d0bbe | ||
|
|
ade9af9ecd | ||
|
|
59f11a1b26 | ||
|
|
cc1d6e2b47 | ||
|
|
2655b1b3d1 | ||
|
|
053dd28f8c | ||
|
|
65e4cf83d8 | ||
|
|
419099318c | ||
|
|
972edd3a30 | ||
|
|
cc4125e766 | ||
|
|
da40cc9830 | ||
|
|
29d7e2dcfe | ||
|
|
3d906fd582 | ||
|
|
0cad43c18b | ||
|
|
c66b686a63 | ||
|
|
e2a1e3f327 | ||
|
|
2b5cf75a53 | ||
|
|
1c350ac87b | ||
|
|
ef6539909c | ||
|
|
47d7c6fe65 | ||
|
|
c94c5fbc95 | ||
|
|
63a833d114 | ||
|
|
72162128e2 | ||
|
|
a8f712c335 | ||
|
|
ea8888a685 | ||
|
|
2aecf69b67 | ||
|
|
1a4c24dd86 | ||
|
|
6cf2b45051 | ||
|
|
38dd2bdf6e | ||
|
|
8784f1da70 | ||
|
|
b9208ea94e | ||
|
|
dfb648b480 | ||
|
|
f7af4f06ea | ||
|
|
d98f9eb62c | ||
|
|
fef1335f51 | ||
|
|
59f14cbdd4 | ||
|
|
e8645c543f | ||
|
|
b92f6177e3 | ||
|
|
2888829a4f | ||
|
|
3d392ead19 | ||
|
|
a79b2bfa79 | ||
|
|
15e1e2f02e | ||
|
|
9e652803d0 | ||
|
|
7795fceb72 | ||
|
|
9f01f26ea6 | ||
|
|
abbc9a1d7a | ||
|
|
2c15befb8e | ||
|
|
ba8ea84c4e | ||
|
|
97912046da | ||
|
|
43e99c23e3 | ||
|
|
ffe14d75fc | ||
|
|
872ef66641 | ||
|
|
6363862ec2 | ||
|
|
775a112e83 | ||
|
|
7168904290 | ||
|
|
4213209b08 | ||
|
|
5dce91b7f6 | ||
|
|
6d7fb9f87c | ||
|
|
cc6c9dd8d1 | ||
|
|
21ebec52f3 | ||
|
|
11023ab225 | ||
|
|
fe97b15905 | ||
|
|
de8738aa03 | ||
|
|
568bbf9126 | ||
|
|
a2481652da | ||
|
|
33e3d3272d | ||
|
|
b909651c29 | ||
|
|
16794c5252 | ||
|
|
49ad270d88 | ||
|
|
df9b6cb7e8 | ||
|
|
c6c4551928 | ||
|
|
de84353eb0 | ||
|
|
6f05b80f34 | ||
|
|
797dc706e2 | ||
|
|
bdb615bcf9 | ||
|
|
1b98717b0e | ||
|
|
7bd1aedada | ||
|
|
34bf8f9d1f | ||
|
|
b07f70e9fe | ||
|
|
e635788955 | ||
|
|
bdb1cef3e5 | ||
|
|
e5a32e65c0 | ||
|
|
5fc77922b4 | ||
|
|
ce8add2895 | ||
|
|
88e1e5b770 | ||
|
|
76ecbd3497 | ||
|
|
e33c9b932f | ||
|
|
038f6caa04 | ||
|
|
36f5249d71 | ||
|
|
570702a5bd | ||
|
|
5d03c9c644 | ||
|
|
83906def4a | ||
|
|
33e0f25fb1 | ||
|
|
2080de4139 | ||
|
|
decaca889c | ||
|
|
794a39032f | ||
|
|
cc3cf17060 | ||
|
|
b07da4bfa8 | ||
|
|
4f52a4ee79 | ||
|
|
cf77b999a3 | ||
|
|
c9f062bdd9 | ||
|
|
12669f7ea0 | ||
|
|
03451d2a6e | ||
|
|
b5d7595f87 | ||
|
|
0d3d760a43 | ||
|
|
1cfc86e5ce | ||
|
|
938343323d | ||
|
|
37112715c0 | ||
|
|
bc80c10193 | ||
|
|
d989f48226 | ||
|
|
45bcbb3a3d | ||
|
|
f4a74e0254 | ||
|
|
ace83cf68d | ||
|
|
209b9b9a6f | ||
|
|
0d9dd6a33e | ||
|
|
e4137a031e | ||
|
|
41e9ea7e4f | ||
|
|
1a8b88973d | ||
|
|
3109e251b9 | ||
|
|
452ab280f1 | ||
|
|
34ecfbb846 | ||
|
|
11fb97d275 | ||
|
|
4bb30d075b | ||
|
|
f38af55c05 | ||
|
|
8a007376cc | ||
|
|
940a1dfc78 | ||
|
|
cbd64d1602 | ||
|
|
955c9992d0 | ||
|
|
87606818cd | ||
|
|
ffcd264bac | ||
|
|
04c70cf99d | ||
|
|
0e0b6fca07 | ||
|
|
b24a789352 | ||
|
|
3d4f447037 | ||
|
|
7aa52ca00a | ||
|
|
a94c9050c5 | ||
|
|
9b18e71e12 | ||
|
|
d5ff048bd0 | ||
|
|
76f44f2573 | ||
|
|
9988f1a76f | ||
|
|
1559376bc2 | ||
|
|
4d0f252acd | ||
|
|
d5a84e66ad | ||
|
|
1dfe3453f4 | ||
|
|
a7dc9f31b9 | ||
|
|
d275f18b3c | ||
|
|
d770429401 | ||
|
|
501f0be99f | ||
|
|
a70122f873 | ||
|
|
1e1bd15a06 | ||
|
|
164fb1f4f5 | ||
|
|
0206514bdb | ||
|
|
ecf2b42fa4 | ||
|
|
df683aeea9 | ||
|
|
cdd05958f7 | ||
|
|
4ec5fc05fb | ||
|
|
4019cafac1 | ||
|
|
4333e179d1 | ||
|
|
87d67be428 | ||
|
|
350863adff | ||
|
|
d125fc46a0 | ||
|
|
5f4c079071 | ||
|
|
5ec576a76e | ||
|
|
9edb00ebbb | ||
|
|
59843e5464 | ||
|
|
4fe0ccf807 | ||
|
|
2ee012d3fe | ||
|
|
161b4cb5f5 | ||
|
|
f9bee68d71 | ||
|
|
e0d98aca1b | ||
|
|
d47518c00c | ||
|
|
58f97dc3ab | ||
|
|
c7706cb80c | ||
|
|
49bcc877ef | ||
|
|
4b81dd552e | ||
|
|
ff63aaf3f3 | ||
|
|
39cbd1477b | ||
|
|
3741cc558a | ||
|
|
2a314ca3c1 | ||
|
|
10f9564825 | ||
|
|
1ae87c9b18 | ||
|
|
24301ba462 | ||
|
|
ff6481274f | ||
|
|
866df585a2 | ||
|
|
416bec04a1 | ||
|
|
f747d6725e | ||
|
|
8846918e55 | ||
|
|
e347aefcd9 | ||
|
|
0a0676af3a | ||
|
|
7a69b63b4f | ||
|
|
fab5626741 | ||
|
|
c6832d6478 | ||
|
|
f451597746 | ||
|
|
a92e5b5156 | ||
|
|
786b3b26ea | ||
|
|
2e9fd2fd79 | ||
|
|
3be2b9893b | ||
|
|
7f6bed4f5f | ||
|
|
37322f284a | ||
|
|
4106bb1792 | ||
|
|
229db1242a | ||
|
|
8201b42135 | ||
|
|
24818575dc | ||
|
|
213050e7f2 | ||
|
|
1efaf4e3ea | ||
|
|
5532147992 | ||
|
|
550b43094d | ||
|
|
5e831837c8 | ||
|
|
480e88a088 | ||
|
|
81cbcbe8af | ||
|
|
5649634809 | ||
|
|
69a253f738 | ||
|
|
fbc8cfddb8 | ||
|
|
3e1024804c | ||
|
|
cf0b51be00 | ||
|
|
5b295a2a8f | ||
|
|
ebb3d7a149 | ||
|
|
73ab348d11 | ||
|
|
8ce17086f8 | ||
|
|
45cffc93b1 | ||
|
|
71cf4d5a34 | ||
|
|
b79a4154af | ||
|
|
ff818f8472 | ||
|
|
b540d32ca3 | ||
|
|
895a11d45f | ||
|
|
bdafae6132 | ||
|
|
f45d9ce6da | ||
|
|
7456e2c8f5 | ||
|
|
fc51b50fe6 | ||
|
|
b31aa26975 | ||
|
|
49801e1b14 | ||
|
|
1453464cbd | ||
|
|
b9be8ff13d | ||
|
|
9663c3cadd | ||
|
|
da5490bc0a | ||
|
|
df9b1b9fbb | ||
|
|
6a0a3ded13 | ||
|
|
ac7f4a08c3 | ||
|
|
e5ff5e3364 | ||
|
|
f0ac19fcc1 | ||
|
|
ce23d34923 | ||
|
|
bdbba87f5a | ||
|
|
fa8ef0477b | ||
|
|
3e9ec4cfb6 | ||
|
|
ad1c9d8129 | ||
|
|
7aaac4c13c | ||
|
|
a6c71c3d54 | ||
|
|
05714f38dc | ||
|
|
4ef5e4d8ae | ||
|
|
5fd0e1d6fe | ||
|
|
97faf5bef3 | ||
|
|
bd77128733 | ||
|
|
935debc6bf | ||
|
|
699ed19b6a | ||
|
|
126e5a94c2 | ||
|
|
792bd36ea7 | ||
|
|
04dfee1b43 | ||
|
|
6a08cfeea9 | ||
|
|
29a0f6c9f6 | ||
|
|
e2bdf26d82 | ||
|
|
1643949110 | ||
|
|
577dabd727 | ||
|
|
ed4bd0693f | ||
|
|
0f2982b34d | ||
|
|
0954094800 | ||
|
|
22dcfafc62 | ||
|
|
dd34051426 | ||
|
|
6feaf2cb8a | ||
|
|
603f64ea92 | ||
|
|
568f9ab0d0 | ||
|
|
a2ff1c33e6 | ||
|
|
84452e9fc0 | ||
|
|
5a6ae453cf | ||
|
|
cde3e3361d | ||
|
|
25f01192a4 | ||
|
|
bacb08ec65 | ||
|
|
36c905ee1c | ||
|
|
6b9280534a | ||
|
|
f94c6e850f | ||
|
|
f4cecf6906 | ||
|
|
a81d3766f8 |
100
CHANGELOG
100
CHANGELOG
@@ -1,4 +1,102 @@
|
|||||||
KeePassDX(2.5beta31)
|
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
|
||||||
|
* Fix entry in Magikeyboard memory
|
||||||
|
* Fix biometric view visibility
|
||||||
|
* Fix fields order
|
||||||
|
* Upgrade code with ViewModel and LiveData
|
||||||
|
|
||||||
|
KeePassDX(2.8.1)
|
||||||
|
* Capture exceptions in coroutines
|
||||||
|
|
||||||
|
KeePassDX(2.8)
|
||||||
|
* Fix TOTP period (> 60s)
|
||||||
|
* Fix searching in recycle bin
|
||||||
|
* Settings to back to the previous keyboard during database credentials and after form filling
|
||||||
|
* Improve action tasks
|
||||||
|
* Improve recognition to reset app timeout
|
||||||
|
* Fix minor issues
|
||||||
|
|
||||||
|
KeePassDX(2.7)
|
||||||
|
* Add blocklists for autofill
|
||||||
|
* Add autofill compatibility mode (usefull for Browser not compatible)
|
||||||
|
* Upgrade autofill recognition algorithm
|
||||||
|
* Setting to search through web subdomains
|
||||||
|
* Refactoring selection mode
|
||||||
|
|
||||||
|
KeePassDX(2.6)
|
||||||
|
* Share a web domain to automatically search for an entry
|
||||||
|
* Default group icon for a new entry
|
||||||
|
* Better autofill recognition
|
||||||
|
* Fix entry not visually deleted in search
|
||||||
|
* Fix hanged loading dialog
|
||||||
|
* Fix auto open biometric prompt if comes from background
|
||||||
|
* Minor fixes
|
||||||
|
|
||||||
|
KeePassDX(2.5)
|
||||||
|
* First stable version of KeePassDX
|
||||||
|
* Fork completely rewritten from the KeePassDroid project
|
||||||
|
* Fix small issues from the last Release Candidate
|
||||||
|
|
||||||
|
KeePassDX(2.5RC2)
|
||||||
|
* Replacement of Spongy Castle by Bouncy Castle
|
||||||
|
* Update Autofill compatibility
|
||||||
|
* Fix Magikeyboard "Go" action
|
||||||
|
* Fix KeeWeb database opening
|
||||||
|
* Fix default username
|
||||||
|
* Fix themes
|
||||||
|
* Fix small issues
|
||||||
|
|
||||||
|
KeePassDX(2.5RC1)
|
||||||
* Add write permission to keep compatibility with old file managers
|
* Add write permission to keep compatibility with old file managers
|
||||||
* Fix autofill for apps
|
* Fix autofill for apps
|
||||||
* Auto search for autofill
|
* Auto search for autofill
|
||||||
|
|||||||
177
LICENSES/LICENSE_ANDROID_BACKUP_SERVICE
Normal file
177
LICENSES/LICENSE_ANDROID_BACKUP_SERVICE
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
Terms of Service
|
||||||
|
|
||||||
|
This is the terms of service for the Android Backup Service.
|
||||||
|
|
||||||
|
1. Your relationship with Google
|
||||||
|
|
||||||
|
1.1 Your use of the Android Backup Service (referred to as the "Service" in this document) is subject to the terms of a legal agreement between you and Google. "Google" means Google LLC, whose principal place of business is at 1600 Amphitheatre Parkway, Mountain View, CA 94043, United States. This document explains how the agreement is made up, and sets out some of the terms of that agreement.
|
||||||
|
|
||||||
|
1.2 Unless otherwise agreed in writing with Google, your agreement with Google will always include, at a minimum, the terms and conditions set out in this document. These are referred to below as the "Terms".
|
||||||
|
|
||||||
|
1.3 The Terms form a legally binding agreement between you and Google in relation to your use of the Service. It is important that you take the time to read them carefully.
|
||||||
|
|
||||||
|
2. Accepting the Terms
|
||||||
|
|
||||||
|
2.1 In order to use the Service, you must first agree to the Terms. You may not use the Service if you do not accept the Terms.
|
||||||
|
|
||||||
|
2.2 You can accept the Terms by clicking to accept or agree to the Terms, where this option is made available to you by Google.
|
||||||
|
|
||||||
|
2.3 You may not use the Service and may not accept the Terms if you are not of legal age to form a binding contract with Google.
|
||||||
|
|
||||||
|
2.4 You represent that you have full power, capacity and authority to accept these Terms. If you are accepting on behalf of your employer or another entity, you represent that you have full legal authority to bind your employer or such entity to these Terms. If you don't have the legal authority to bind, please ensure that an authorized person from your entity consents to and accepts these Terms.
|
||||||
|
|
||||||
|
3. Provision of the Service by Google
|
||||||
|
|
||||||
|
3.1 Google has subsidiaries and affiliated legal entities around the world ("Subsidiaries and Affiliates"). Sometimes, these companies will be providing the Service to you on behalf of Google itself. You acknowledge and agree that Subsidiaries and Affiliates will be entitled to provide the Service to you.
|
||||||
|
|
||||||
|
3.2 Google is constantly innovating in order to provide the best possible experience for its users. You acknowledge and agree that the form and nature of the Service which Google provides may change from time to time without prior notice to you.
|
||||||
|
|
||||||
|
3.3 As part of this continuing innovation, you acknowledge and agree that Google may stop (permanently or temporarily) providing the Service (or any features within the Service) to you or to users generally at Google's sole discretion, without prior notice to you. You may stop using the Service at any time. You do not need to specifically inform Google when you stop using the Service.
|
||||||
|
|
||||||
|
3.4 You acknowledge and agree that if Google disables your Backup Service Key, you and the Android application(s) you developed ("Application(s)") may be prevented from accessing the Service and any content that is stored with the Service.
|
||||||
|
|
||||||
|
3.5 You acknowledge and agree that Google may set a fixed upper limit on the number of backup transmissions you may send or receive through the Service or on the amount of storage space used for the provision of the Service at any time, at Google's discretion. You agree to abide by any such fixed upper limits.
|
||||||
|
|
||||||
|
4. Use of the Service by you
|
||||||
|
|
||||||
|
4.1 In order to access the Service, you must have a unique application identifier ("Package Name") for your Application as described in the documentation for the Service.
|
||||||
|
|
||||||
|
4.2 After supplying Google with the Package Name and accepting the Terms, you will be issued an alphanumeric key ("Backup Service Key") assigned to you by Google that is uniquely associated with your Application. Your Application must include this Backup Service Key as described in the documentation for the Service.
|
||||||
|
|
||||||
|
4.3 There is currently no limit to the number of Backup Service Keys you may obtain in this manner provided that you use a different Package Name for each Backup Service Key you obtain. You agree that each Backup Service Key is only valid for Applications with the corresponding Package Name. You agree that Google may, in its sole discretion, impose a limit on the number of Backup Service Keys that may be obtained in the future. You agree that your continued use of any of the Backup Service Keys assigned by Google, or distribution of any Applications using such Backup Service Keys, constitutes your continued agreement to these Terms.
|
||||||
|
|
||||||
|
4.4 You agree to use the Service only for purposes that are permitted by (a) the Terms and (b) any applicable law, regulation, third-party terms of service, or generally accepted practices or guidelines in the relevant jurisdictions (including any laws regarding the export of data or software to and from the United States or other relevant countries).
|
||||||
|
|
||||||
|
4.5 You agree not to access (or attempt to access) any of the Service by any means other than through the interfaces, methods, and APIs that are provided by Google, unless you have been specifically allowed to do so in a separate agreement with Google.
|
||||||
|
|
||||||
|
4.6 You agree that you will not engage in any activity that interferes with or disrupts the Service (or the servers and networks which are connected to the Service), or the servers or networks of any third-party.
|
||||||
|
|
||||||
|
4.7 You agree that your use of the Service will be in compliance with any documentation guidelines provided by Google and that failure to comply with the documentation guidelines may result in the disabling of the Backup Service Key(s) for your Application(s).
|
||||||
|
|
||||||
|
4.8 Unless you have been specifically permitted to do so in a separate agreement with Google, you agree that you will not reproduce, duplicate, copy, sell, trade or resell (a) use of the Service, or (b) access to the Service.
|
||||||
|
|
||||||
|
4.9 You agree that you are solely responsible for (and that Google has no responsibility to you or to any third party for) your and your Application's use of the Service, any breach of your obligations under the Terms, and for the consequences (including any loss or damage which Google may suffer) of any such breach.
|
||||||
|
|
||||||
|
4.10 You agree that in your use of the Service, you and your Applications will protect the privacy and legal rights of users. You must provide legally adequate privacy notice and protection for users whose data your Applications back up to the Service. Further, your Application may only use that information for the limited purpose of backing up the data to the Service unless the user has given you permission for further use. If the user has not given you permission to back up information to the Service, you may not transmit such information to the Service.
|
||||||
|
|
||||||
|
4.11 You agree that you and your Applications will not transmit or store sensitive user information, such as user names, passwords, or credit card numbers, through the Service.
|
||||||
|
|
||||||
|
5. Security
|
||||||
|
|
||||||
|
5.1 You agree and understand that you are responsible for maintaining the security associated with any information you provide to access the Service as well as of the Backup Service Key(s) assigned to you by Google. You agree that only you are authorized to use the Backup Service Key(s) assigned to you.
|
||||||
|
|
||||||
|
5.2 Accordingly, you agree that you will be solely responsible to Google for all activities that occur in connection with your access to the Service, as well as the Backup Service Key.
|
||||||
|
|
||||||
|
5.3 If you become aware of any unauthorized use of your Backup Service Key(s) you agree to notify Google immediately.
|
||||||
|
|
||||||
|
6. Privacy and your personal information
|
||||||
|
|
||||||
|
6.1 For information about Google's data protection practices, please read Google's privacy policy at http://www.google.com/privacy.html. This policy explains how Google treats your personal information when you use the Service.
|
||||||
|
|
||||||
|
6.2 You agree to the use of your data in accordance with Google's privacy policies.
|
||||||
|
|
||||||
|
7. Content in the Service
|
||||||
|
|
||||||
|
7.1 You agree that you are solely responsible for (and that Google has no responsibility to you or to any third party for) any Content that you or your Applications transmit or store through the Service and for the consequences of your actions (including any loss or damage which Google may suffer) by doing so. You agree that you are solely responsible for (A) any Content that is transmitted through the Service by your Applications, and (B) any Content that Devices retrieve from the Service by virtue of your Applications. For purposes of the Terms, "Content" means information such as data, messages, settings information, written text, computer software, music, audio files or other sounds, photographs, videos or other images. "Device(s)" means device(s) powered by the Android operating system.
|
||||||
|
|
||||||
|
7.2 You agree that you will not transmit any Content through the Service that is copyrighted, protected by trade secret or otherwise subject to third party proprietary rights, including patent, privacy and publicity rights, unless you are the owner of such rights or have permission from their rightful owner to transmit the Content through the Service.
|
||||||
|
|
||||||
|
8. Proprietary rights
|
||||||
|
|
||||||
|
8.1 You acknowledge and agree that Google (or Google's licensors) own all legal right, title and interest in and to the Service, including any intellectual property rights which subsist in the Service (whether those rights happen to be registered or not, and wherever in the world those rights may exist).
|
||||||
|
|
||||||
|
8.2 Unless you have agreed otherwise in writing with Google, nothing in the Terms gives you a right to use any of Google's trade names, trademarks, service marks, logos, domain names, and other distinctive brand features.
|
||||||
|
|
||||||
|
8.3 If you have been given an explicit right to use any of these brand features in a separate written agreement with Google, then you agree that your use of such features shall be in compliance with that agreement, any applicable provisions of the Terms, and Google's brand feature use guidelines as updated from time to time. These guidelines can be viewed online at http://www.google.com/permissions/ guidelines.html (or such other URL as Google may provide for this purpose from time to time).
|
||||||
|
|
||||||
|
8.4 You agree that you shall not remove, obscure, or alter any proprietary rights notices (including copyright, trade mark notices) which may be affixed to or contained within the Service.
|
||||||
|
|
||||||
|
9. License from Google
|
||||||
|
|
||||||
|
9.1 Subject to terms and conditions of these Terms, Google gives you a personal, worldwide, royalty-free, non-assignable and non-exclusive license to use the Service as provided to you by Google. This license is for the sole purpose of enabling you to use and enjoy the benefit of the Service as provided by Google, in the manner permitted by the Terms.
|
||||||
|
|
||||||
|
9.2 You may not (and you may not permit anyone else to) copy, modify, create a derivative work of, reverse engineer, decompile or otherwise attempt to extract the source code from the Service or any part thereof, unless this is expressly permitted or required by law, or unless you have been specifically told that you may do so by Google, in writing.
|
||||||
|
|
||||||
|
9.3 Unless Google has given you specific written permission to do so, you may not assign (or grant a sub-license of) your rights to use the Service, grant a security interest in or over your rights to use the Service, or otherwise transfer any part of your rights to use the Service.
|
||||||
|
|
||||||
|
10. Your code
|
||||||
|
|
||||||
|
10.1 Google claims no ownership or control over any source code written by you to be used with the Service. You retain copyright and any other rights you already hold in this code, and you are responsible for protecting those rights, as appropriate.
|
||||||
|
|
||||||
|
11. Ending your relationship with Google
|
||||||
|
|
||||||
|
11.1 The Terms will continue to apply until terminated by either you or Google as set out below.
|
||||||
|
|
||||||
|
11.2 You may terminate your legal agreement with Google by discontinuing your use of the Service at any time.
|
||||||
|
|
||||||
|
11.3 Google may, at any time, terminate its legal agreement with you if:
|
||||||
|
|
||||||
|
(A) you have breached any provision of the Terms (or have acted in manner which clearly shows that you do not intend to, or are unable to comply with the provisions of the Terms); or
|
||||||
|
|
||||||
|
(B) Google is required to do so by law (for example, where the provision of the Service to you is, or becomes, unlawful); or
|
||||||
|
|
||||||
|
(C) Google is transitioning to no longer providing the Service; or
|
||||||
|
|
||||||
|
(D) your Application fails to meet the documentation guidelines provided by Google.
|
||||||
|
|
||||||
|
11.4 Nothing in this Section shall affect Google's rights regarding provision of the Service under Section 3 of the Terms.
|
||||||
|
|
||||||
|
11.5 When these Terms come to an end, all of the legal rights, obligations and liabilities that you and Google have benefited from, been subject to (or which have accrued over time whilst the Terms have been in force) or which are expressed to continue indefinitely, shall be unaffected by this cessation, and the provisions of Sections 12, 13 and Paragraph 16 shall continue to apply to such rights, obligations and liabilities indefinitely.
|
||||||
|
|
||||||
|
12. EXCLUSION OF WARRANTIES
|
||||||
|
|
||||||
|
12.1 NOTHING IN THESE TERMS, INCLUDING SECTIONS 12 AND 13, SHALL EXCLUDE OR LIMIT GOOGLE'S WARRANTY OR LIABILITY FOR LOSSES WHICH MAY NOT BE LAWFULLY EXCLUDED OR LIMITED BY APPLICABLE LAW. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR CONDITIONS OR THE LIMITATION OR EXCLUSION OF LIABILITY FOR LOSS OR DAMAGE CAUSED BY NEGLIGENCE, BREACH OF CONTRACT OR BREACH OF IMPLIED TERMS, OR INCIDENTAL OR CONSEQUENTIAL DAMAGES. ACCORDINGLY, ONLY THE LIMITATIONS WHICH ARE LAWFUL IN YOUR JURISDICTION WILL APPLY TO YOU AND OUR LIABILITY WILL BE LIMITED TO THE MAXIMUM EXTENT PERMITTED BY LAW.
|
||||||
|
|
||||||
|
12.2 YOU EXPRESSLY UNDERSTAND AND AGREE THAT YOUR USE OF THE SERVICE IS AT YOUR SOLE RISK AND THAT THE SERVICE AND CONTENT ARE PROVIDED "AS IS" AND "AS AVAILABLE".
|
||||||
|
|
||||||
|
12.3 IN PARTICULAR, GOOGLE, ITS SUBSIDIARIES AND AFFILIATES, AND ITS LICENSORS DO NOT REPRESENT OR WARRANT TO YOU THAT:
|
||||||
|
|
||||||
|
(A) YOUR USE OF THE SERVICE WILL MEET YOUR REQUIREMENTS,
|
||||||
|
|
||||||
|
(B) YOUR USE OF THE SERVICE WILL BE UNINTERRUPTED, TIMELY, SECURE OR FREE FROM ERROR, AND
|
||||||
|
|
||||||
|
(C) THAT DEFECTS IN THE OPERATION OR FUNCTIONALITY OF ANY SOFTWARE PROVIDED TO YOU AS PART OF THE SERVICE WILL BE CORRECTED.
|
||||||
|
|
||||||
|
12.4 NO ADVICE OR INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED BY YOU FROM GOOGLE OR THROUGH OR FROM THE SERVICE SHALL CREATE ANY WARRANTY NOT EXPRESSLY STATED IN THE TERMS.
|
||||||
|
|
||||||
|
12.5 GOOGLE FURTHER EXPRESSLY DISCLAIMS ALL WARRANTIES AND CONDITIONS OF ANY KIND, WHETHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
|
||||||
|
|
||||||
|
13. LIMITATION OF LIABILITY
|
||||||
|
|
||||||
|
13.1 SUBJECT TO OVERALL PROVISION IN PARAGRAPH 12.1 ABOVE, YOU EXPRESSLY UNDERSTAND AND AGREE THAT GOOGLE, ITS SUBSIDIARIES AND AFFILIATES, AND ITS LICENSORS SHALL NOT BE LIABLE TO YOU FOR:
|
||||||
|
|
||||||
|
(A) ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL CONSEQUENTIAL OR EXEMPLARY DAMAGES WHICH MAY BE INCURRED BY YOU, HOWEVER CAUSED AND UNDER ANY THEORY OF LIABILITY. THIS SHALL INCLUDE, BUT NOT BE LIMITED TO, ANY LOSS OF PROFIT (WHETHER INCURRED DIRECTLY OR INDIRECTLY), ANY LOSS OF GOODWILL OR BUSINESS REPUTATION, ANY LOSS OF DATA SUFFERED, COST OF PROCUREMENT OF SUBSTITUTE GOODS OR SERVICE, OR OTHER INTANGIBLE LOSS;
|
||||||
|
|
||||||
|
(B) ANY LOSS OR DAMAGE WHICH MAY BE INCURRED BY YOU, INCLUDING BUT NOT LIMITED TO LOSS OR DAMAGE AS A RESULT OF:
|
||||||
|
|
||||||
|
(I) ANY CHANGES WHICH GOOGLE MAY MAKE TO THE SERVICE, OR FOR ANY PERMANENT OR TEMPORARY CESSATION IN THE PROVISION OF THE SERVICE (OR ANY FEATURES WITHIN THE SERVICE);
|
||||||
|
|
||||||
|
(II) THE DELETION OF, CORRUPTION OF, OR FAILURE TO STORE, ANY CONTENT AND OTHER COMMUNICATIONS DATA MAINTAINED OR TRANSMITTED BY OR THROUGH YOUR USE OF THE SERVICE;
|
||||||
|
|
||||||
|
(III) YOUR FAILURE TO PROVIDE GOOGLE WITH ACCURATE ACCOUNT INFORMATION; OR
|
||||||
|
|
||||||
|
(IV) YOUR FAILURE TO KEEP YOUR PASSWORD OR ACCOUNT DETAILS SECURE AND CONFIDENTIAL.
|
||||||
|
|
||||||
|
13.2 THE LIMITATIONS ON GOOGLE'S LIABILITY TO YOU IN PARAGRAPH 13.1 ABOVE SHALL APPLY WHETHER OR NOT GOOGLE HAS BEEN ADVISED OF OR SHOULD HAVE BEEN AWARE OF THE POSSIBILITY OF ANY SUCH LOSSES ARISING.
|
||||||
|
|
||||||
|
14. Indemnification
|
||||||
|
|
||||||
|
14.1 You agree to hold harmless and indemnify Google, and its subsidiaries, affiliates, officers, agents, employees, or licensors from and against any third party claim arising from or in any way related to (a) your breach of the Terms, (b) your use of the Service, or (c) your violation of applicable laws, rules or regulations in connection with the Service, including any liability or expense arising from all claims, losses, damages (actual and consequential), suits, judgments, litigation costs and attorneys' fees, of every kind and nature. In such a case, Google will provide you with written notice of such claim, suit or action.
|
||||||
|
|
||||||
|
15. Changes to the Terms
|
||||||
|
|
||||||
|
15.1 Due to things like changes to the law or changes to functionality offered through the Service, Google may need to change these Terms from time to time. You should look at the Terms regularly. We'll post notice of the modified Terms within, or through, the Service. Once the modified Terms are posted, the changes will become effective immediately, and you are deemed to have accepted the modified Terms if you continue to use the Service. If you do not agree to the modified Terms for the Service, please stop using the Service.
|
||||||
|
|
||||||
|
16. General legal terms
|
||||||
|
|
||||||
|
16.1 The Terms constitute the whole legal agreement between you and Google and govern your use of the Service (but excluding any service which Google may provide to you under a separate written agreement), and completely replace any prior agreements between you and Google in relation to the Service.
|
||||||
|
|
||||||
|
16.2 You agree that Google may provide you with notices, including those regarding changes to the Terms, by email, regular mail, or postings on the Service.
|
||||||
|
|
||||||
|
16.3 You agree that if Google does not exercise or enforce any legal right or remedy which is contained in the Terms (or which Google has the benefit of under any applicable law), this will not be taken to be a formal waiver of Google's rights and that those rights or remedies will still be available to Google.
|
||||||
|
|
||||||
|
16.4 If any court of law, having the jurisdiction to decide on this matter, rules that any provision of these Terms is invalid, then that provision will be removed from the Terms without affecting the rest of the Terms. The remaining provisions of the Terms will continue to be valid and enforceable.
|
||||||
|
|
||||||
|
16.5 You acknowledge and agree that each member of the group of companies of which Google is the parent shall be third party beneficiaries to the Terms and that such other companies shall be entitled to directly enforce, and rely upon, any provision of the Terms which confers a benefit on (or rights in favor of) them. Other than this, no other person or company shall be third party beneficiaries to the Terms.
|
||||||
|
|
||||||
|
16.6 The Terms, and your relationship with Google under the Terms, shall be governed by the laws of the State of California without regard to its conflict of laws provisions. You and Google agree to submit to the exclusive jurisdiction of the courts located within the county of Santa Clara, California to resolve any legal matter arising from the Terms. Notwithstanding this, you agree that Google shall still be allowed to apply for injunctive remedies (or an equivalent type of urgent legal relief) in any jurisdiction.
|
||||||
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.
|
||||||
91
README.md
Normal file
91
README.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# Android KeepassDX
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> KeepassDX is a **multi-format KeePass manager for Android devices**. The app allows creating keys and passwords in a secure way by integrating with the Android design standards.
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Create database files / entries and groups.
|
||||||
|
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
|
||||||
|
- **Compatible** with the majority of alternative programs (KeePass, KeePassX, KeePassXC, …).
|
||||||
|
- Allows opening and **copying URI / URL fields quickly**.
|
||||||
|
- **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*.
|
||||||
|
- **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA).
|
||||||
|
- Material design with **themes**.
|
||||||
|
- **Auto-Fill** and Integration.
|
||||||
|
- Field filling **keyboard**.
|
||||||
|
- **History** of each entry.
|
||||||
|
- Precise management of **settings**.
|
||||||
|
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
|
||||||
|
|
||||||
|
KeepassDX is **open source** and **ad-free**.
|
||||||
|
|
||||||
|
## What is KeePassDX?
|
||||||
|
|
||||||
|
An alternative to remembering an endless list of passwords manually. This is made more difficult by **using different passwords for each account**. If you use one password everywhere and security fails only one of those places, it grants access to your e-mail account, website, etc, and you may not know about it or notice, before bad things happen.
|
||||||
|
|
||||||
|
KeePassDX is a **password manager for Android**, which helps you **manage your passwords in a secure way**. You can put all your passwords in one database, locked with a **master key** and/or a **keyfile**. You **only have to remember one single master password and/or select the keyfile** to unlock the whole database. The databases are encrypted using the best and **most secure encryption algorithms** currently known.
|
||||||
|
|
||||||
|
## Small print?
|
||||||
|
|
||||||
|
KeePassDX is under **open source GPL3 license**, meaning you can use, study, change and share it at will. Copyleft ensures it stays that way.
|
||||||
|
From the full source, anyone can build, fork, and check whether for example the encryption algorithms are implemented correctly.
|
||||||
|
There is **no advertising**.
|
||||||
|
|
||||||
|
Do not worry, **the main features remain completely free**.
|
||||||
|
|
||||||
|
Optional visual styles are accessible after a contribution (and a congratulatory message (Ո‿Ո) ) or the purchase of an extended version to encourage contribution to the work of open source projects!
|
||||||
|
*If you contribute to the project and do not have access to the styles, do not hesitate to contact the author at [contact@kunzisoft.com](contact@kunzisoft.com).*
|
||||||
|
|
||||||
|
## Contributions
|
||||||
|
|
||||||
|
* Add features by making a **[pull request](https://help.github.com/articles/about-pull-requests/)**.
|
||||||
|
* Help to **[translate](https://hosted.weblate.org/projects/keepass-dx/strings/)** KeePassDX to your language (on [Weblate](https://hosted.weblate.org/projects/keepass-dx/) or by sending a [pull request](https://help.github.com/articles/about-pull-requests/)).
|
||||||
|
* **[Donate](https://www.kunzisoft.com/donation)** 人◕ ‿‿ ◕人Y for a better service and a quick development of your features.
|
||||||
|
* Buy the **[Pro version](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro)** of KeePassDX.
|
||||||
|
|
||||||
|
## Download
|
||||||
|
|
||||||
|
*[F-Droid](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/) is the recommended way of installing, a libre software project that verifies that all the libraries and app code is libre software.*
|
||||||
|
|
||||||
|
[<img src="https://f-droid.org/badge/get-it-on.png"
|
||||||
|
alt="Get it on F-Droid"
|
||||||
|
height="80">](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/)
|
||||||
|
|
||||||
|
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
|
||||||
|
alt="Get it on Google Play"
|
||||||
|
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
|
||||||
|
|
||||||
|
## Frequently Asked Questions
|
||||||
|
|
||||||
|
Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/wiki/FAQ)
|
||||||
|
|
||||||
|
## Other devices
|
||||||
|
|
||||||
|
- [KeePass](https://keepass.info/) (https://keepass.info/) is the original and official project for the desktop, with technical documentation for standardized database files. It is updated regularly with active maintenance (written in C#).
|
||||||
|
|
||||||
|
- [KeePassXC](https://keepassxc.org/) (https://keepassxc.org/) is an alternative integration of KeePass written in C++.
|
||||||
|
|
||||||
|
- [KeeWeb](https://keeweb.info/) (https://keeweb.info/) is a web version that is also compatible with KeePass files.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Copyright © 2020 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||||
|
|
||||||
|
This file is part of KeePassDX.
|
||||||
|
|
||||||
|
[KeePassDX](https://www.keepassdx.com) is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
*This project is a fork of [KeepassDroid](https://github.com/bpellin/keepassdroid) by bpellin.*
|
||||||
87
ReadMe.md
87
ReadMe.md
@@ -1,87 +0,0 @@
|
|||||||
# Android KeepassDX
|
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> KeepassDX is a **multi-format KeePass manager for Android devices**. The application allows to create keys and passwords in a secure way by integrating with the Android design standards.
|
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Create database files / entries and groups
|
|
||||||
* Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm
|
|
||||||
* **Compatible** with the majority of alternative programs (KeePass, KeePassX, KeePassXC...)
|
|
||||||
* Allows **fast copy** of fields and opening of URI / URL
|
|
||||||
* **Biometric recognition** for fast unlocking *(Fingerprint / Face unlock / ...)*
|
|
||||||
* **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA)
|
|
||||||
* Material design with **themes**
|
|
||||||
* **AutoFill** and Integration
|
|
||||||
* Field filling **keyboard**
|
|
||||||
* Precise management of **settings**
|
|
||||||
* Code written in **native language** *(Kotlin / Java / JNI / C)*
|
|
||||||
|
|
||||||
KeepassDX is **open source** and **ad-free**.
|
|
||||||
|
|
||||||
## What is KeePassDX?
|
|
||||||
|
|
||||||
Today you need to remember many passwords. You need a password for your e-mail account, your website's FTP password, online passwords (like website member account), etc. etc. etc. The list is endless. Also, you **should use different passwords for each account**. Because if you use only one password everywhere and someone gets this password you have a problem... A serious problem. The thief would have access to your e-mail account, website, etc. Unimaginable.
|
|
||||||
|
|
||||||
KeePassDX is a **free open source password manager for Android**, which helps you to **manage your passwords in a secure way**. You can put all your passwords in one database, which is locked with one **master key** or a **key file**. So you **only have to remember one single master password or select the key file** to unlock the whole database. The databases are encrypted using the best and **most secure encryption algorithms** currently known.
|
|
||||||
|
|
||||||
## Is it really free?
|
|
||||||
|
|
||||||
Yes, KeePassDX is under **free license (OSI certified)** and **without advertising**. You can have a look at its full source and check whether the encryption algorithms are implemented correctly.
|
|
||||||
|
|
||||||
*Note : If you access the application from a store, visual styles may not be available to encourage contribution to the work of open source projects. These optional styles are accessible after a contribution (and a congratulation message :) or the purchase of an extended version, but do not worry, the main features remain completely free. If you contribute to the project and you do not have access to the themes, do not hesitate to contact me at [contact@kunzisoft.com](contact@kunzisoft.com), I will give you the procedure.*
|
|
||||||
|
|
||||||
## Contributions
|
|
||||||
|
|
||||||
You can contribute in different ways to help us on our work.
|
|
||||||
|
|
||||||
* Add features by a **[pull request](https://help.github.com/articles/about-pull-requests/)**.
|
|
||||||
* Help to **[translate](https://hosted.weblate.org/projects/keepass-dx/strings/)** into your language (By using [Weblate](https://hosted.weblate.org/projects/keepass-dx/) or with a manual [pull request](https://help.github.com/articles/about-pull-requests/))
|
|
||||||
* **[Donate](https://www.kunzisoft.com/donation)** 人◕ ‿‿ ◕人Y for a better service and a quick development of your features.
|
|
||||||
* Buy the **[Pro version](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro)** of KeePassDX
|
|
||||||
|
|
||||||
## Download
|
|
||||||
|
|
||||||
*We recommend the installation from [F-Droid](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/) which verifies that all libraries and application code are open source.*
|
|
||||||
|
|
||||||
[<img src="https://f-droid.org/badge/get-it-on.png"
|
|
||||||
alt="Get it on F-Droid"
|
|
||||||
height="80">](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/)
|
|
||||||
|
|
||||||
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
|
|
||||||
alt="Get it on Google Play"
|
|
||||||
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
|
|
||||||
|
|
||||||
## Frequently Asked Questions
|
|
||||||
|
|
||||||
Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/wiki/FAQ)
|
|
||||||
|
|
||||||
## Other devices
|
|
||||||
|
|
||||||
- [KeePass](https://keepass.info/) (https://keepass.info/) is the original and official project for desktop, with technical documentation for standardized database files. It is updated regularly with active maintenance (written in C#).
|
|
||||||
|
|
||||||
- [KeePassXC](https://keepassxc.org/) (https://keepassxc.org/) is an alternative integration to KeePass written in C++.
|
|
||||||
|
|
||||||
- [KeeWeb](https://keeweb.info/) (https://keeweb.info/) is a web version also compatible with KeePass files.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Copyright (c) 2020 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
|
||||||
|
|
||||||
This file is part of KeePassDX.
|
|
||||||
|
|
||||||
[KeePassDX](https://www.keepassdx.com) is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
*This project is a fork of [KeepassDroid](https://github.com/bpellin/keepassdroid) by bpellin.*
|
|
||||||
@@ -4,21 +4,22 @@ apply plugin: 'kotlin-android-extensions'
|
|||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 29
|
compileSdkVersion 30
|
||||||
buildToolsVersion '29.0.3'
|
buildToolsVersion '30.0.2'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 14
|
minSdkVersion 14
|
||||||
targetSdkVersion 29
|
targetSdkVersion 30
|
||||||
versionCode = 31
|
versionCode = 45
|
||||||
versionName = "2.5RC1"
|
versionName = "2.9.1"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
testInstrumentationRunner = "android.test.InstrumentationTestRunner"
|
testInstrumentationRunner = "android.test.InstrumentationTestRunner"
|
||||||
|
|
||||||
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
|
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
|
||||||
|
manifestPlaceholders = [ googleAndroidBackupAPIKey:"" ]
|
||||||
|
|
||||||
kapt {
|
kapt {
|
||||||
arguments {
|
arguments {
|
||||||
@@ -41,35 +42,36 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dexOptions {
|
flavorDimensions "version"
|
||||||
}
|
|
||||||
|
|
||||||
flavorDimensions "tier"
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
libre {
|
libre {
|
||||||
|
dimension "version"
|
||||||
applicationIdSuffix = ".libre"
|
applicationIdSuffix = ".libre"
|
||||||
buildConfigField "String", "BUILD_VERSION", "\"libre\""
|
buildConfigField "String", "BUILD_VERSION", "\"libre\""
|
||||||
buildConfigField "boolean", "FULL_VERSION", "true"
|
buildConfigField "boolean", "FULL_VERSION", "true"
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "false"
|
buildConfigField "boolean", "CLOSED_STORE", "false"
|
||||||
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Dark\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||||
|
|
||||||
}
|
}
|
||||||
pro {
|
pro {
|
||||||
|
dimension "version"
|
||||||
applicationIdSuffix = ".pro"
|
applicationIdSuffix = ".pro"
|
||||||
buildConfigField "String", "BUILD_VERSION", "\"pro\""
|
buildConfigField "String", "BUILD_VERSION", "\"pro\""
|
||||||
buildConfigField "boolean", "FULL_VERSION", "true"
|
buildConfigField "boolean", "FULL_VERSION", "true"
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "true"
|
buildConfigField "boolean", "CLOSED_STORE", "true"
|
||||||
buildConfigField "String[]", "STYLES_DISABLED", "{}"
|
buildConfigField "String[]", "STYLES_DISABLED", "{}"
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||||
|
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIZiXvrQCzSV9LNI6-p7cjTKENZLHIrz_zaqZuQQ" ]
|
||||||
}
|
}
|
||||||
free {
|
free {
|
||||||
|
dimension "version"
|
||||||
applicationIdSuffix = ".free"
|
applicationIdSuffix = ".free"
|
||||||
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
||||||
buildConfigField "boolean", "FULL_VERSION", "false"
|
buildConfigField "boolean", "FULL_VERSION", "false"
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "true"
|
buildConfigField "boolean", "CLOSED_STORE", "true"
|
||||||
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Dark\",\"KeepassDXStyle_Blue\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Blue\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||||
|
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,41 +85,46 @@ android {
|
|||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def spongycastleVersion = "1.58.0.0"
|
|
||||||
def room_version = "2.2.5"
|
def room_version = "2.2.5"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||||
implementation 'androidx.preference:preference:1.1.0'
|
implementation 'androidx.preference:preference:1.1.1'
|
||||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.3'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'androidx.biometric:biometric:1.0.1'
|
implementation 'androidx.biometric:biometric:1.1.0-beta01'
|
||||||
implementation "androidx.core:core-ktx:1.2.0"
|
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||||
// To upgrade with style
|
implementation "androidx.core:core-ktx:1.3.2"
|
||||||
|
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||||
|
// WARNING: To upgrade with style, bug in edit text
|
||||||
implementation 'com.google.android.material:material:1.0.0'
|
implementation 'com.google.android.material:material:1.0.0'
|
||||||
|
// Database
|
||||||
implementation "androidx.room:room-runtime:$room_version"
|
implementation "androidx.room:room-runtime:$room_version"
|
||||||
kapt "androidx.room:room-compiler:$room_version"
|
kapt "androidx.room:room-compiler:$room_version"
|
||||||
|
// Crypto
|
||||||
implementation "com.madgag.spongycastle:core:$spongycastleVersion"
|
implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01'
|
||||||
implementation "com.madgag.spongycastle:prov:$spongycastleVersion"
|
|
||||||
// Time
|
// Time
|
||||||
implementation 'joda-time:joda-time:2.9.9'
|
implementation 'joda-time:joda-time:2.10.6'
|
||||||
// Color
|
// Color
|
||||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.3'
|
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4'
|
||||||
// Education
|
// Education
|
||||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.12.0'
|
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
|
||||||
// Apache Commons Collections
|
// Apache Commons Collections
|
||||||
implementation 'commons-collections:commons-collections:3.2.1'
|
implementation 'commons-collections:commons-collections:3.2.2'
|
||||||
implementation 'org.apache.commons:commons-io:1.3.2'
|
|
||||||
// Apache Commons Codec
|
// Apache Commons Codec
|
||||||
implementation 'commons-codec:commons-codec:1.11'
|
implementation 'commons-codec:commons-codec:1.14'
|
||||||
// Icon pack
|
// Icon pack
|
||||||
implementation project(path: ':icon-pack-classic')
|
implementation project(path: ':icon-pack-classic')
|
||||||
implementation project(path: ':icon-pack-material')
|
implementation project(path: ':icon-pack-material')
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
androidTestImplementation 'junit:junit:4.13'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ import java.util.Random
|
|||||||
|
|
||||||
import junit.framework.TestCase
|
import junit.framework.TestCase
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.finalkey.AndroidFinalKey
|
import com.kunzisoft.keepass.crypto.finalkey.AndroidAESKeyTransformer
|
||||||
import com.kunzisoft.keepass.crypto.finalkey.NativeFinalKey
|
import com.kunzisoft.keepass.crypto.finalkey.NativeAESKeyTransformer
|
||||||
|
|
||||||
class FinalKeyTest : TestCase() {
|
class AESKeyTest : TestCase() {
|
||||||
private var mRand: Random? = null
|
private lateinit var mRand: Random
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun setUp() {
|
override fun setUp() {
|
||||||
@@ -40,29 +40,28 @@ class FinalKeyTest : TestCase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun testNativeAndroid() {
|
fun testAES() {
|
||||||
// Test both an old and an even number to test my flip variable
|
// Test both an old and an even number to test my flip variable
|
||||||
testNativeFinalKey(5)
|
testAESFinalKey(5)
|
||||||
testNativeFinalKey(6)
|
testAESFinalKey(6)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun testNativeFinalKey(rounds: Int) {
|
private fun testAESFinalKey(rounds: Long) {
|
||||||
val seed = ByteArray(32)
|
val seed = ByteArray(32)
|
||||||
val key = ByteArray(32)
|
val key = ByteArray(32)
|
||||||
val nativeKey: ByteArray
|
val nativeKey: ByteArray?
|
||||||
val androidKey: ByteArray
|
val androidKey: ByteArray?
|
||||||
|
|
||||||
mRand!!.nextBytes(seed)
|
mRand.nextBytes(seed)
|
||||||
mRand!!.nextBytes(key)
|
mRand.nextBytes(key)
|
||||||
|
|
||||||
val aKey = AndroidFinalKey()
|
val androidAESKey = AndroidAESKeyTransformer()
|
||||||
androidKey = aKey.transformMasterKey(seed, key, rounds.toLong())
|
androidKey = androidAESKey.transformMasterKey(seed, key, rounds)
|
||||||
|
|
||||||
val nKey = NativeFinalKey()
|
val nativeAESKey = NativeAESKeyTransformer()
|
||||||
nativeKey = nKey.transformMasterKey(seed, key, rounds.toLong())
|
nativeKey = nativeAESKey.transformMasterKey(seed, key, rounds)
|
||||||
|
|
||||||
assertArrayEquals("Does not match", androidKey, nativeKey)
|
assertArrayEquals("Does not match", androidKey, nativeKey)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,6 +80,4 @@ class AESTest : TestCase() {
|
|||||||
|
|
||||||
assertArrayEquals("Arrays differ on size: $dataSize", outAndroid, outNative)
|
assertArrayEquals("Arrays differ on size: $dataSize", outAndroid, outNative)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import junit.framework.TestCase
|
|||||||
import com.kunzisoft.keepass.stream.HashedBlockInputStream
|
import com.kunzisoft.keepass.stream.HashedBlockInputStream
|
||||||
import com.kunzisoft.keepass.stream.HashedBlockOutputStream
|
import com.kunzisoft.keepass.stream.HashedBlockOutputStream
|
||||||
|
|
||||||
class HashedBlock : TestCase() {
|
class HashedBlockTest : TestCase() {
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun testBlockAligned() {
|
fun testBlockAligned() {
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Brian Pellin, 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.tests.utils
|
|
||||||
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.utils.StringUtil
|
|
||||||
|
|
||||||
import junit.framework.TestCase
|
|
||||||
|
|
||||||
class StringUtilTest : TestCase() {
|
|
||||||
private val text = "AbCdEfGhIj"
|
|
||||||
private val search = "BcDe"
|
|
||||||
private val badSearch = "Ed"
|
|
||||||
|
|
||||||
private val repText = "AbCtestingaBc"
|
|
||||||
private val repSearch = "ABc"
|
|
||||||
private val repSearchBad = "CCCCCC"
|
|
||||||
private val repNew = "12345"
|
|
||||||
private val repResult = "12345testing12345"
|
|
||||||
|
|
||||||
fun testIndexOfIgnoreCase1() {
|
|
||||||
assertEquals(1, StringUtil.indexOfIgnoreCase(text, search, Locale.ENGLISH))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun testIndexOfIgnoreCase2() {
|
|
||||||
assertEquals(-1f, StringUtil.indexOfIgnoreCase(text, search, Locale.ENGLISH).toFloat(), 2f)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun testIndexOfIgnoreCase3() {
|
|
||||||
assertEquals(-1, StringUtil.indexOfIgnoreCase(text, badSearch, Locale.ENGLISH))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun testReplaceAllIgnoresCase1() {
|
|
||||||
assertEquals(repResult, StringUtil.replaceAllIgnoresCase(repText, repSearch, repNew, Locale.ENGLISH))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun testReplaceAllIgnoresCase2() {
|
|
||||||
assertEquals(repText, StringUtil.replaceAllIgnoresCase(repText, repSearchBad, repNew, Locale.ENGLISH))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.kunzisoft.keepass.tests.utils
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
|
import junit.framework.TestCase
|
||||||
|
|
||||||
|
class UnsignedIntTest: TestCase() {
|
||||||
|
|
||||||
|
fun testUInt() {
|
||||||
|
val standardInt = UnsignedInt(15).toKotlinInt()
|
||||||
|
assertEquals(15, standardInt)
|
||||||
|
val unsignedInt = UnsignedInt(-1).toKotlinLong()
|
||||||
|
assertEquals(4294967295L, unsignedInt)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testMaxValue() {
|
||||||
|
val maxValue = UnsignedInt.MAX_VALUE.toKotlinLong()
|
||||||
|
assertEquals(4294967295L, maxValue)
|
||||||
|
val longValue = UnsignedInt.fromKotlinLong(4294967295L).toKotlinLong()
|
||||||
|
assertEquals(longValue, maxValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testLong() {
|
||||||
|
val longValue = UnsignedInt.fromKotlinLong(50L).toKotlinInt()
|
||||||
|
assertEquals(50, longValue)
|
||||||
|
val uIntLongValue = UnsignedInt.fromKotlinLong(4294967290).toKotlinLong()
|
||||||
|
assertEquals(4294967290, uIntLongValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,28 +17,29 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests
|
package com.kunzisoft.keepass.tests.utils
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.ULONG_MAX_VALUE
|
|
||||||
import com.kunzisoft.keepass.stream.*
|
import com.kunzisoft.keepass.stream.*
|
||||||
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
|
import com.kunzisoft.keepass.utils.UnsignedLong
|
||||||
import junit.framework.TestCase
|
import junit.framework.TestCase
|
||||||
import org.junit.Assert.assertArrayEquals
|
import org.junit.Assert.assertArrayEquals
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class StringDatabaseKDBUtilsTest : TestCase() {
|
class ValuesTest : TestCase() {
|
||||||
|
|
||||||
fun testReadWriteLongZero() {
|
fun testReadWriteLongZero() {
|
||||||
testReadWriteLong(0.toByte())
|
testReadWriteLong(0.toByte())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteLongMax() {
|
fun testReadWriteLongMax() {
|
||||||
testReadWriteLong(java.lang.Byte.MAX_VALUE)
|
testReadWriteLong(Byte.MAX_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteLongMin() {
|
fun testReadWriteLongMin() {
|
||||||
testReadWriteLong(java.lang.Byte.MIN_VALUE)
|
testReadWriteLong(Byte.MIN_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteLongRnd() {
|
fun testReadWriteLongRnd() {
|
||||||
@@ -61,11 +62,11 @@ class StringDatabaseKDBUtilsTest : TestCase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteIntMin() {
|
fun testReadWriteIntMin() {
|
||||||
testReadWriteInt(java.lang.Byte.MIN_VALUE)
|
testReadWriteInt(Byte.MIN_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteIntMax() {
|
fun testReadWriteIntMax() {
|
||||||
testReadWriteInt(java.lang.Byte.MAX_VALUE)
|
testReadWriteInt(Byte.MAX_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun testReadWriteInt(value: Byte) {
|
private fun testReadWriteInt(value: Byte) {
|
||||||
@@ -77,11 +78,10 @@ class StringDatabaseKDBUtilsTest : TestCase() {
|
|||||||
|
|
||||||
setArray(orig, value, 4)
|
setArray(orig, value, 4)
|
||||||
|
|
||||||
val one = bytes4ToInt(orig)
|
val one = bytes4ToUInt(orig)
|
||||||
val dest = intTo4Bytes(one)
|
val dest = uIntTo4Bytes(one)
|
||||||
|
|
||||||
assertArrayEquals(orig, dest)
|
assertArrayEquals(orig, dest)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setArray(buf: ByteArray, value: Byte, size: Int) {
|
private fun setArray(buf: ByteArray, value: Byte, size: Int) {
|
||||||
@@ -103,11 +103,11 @@ class StringDatabaseKDBUtilsTest : TestCase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteShortMin() {
|
fun testReadWriteShortMin() {
|
||||||
testReadWriteShort(java.lang.Byte.MIN_VALUE)
|
testReadWriteShort(Byte.MIN_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteShortMax() {
|
fun testReadWriteShortMax() {
|
||||||
testReadWriteShort(java.lang.Byte.MAX_VALUE)
|
testReadWriteShort(Byte.MAX_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun testReadWriteShort(value: Byte) {
|
private fun testReadWriteShort(value: Byte) {
|
||||||
@@ -125,15 +125,15 @@ class StringDatabaseKDBUtilsTest : TestCase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteByteMin() {
|
fun testReadWriteByteMin() {
|
||||||
testReadWriteByte(java.lang.Byte.MIN_VALUE)
|
testReadWriteByte(Byte.MIN_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteByteMax() {
|
fun testReadWriteByteMax() {
|
||||||
testReadWriteShort(java.lang.Byte.MAX_VALUE)
|
testReadWriteShort(Byte.MAX_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun testReadWriteByte(value: Byte) {
|
private fun testReadWriteByte(value: Byte) {
|
||||||
val dest: Byte = uIntToByte(byteToUInt(value))
|
val dest: Byte = UnsignedInt(UnsignedInt.fromKotlinByte(value)).toKotlinByte()
|
||||||
assert(value == dest)
|
assert(value == dest)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +185,7 @@ class StringDatabaseKDBUtilsTest : TestCase() {
|
|||||||
|
|
||||||
val bos = ByteArrayOutputStream()
|
val bos = ByteArrayOutputStream()
|
||||||
val leos = LittleEndianDataOutputStream(bos)
|
val leos = LittleEndianDataOutputStream(bos)
|
||||||
leos.writeLong(ULONG_MAX_VALUE)
|
leos.writeLong(UnsignedLong.MAX_VALUE)
|
||||||
leos.close()
|
leos.close()
|
||||||
|
|
||||||
val uLongMax = bos.toByteArray()
|
val uLongMax = bos.toByteArray()
|
||||||
@@ -1,19 +1,30 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:viewportWidth="108"
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
android:viewportHeight="108"
|
android:viewportWidth="108"
|
||||||
android:width="108dp"
|
android:viewportHeight="108"
|
||||||
android:height="108dp">
|
android:width="108dp"
|
||||||
|
android:height="108dp">
|
||||||
<group
|
<group
|
||||||
android:translateY="-332">
|
android:translateY="-332">
|
||||||
<group
|
<group
|
||||||
android:translateY="332">
|
android:translateY="332">
|
||||||
<path
|
<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: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:strokeLineJoin="round"
|
||||||
android:strokeLineCap="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>
|
||||||
<group
|
<group
|
||||||
android:scaleX="0.3939503"
|
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,19 +1,30 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:viewportWidth="108"
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
android:viewportHeight="108"
|
android:viewportWidth="108"
|
||||||
android:width="108dp"
|
android:viewportHeight="108"
|
||||||
android:height="108dp">
|
android:width="108dp"
|
||||||
|
android:height="108dp">
|
||||||
<group
|
<group
|
||||||
android:translateY="-332">
|
android:translateY="-332">
|
||||||
<group
|
<group
|
||||||
android:translateY="332">
|
android:translateY="332">
|
||||||
<path
|
<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: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:strokeLineJoin="round"
|
||||||
android:strokeLineCap="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>
|
||||||
<group
|
<group
|
||||||
android:scaleX="0.3939503"
|
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"/>
|
android:name="android.permission.VIBRATE"/>
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.QUERY_ALL_PACKAGES"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
@@ -23,17 +25,15 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:name="com.kunzisoft.keepass.app.App"
|
android:name="com.kunzisoft.keepass.app.App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:fullBackupContent="@xml/backup"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
|
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:theme="@style/KeepassDXStyle.Night"
|
android:theme="@style/KeepassDXStyle.Night"
|
||||||
tools:targetApi="n">
|
tools:targetApi="n">
|
||||||
<!-- TODO backup API Key -->
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.backup.api_key"
|
android:name="com.google.android.backup.api_key"
|
||||||
android:value="" />
|
android:value="${googleAndroidBackupAPIKey}" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.FileDatabaseSelectActivity"
|
android:name="com.kunzisoft.keepass.activities.FileDatabaseSelectActivity"
|
||||||
android:theme="@style/KeepassDXStyle.SplashScreen"
|
android:theme="@style/KeepassDXStyle.SplashScreen"
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.PasswordActivity"
|
android:name="com.kunzisoft.keepass.activities.PasswordActivity"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize|stateUnchanged">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
@@ -126,33 +126,42 @@
|
|||||||
android:configChanges="keyboardHidden" />
|
android:configChanges="keyboardHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
|
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
|
||||||
android:configChanges="keyboardHidden"
|
android:windowSoftInputMode="adjustPan|stateAlwaysHidden" />
|
||||||
android:windowSoftInputMode="adjustResize" />
|
|
||||||
<!-- About and Settings -->
|
<!-- About and Settings -->
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:label="@string/about" />
|
android:label="@string/about" />
|
||||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
<activity
|
||||||
<activity android:name="com.kunzisoft.keepass.autofill.AutofillLauncherActivity"
|
android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
|
||||||
|
android:theme="@style/Theme.Transparent"
|
||||||
android:configChanges="keyboardHidden" />
|
android:configChanges="keyboardHidden" />
|
||||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
|
<activity
|
||||||
<activity android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
|
||||||
<activity android:name="com.kunzisoft.keepass.magikeyboard.KeyboardLauncherActivity"
|
<activity
|
||||||
android:label="@string/keyboard_name"
|
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
||||||
android:exported="true">
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
|
||||||
|
android:theme="@style/Theme.Transparent">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.MagikeyboardLauncherActivity"
|
||||||
|
android:theme="@style/Theme.Transparent" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
||||||
android:label="@string/keyboard_setting_label">
|
android:label="@string/keyboard_setting_label">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
|
||||||
android:name="com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false" />
|
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService"
|
android:name="com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
|
|||||||
Binary file not shown.
BIN
app/src/main/assets/publicsuffixes
Normal file
BIN
app/src/main/assets/publicsuffixes
Normal file
Binary file not shown.
@@ -0,0 +1,231 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentSender
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
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.autofillAllowedFor(searchInfo.applicationId,
|
||||||
|
PreferencesUtil.applicationIdBlocklist(this))
|
||||||
|
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
|
||||||
|
PreferencesUtil.webDomainBlocklist(this))) {
|
||||||
|
showBlockRestartMessage()
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
val database = Database.getInstance()
|
||||||
|
val readOnly = database.isReadOnly
|
||||||
|
// If database is open
|
||||||
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
|
Database.getInstance(),
|
||||||
|
searchInfo,
|
||||||
|
{ items ->
|
||||||
|
// Items found
|
||||||
|
AutofillHelper.buildResponse(this, items)
|
||||||
|
finish()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Show the database UI to select the entry
|
||||||
|
GroupActivity.launchForAutofillResult(this,
|
||||||
|
readOnly,
|
||||||
|
assistStructure,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// If database not open
|
||||||
|
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
||||||
|
assistStructure,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
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?)
|
||||||
|
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||||
|
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.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
@@ -39,14 +40,15 @@ import com.google.android.material.appbar.CollapsingToolbarLayout
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||||
import com.kunzisoft.keepass.model.AttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.EntryAttachment
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
|
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||||
@@ -86,10 +88,10 @@ class EntryActivity : LockingActivity() {
|
|||||||
private var mShowPassword: Boolean = false
|
private var mShowPassword: Boolean = false
|
||||||
|
|
||||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||||
private var mAttachmentsToDownload: HashMap<Int, EntryAttachment> = HashMap()
|
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
|
||||||
|
|
||||||
private var clipboardHelper: ClipboardHelper? = null
|
private var clipboardHelper: ClipboardHelper? = null
|
||||||
private var firstLaunchOfActivity: Boolean = false
|
private var mFirstLaunchOfActivity: Boolean = false
|
||||||
|
|
||||||
private var iconColor: Int = 0
|
private var iconColor: Int = 0
|
||||||
|
|
||||||
@@ -130,14 +132,17 @@ class EntryActivity : LockingActivity() {
|
|||||||
lockAndExit()
|
lockAndExit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Focus view to reinitialize timeout
|
||||||
|
resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout)
|
||||||
|
|
||||||
// Init the clipboard helper
|
// Init the clipboard helper
|
||||||
clipboardHelper = ClipboardHelper(this)
|
clipboardHelper = ClipboardHelper(this)
|
||||||
firstLaunchOfActivity = true
|
mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true
|
||||||
|
|
||||||
// Init attachment service binder manager
|
// Init attachment service binder manager
|
||||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||||
|
|
||||||
mProgressDialogThread?.onActionFinish = { actionTask, result ->
|
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
|
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
|
||||||
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
|
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
|
||||||
@@ -196,7 +201,7 @@ class EntryActivity : LockingActivity() {
|
|||||||
val entryInfo = entry.getEntryInfo(Database.getInstance())
|
val entryInfo = entry.getEntryInfo(Database.getInstance())
|
||||||
|
|
||||||
// Manage entry copy to start notification if allowed
|
// Manage entry copy to start notification if allowed
|
||||||
if (firstLaunchOfActivity) {
|
if (mFirstLaunchOfActivity) {
|
||||||
// Manage entry to launch copying notification if allowed
|
// Manage entry to launch copying notification if allowed
|
||||||
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
||||||
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
||||||
@@ -209,13 +214,13 @@ class EntryActivity : LockingActivity() {
|
|||||||
mAttachmentFileBinderManager?.apply {
|
mAttachmentFileBinderManager?.apply {
|
||||||
registerProgressTask()
|
registerProgressTask()
|
||||||
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
||||||
override fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment) {
|
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
||||||
entryContentsView?.updateAttachmentDownloadProgress(attachment)
|
entryContentsView?.putAttachment(entryAttachmentState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
firstLaunchOfActivity = false
|
mFirstLaunchOfActivity = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -237,14 +242,13 @@ class EntryActivity : LockingActivity() {
|
|||||||
toolbar?.title = entryTitle
|
toolbar?.title = entryTitle
|
||||||
|
|
||||||
// Assign basic fields
|
// Assign basic fields
|
||||||
entryContentsView?.assignUserName(entry.username)
|
entryContentsView?.assignUserName(entry.username) {
|
||||||
entryContentsView?.assignUserNameCopyListener(View.OnClickListener {
|
|
||||||
database.startManageEntry(entry)
|
database.startManageEntry(entry)
|
||||||
clipboardHelper?.timeoutCopyToClipboard(entry.username,
|
clipboardHelper?.timeoutCopyToClipboard(entry.username,
|
||||||
getString(R.string.copy_field,
|
getString(R.string.copy_field,
|
||||||
getString(R.string.entry_user_name)))
|
getString(R.string.entry_user_name)))
|
||||||
database.stopManageEntry(entry)
|
database.stopManageEntry(entry)
|
||||||
})
|
}
|
||||||
|
|
||||||
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
||||||
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
|
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
|
||||||
@@ -271,23 +275,25 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entryContentsView?.assignPassword(entry.password, allowCopyPasswordAndProtectedFields)
|
val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) {
|
||||||
if (allowCopyPasswordAndProtectedFields) {
|
View.OnClickListener {
|
||||||
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
|
|
||||||
database.startManageEntry(entry)
|
database.startManageEntry(entry)
|
||||||
clipboardHelper?.timeoutCopyToClipboard(entry.password,
|
clipboardHelper?.timeoutCopyToClipboard(entry.password,
|
||||||
getString(R.string.copy_field,
|
getString(R.string.copy_field,
|
||||||
getString(R.string.entry_password)))
|
getString(R.string.entry_password)))
|
||||||
database.stopManageEntry(entry)
|
database.stopManageEntry(entry)
|
||||||
})
|
}
|
||||||
} else {
|
} else {
|
||||||
// If dialog not already shown
|
// If dialog not already shown
|
||||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||||
entryContentsView?.assignPasswordCopyListener(showWarningClipboardDialogOnClickListener)
|
showWarningClipboardDialogOnClickListener
|
||||||
} else {
|
} else {
|
||||||
entryContentsView?.assignPasswordCopyListener(null)
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
entryContentsView?.assignPassword(entry.password,
|
||||||
|
allowCopyPasswordAndProtectedFields,
|
||||||
|
onPasswordCopyClickListener)
|
||||||
|
|
||||||
//Assign OTP field
|
//Assign OTP field
|
||||||
entryContentsView?.assignOtp(entry.getOtpElement(), entryProgress,
|
entryContentsView?.assignOtp(entry.getOtpElement(), entryProgress,
|
||||||
@@ -301,24 +307,22 @@ class EntryActivity : LockingActivity() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
entryContentsView?.assignURL(entry.url)
|
entryContentsView?.assignURL(entry.url)
|
||||||
entryContentsView?.assignComment(entry.notes)
|
entryContentsView?.assignNotes(entry.notes)
|
||||||
|
|
||||||
// Assign custom fields
|
// Assign custom fields
|
||||||
if (entry.allowCustomFields()) {
|
if (mDatabase?.allowEntryCustomFields() == true) {
|
||||||
entryContentsView?.clearExtraFields()
|
entryContentsView?.clearExtraFields()
|
||||||
|
entry.getExtraFields().forEach { field ->
|
||||||
for (element in entry.customFields.entries) {
|
val label = field.name
|
||||||
val label = element.key
|
val value = field.protectedValue
|
||||||
val value = element.value
|
|
||||||
|
|
||||||
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
||||||
if (allowCopyProtectedField) {
|
if (allowCopyProtectedField) {
|
||||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, View.OnClickListener {
|
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
|
||||||
clipboardHelper?.timeoutCopyToClipboard(
|
clipboardHelper?.timeoutCopyToClipboard(
|
||||||
value.toString(),
|
value.toString(),
|
||||||
getString(R.string.copy_field, label)
|
getString(R.string.copy_field, label)
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
} else {
|
} else {
|
||||||
// If dialog not already shown
|
// If dialog not already shown
|
||||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||||
@@ -329,28 +333,16 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
|
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
|
||||||
|
|
||||||
// Manage attachments
|
// Manage attachments
|
||||||
val attachments = entry.getAttachments()
|
mDatabase?.binaryPool?.let { binaryPool ->
|
||||||
val showAttachmentsView = attachments.isNotEmpty()
|
entryContentsView?.assignAttachments(entry.getAttachments(binaryPool).toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
|
||||||
entryContentsView?.showAttachments(showAttachmentsView)
|
createDocument(this, attachmentItem.name)?.let { requestCode ->
|
||||||
if (showAttachmentsView) {
|
mAttachmentsToDownload[requestCode] = attachmentItem
|
||||||
entryContentsView?.assignAttachments(attachments)
|
|
||||||
entryContentsView?.onAttachmentClick { attachmentItem, _ ->
|
|
||||||
when (attachmentItem.downloadState) {
|
|
||||||
AttachmentState.NULL, AttachmentState.ERROR, AttachmentState.COMPLETE -> {
|
|
||||||
createDocument(this, attachmentItem.name)?.let { requestCode ->
|
|
||||||
mAttachmentsToDownload[requestCode] = attachmentItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
// TODO Stop download
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entryContentsView?.refreshAttachments()
|
|
||||||
|
|
||||||
// Assign dates
|
// Assign dates
|
||||||
entryContentsView?.assignCreationDate(entry.creationTime)
|
entryContentsView?.assignCreationDate(entry.creationTime)
|
||||||
@@ -370,17 +362,9 @@ class EntryActivity : LockingActivity() {
|
|||||||
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
||||||
taColorAccent.recycle()
|
taColorAccent.recycle()
|
||||||
}
|
}
|
||||||
val entryHistory = entry.getHistory()
|
entryContentsView?.assignHistory(entry.getHistory()) { historyItem, position ->
|
||||||
// TODO isMainEntry = not an history
|
launch(this, historyItem, mReadOnly, position)
|
||||||
val showHistoryView = entryHistory.isNotEmpty()
|
|
||||||
entryContentsView?.showHistory(showHistoryView)
|
|
||||||
if (showHistoryView) {
|
|
||||||
entryContentsView?.assignHistory(entryHistory)
|
|
||||||
entryContentsView?.onHistoryClick { historyItem, position ->
|
|
||||||
launch(this, historyItem, mReadOnly, position)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
entryContentsView?.refreshHistory()
|
|
||||||
|
|
||||||
// Assign special data
|
// Assign special data
|
||||||
entryContentsView?.assignUUID(entry.nodeId.id)
|
entryContentsView?.assignUUID(entry.nodeId.id)
|
||||||
@@ -409,16 +393,6 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun changeShowPasswordIcon(togglePassword: MenuItem?) {
|
|
||||||
if (mShowPassword) {
|
|
||||||
togglePassword?.setTitle(R.string.menu_hide_password)
|
|
||||||
togglePassword?.setIcon(R.drawable.ic_visibility_off_white_24dp)
|
|
||||||
} else {
|
|
||||||
togglePassword?.setTitle(R.string.menu_showpass)
|
|
||||||
togglePassword?.setIcon(R.drawable.ic_visibility_white_24dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
|
|
||||||
@@ -434,15 +408,6 @@ class EntryActivity : LockingActivity() {
|
|||||||
menu.findItem(R.id.menu_edit)?.isVisible = false
|
menu.findItem(R.id.menu_edit)?.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
val togglePassword = menu.findItem(R.id.menu_toggle_pass)
|
|
||||||
entryContentsView?.let {
|
|
||||||
if (it.isPasswordPresent || it.atLeastOneFieldProtectedPresent()) {
|
|
||||||
changeShowPasswordIcon(togglePassword)
|
|
||||||
} else {
|
|
||||||
togglePassword?.isVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val gotoUrl = menu.findItem(R.id.menu_goto_url)
|
val gotoUrl = menu.findItem(R.id.menu_goto_url)
|
||||||
gotoUrl?.apply {
|
gotoUrl?.apply {
|
||||||
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
|
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
|
||||||
@@ -458,37 +423,38 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show education views
|
// Show education views
|
||||||
Handler().post { performedNextEducation(EntryActivityEducation(this), menu) }
|
Handler(Looper.getMainLooper()).post { performedNextEducation(EntryActivityEducation(this), menu) }
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
||||||
menu: Menu) {
|
menu: Menu) {
|
||||||
val entryCopyEducationPerformed = entryContentsView?.isUserNamePresent == true
|
val entryFieldCopyView = entryContentsView?.firstEntryFieldCopyView()
|
||||||
|
val entryCopyEducationPerformed = entryFieldCopyView != null
|
||||||
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
||||||
findViewById(R.id.entry_user_name_action_image),
|
entryFieldCopyView,
|
||||||
{
|
{
|
||||||
clipboardHelper?.timeoutCopyToClipboard(mEntry!!.username,
|
val appNameString = getString(R.string.app_name)
|
||||||
getString(R.string.copy_field,
|
clipboardHelper?.timeoutCopyToClipboard(appNameString,
|
||||||
getString(R.string.entry_user_name)))
|
getString(R.string.copy_field, appNameString))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(entryActivityEducation, menu)
|
performedNextEducation(entryActivityEducation, menu)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!entryCopyEducationPerformed) {
|
if (!entryCopyEducationPerformed) {
|
||||||
|
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
|
||||||
// entryEditEducationPerformed
|
// entryEditEducationPerformed
|
||||||
toolbar?.findViewById<View>(R.id.menu_edit) != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
|
menuEditView != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
|
||||||
toolbar!!.findViewById(R.id.menu_edit),
|
menuEditView,
|
||||||
{
|
{
|
||||||
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Open Keepass doc to create field references
|
performedNextEducation(entryActivityEducation, menu)
|
||||||
startActivity(Intent(Intent.ACTION_VIEW,
|
}
|
||||||
UriUtil.parse(getString(R.string.field_references_url))))
|
)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,12 +464,6 @@ class EntryActivity : LockingActivity() {
|
|||||||
MenuUtil.onContributionItemSelected(this)
|
MenuUtil.onContributionItemSelected(this)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_toggle_pass -> {
|
|
||||||
mShowPassword = !mShowPassword
|
|
||||||
changeShowPasswordIcon(item)
|
|
||||||
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
R.id.menu_edit -> {
|
R.id.menu_edit -> {
|
||||||
mEntry?.let {
|
mEntry?.let {
|
||||||
EntryEditActivity.launch(this@EntryActivity, it)
|
EntryEditActivity.launch(this@EntryActivity, it)
|
||||||
@@ -523,7 +483,7 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
R.id.menu_restore_entry_history -> {
|
R.id.menu_restore_entry_history -> {
|
||||||
mEntryLastVersion?.let { mainEntry ->
|
mEntryLastVersion?.let { mainEntry ->
|
||||||
mProgressDialogThread?.startDatabaseRestoreEntryHistory(
|
mProgressDatabaseTaskProvider?.startDatabaseRestoreEntryHistory(
|
||||||
mainEntry,
|
mainEntry,
|
||||||
mEntryHistoryPosition,
|
mEntryHistoryPosition,
|
||||||
!mReadOnly && mAutoSaveEnable)
|
!mReadOnly && mAutoSaveEnable)
|
||||||
@@ -531,20 +491,25 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
R.id.menu_delete_entry_history -> {
|
R.id.menu_delete_entry_history -> {
|
||||||
mEntryLastVersion?.let { mainEntry ->
|
mEntryLastVersion?.let { mainEntry ->
|
||||||
mProgressDialogThread?.startDatabaseDeleteEntryHistory(
|
mProgressDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(
|
||||||
mainEntry,
|
mainEntry,
|
||||||
mEntryHistoryPosition,
|
mEntryHistoryPosition,
|
||||||
!mReadOnly && mAutoSaveEnable)
|
!mReadOnly && mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
R.id.menu_save_database -> {
|
R.id.menu_save_database -> {
|
||||||
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
|
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
|
||||||
}
|
}
|
||||||
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
|
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
|
||||||
|
outState.putBoolean(KEY_FIRST_LAUNCH_ACTIVITY, mFirstLaunchOfActivity)
|
||||||
|
}
|
||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
// Transit data in previous Activity after an update
|
// Transit data in previous Activity after an update
|
||||||
@@ -558,6 +523,8 @@ class EntryActivity : LockingActivity() {
|
|||||||
companion object {
|
companion object {
|
||||||
private val TAG = EntryActivity::class.java.name
|
private val TAG = EntryActivity::class.java.name
|
||||||
|
|
||||||
|
private const val KEY_FIRST_LAUNCH_ACTIVITY = "KEY_FIRST_LAUNCH_ACTIVITY"
|
||||||
|
|
||||||
const val KEY_ENTRY = "KEY_ENTRY"
|
const val KEY_ENTRY = "KEY_ENTRY"
|
||||||
const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION"
|
const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION"
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,546 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||||
|
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
|
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||||
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
|
import com.kunzisoft.keepass.model.*
|
||||||
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||||
|
import com.kunzisoft.keepass.view.collapse
|
||||||
|
import com.kunzisoft.keepass.view.expand
|
||||||
|
|
||||||
|
class EntryEditFragment: StylishFragment() {
|
||||||
|
|
||||||
|
private lateinit var entryTitleLayoutView: TextInputLayout
|
||||||
|
private lateinit var entryTitleView: EditText
|
||||||
|
private lateinit var entryIconView: ImageView
|
||||||
|
private lateinit var entryUserNameView: EditText
|
||||||
|
private lateinit var entryUrlView: EditText
|
||||||
|
private lateinit var entryPasswordLayoutView: TextInputLayout
|
||||||
|
private lateinit var entryPasswordView: EditText
|
||||||
|
private lateinit var entryPasswordGeneratorView: View
|
||||||
|
private lateinit var entryExpiresCheckBox: CompoundButton
|
||||||
|
private lateinit var entryExpiresTextView: TextView
|
||||||
|
private lateinit var entryNotesView: EditText
|
||||||
|
private lateinit var extraFieldsContainerView: View
|
||||||
|
private lateinit var extraFieldsListView: ViewGroup
|
||||||
|
private lateinit var attachmentsContainerView: View
|
||||||
|
private lateinit var attachmentsListView: RecyclerView
|
||||||
|
|
||||||
|
private lateinit var attachmentsAdapter: EntryAttachmentsItemsAdapter
|
||||||
|
|
||||||
|
private var fontInVisibility: Boolean = false
|
||||||
|
private var iconColor: Int = 0
|
||||||
|
private var expiresInstant: DateInstant = DateInstant.IN_ONE_MONTH
|
||||||
|
|
||||||
|
var drawFactory: IconDrawableFactory? = null
|
||||||
|
var setOnDateClickListener: View.OnClickListener? = null
|
||||||
|
var setOnPasswordGeneratorClickListener: View.OnClickListener? = null
|
||||||
|
var setOnIconViewClickListener: View.OnClickListener? = null
|
||||||
|
var setOnEditCustomField: ((Field) -> Unit)? = null
|
||||||
|
var setOnRemoveAttachment: ((Attachment) -> Unit)? = null
|
||||||
|
|
||||||
|
// Elements to modify the current entry
|
||||||
|
private var mEntryInfo = EntryInfo()
|
||||||
|
private var mLastFocusedEditField: FocusedEditField? = null
|
||||||
|
private var mExtraViewToRequestFocus: EditText? = null
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
|
val rootView = inflater.cloneInContext(contextThemed)
|
||||||
|
.inflate(R.layout.fragment_entry_edit_contents, container, false)
|
||||||
|
|
||||||
|
fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(requireContext())
|
||||||
|
|
||||||
|
entryTitleLayoutView = rootView.findViewById(R.id.entry_edit_container_title)
|
||||||
|
entryTitleView = rootView.findViewById(R.id.entry_edit_title)
|
||||||
|
entryIconView = rootView.findViewById(R.id.entry_edit_icon_button)
|
||||||
|
entryIconView.setOnClickListener {
|
||||||
|
setOnIconViewClickListener?.onClick(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
entryUserNameView = rootView.findViewById(R.id.entry_edit_user_name)
|
||||||
|
entryUrlView = rootView.findViewById(R.id.entry_edit_url)
|
||||||
|
entryPasswordLayoutView = rootView.findViewById(R.id.entry_edit_container_password)
|
||||||
|
entryPasswordView = rootView.findViewById(R.id.entry_edit_password)
|
||||||
|
entryPasswordGeneratorView = rootView.findViewById(R.id.entry_edit_password_generator_button)
|
||||||
|
entryPasswordGeneratorView.setOnClickListener {
|
||||||
|
setOnPasswordGeneratorClickListener?.onClick(it)
|
||||||
|
}
|
||||||
|
entryExpiresCheckBox = rootView.findViewById(R.id.entry_edit_expires_checkbox)
|
||||||
|
entryExpiresTextView = rootView.findViewById(R.id.entry_edit_expires_text)
|
||||||
|
entryExpiresTextView.setOnClickListener {
|
||||||
|
if (entryExpiresCheckBox.isChecked)
|
||||||
|
setOnDateClickListener?.onClick(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
entryNotesView = rootView.findViewById(R.id.entry_edit_notes)
|
||||||
|
|
||||||
|
extraFieldsContainerView = rootView.findViewById(R.id.extra_fields_container)
|
||||||
|
extraFieldsListView = rootView.findViewById(R.id.extra_fields_list)
|
||||||
|
|
||||||
|
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
|
||||||
|
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
|
||||||
|
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
|
||||||
|
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
|
||||||
|
if (previousSize > 0 && newSize == 0) {
|
||||||
|
attachmentsContainerView.collapse(true)
|
||||||
|
} else if (previousSize == 0 && newSize == 1) {
|
||||||
|
attachmentsContainerView.expand(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attachmentsListView.apply {
|
||||||
|
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||||
|
adapter = attachmentsAdapter
|
||||||
|
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
|
}
|
||||||
|
|
||||||
|
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
|
||||||
|
assignExpiresDateText()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the textColor to tint the icon
|
||||||
|
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
|
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
|
||||||
|
taIconColor?.recycle()
|
||||||
|
|
||||||
|
// Retrieve the new entry after an orientation change
|
||||||
|
if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true)
|
||||||
|
mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
|
||||||
|
else if (savedInstanceState?.containsKey(KEY_TEMP_ENTRY_INFO) == true) {
|
||||||
|
mEntryInfo = savedInstanceState.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedInstanceState?.containsKey(KEY_LAST_FOCUSED_FIELD) == true) {
|
||||||
|
mLastFocusedEditField = savedInstanceState.getParcelable(KEY_LAST_FOCUSED_FIELD) ?: mLastFocusedEditField
|
||||||
|
}
|
||||||
|
|
||||||
|
populateViewsWithEntry()
|
||||||
|
|
||||||
|
return rootView
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
super.onDetach()
|
||||||
|
|
||||||
|
drawFactory = null
|
||||||
|
setOnDateClickListener = null
|
||||||
|
setOnPasswordGeneratorClickListener = null
|
||||||
|
setOnIconViewClickListener = null
|
||||||
|
setOnRemoveAttachment = null
|
||||||
|
setOnEditCustomField = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryInfo(): EntryInfo? {
|
||||||
|
populateEntryWithViews()
|
||||||
|
return mEntryInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generatePasswordEducationPerformed(entryEditActivityEducation: EntryEditActivityEducation): Boolean {
|
||||||
|
return entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
||||||
|
entryPasswordGeneratorView,
|
||||||
|
{
|
||||||
|
GeneratePasswordDialogFragment().show(parentFragmentManager, "PasswordGeneratorFragment")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
(activity as? EntryEditActivity?)?.performedNextEducation(entryEditActivityEducation)
|
||||||
|
} catch (ignore: Exception) {}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateViewsWithEntry() {
|
||||||
|
// Set info in view
|
||||||
|
icon = mEntryInfo.icon
|
||||||
|
title = mEntryInfo.title
|
||||||
|
username = mEntryInfo.username
|
||||||
|
url = mEntryInfo.url
|
||||||
|
password = mEntryInfo.password
|
||||||
|
expires = mEntryInfo.expires
|
||||||
|
expiryTime = mEntryInfo.expiryTime
|
||||||
|
notes = mEntryInfo.notes
|
||||||
|
assignExtraFields(mEntryInfo.customFields) { fields ->
|
||||||
|
setOnEditCustomField?.invoke(fields)
|
||||||
|
}
|
||||||
|
assignAttachments(mEntryInfo.attachments, StreamDirection.UPLOAD) { attachment ->
|
||||||
|
setOnRemoveAttachment?.invoke(attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateEntryWithViews() {
|
||||||
|
// Icon already populate
|
||||||
|
mEntryInfo.title = title
|
||||||
|
mEntryInfo.username = username
|
||||||
|
mEntryInfo.url = url
|
||||||
|
mEntryInfo.password = password
|
||||||
|
mEntryInfo.expires = expires
|
||||||
|
mEntryInfo.expiryTime = expiryTime
|
||||||
|
mEntryInfo.notes = notes
|
||||||
|
mEntryInfo.customFields = getExtraFields()
|
||||||
|
mEntryInfo.otpModel = OtpEntryFields.parseFields { key ->
|
||||||
|
getExtraFields().firstOrNull { it.name == key }?.protectedValue?.toString()
|
||||||
|
}?.otpModel
|
||||||
|
mEntryInfo.attachments = getAttachments()
|
||||||
|
}
|
||||||
|
|
||||||
|
var title: String
|
||||||
|
get() {
|
||||||
|
return entryTitleView.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
entryTitleView.setText(value)
|
||||||
|
if (fontInVisibility)
|
||||||
|
entryTitleView.applyFontVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
var icon: IconImage
|
||||||
|
get() {
|
||||||
|
return mEntryInfo.icon
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
mEntryInfo.icon = value
|
||||||
|
drawFactory?.let { drawFactory ->
|
||||||
|
entryIconView.assignDatabaseIcon(drawFactory, value, iconColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var username: String
|
||||||
|
get() {
|
||||||
|
return entryUserNameView.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
entryUserNameView.setText(value)
|
||||||
|
if (fontInVisibility)
|
||||||
|
entryUserNameView.applyFontVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
var url: String
|
||||||
|
get() {
|
||||||
|
return entryUrlView.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
entryUrlView.setText(value)
|
||||||
|
if (fontInVisibility)
|
||||||
|
entryUrlView.applyFontVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
var password: String
|
||||||
|
get() {
|
||||||
|
return entryPasswordView.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
entryPasswordView.setText(value)
|
||||||
|
if (fontInVisibility) {
|
||||||
|
entryPasswordView.applyFontVisibility()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assignExpiresDateText() {
|
||||||
|
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
|
||||||
|
entryExpiresTextView.setOnClickListener(setOnDateClickListener)
|
||||||
|
expiresInstant.getDateTimeString(resources)
|
||||||
|
} else {
|
||||||
|
entryExpiresTextView.setOnClickListener(null)
|
||||||
|
resources.getString(R.string.never)
|
||||||
|
}
|
||||||
|
if (fontInVisibility)
|
||||||
|
entryExpiresTextView.applyFontVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
var expires: Boolean
|
||||||
|
get() {
|
||||||
|
return entryExpiresCheckBox.isChecked
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
if (!value) {
|
||||||
|
expiresInstant = DateInstant.IN_ONE_MONTH
|
||||||
|
}
|
||||||
|
entryExpiresCheckBox.isChecked = value
|
||||||
|
assignExpiresDateText()
|
||||||
|
}
|
||||||
|
|
||||||
|
var expiryTime: DateInstant
|
||||||
|
get() {
|
||||||
|
return if (expires)
|
||||||
|
expiresInstant
|
||||||
|
else
|
||||||
|
DateInstant.NEVER_EXPIRE
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
if (expires)
|
||||||
|
expiresInstant = value
|
||||||
|
assignExpiresDateText()
|
||||||
|
}
|
||||||
|
|
||||||
|
var notes: String
|
||||||
|
get() {
|
||||||
|
return entryNotesView.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
entryNotesView.setText(value)
|
||||||
|
if (fontInVisibility)
|
||||||
|
entryNotesView.applyFontVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------
|
||||||
|
* Extra Fields
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
private var mExtraFieldsList: MutableList<Field> = ArrayList()
|
||||||
|
private var mOnEditButtonClickListener: ((item: Field)->Unit)? = null
|
||||||
|
|
||||||
|
private fun buildViewFromField(extraField: Field): View? {
|
||||||
|
val inflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||||
|
val itemView: View? = inflater?.inflate(R.layout.item_entry_edit_extra_field, extraFieldsListView, false)
|
||||||
|
itemView?.id = View.NO_ID
|
||||||
|
|
||||||
|
val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container)
|
||||||
|
extraFieldValueContainer?.isPasswordVisibilityToggleEnabled = extraField.protectedValue.isProtected
|
||||||
|
extraFieldValueContainer?.hint = extraField.name
|
||||||
|
extraFieldValueContainer?.id = View.NO_ID
|
||||||
|
|
||||||
|
val extraFieldValue: TextInputEditText? = itemView?.findViewById(R.id.entry_extra_field_value)
|
||||||
|
extraFieldValue?.apply {
|
||||||
|
if (extraField.protectedValue.isProtected) {
|
||||||
|
inputType = extraFieldValue.inputType or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
|
||||||
|
}
|
||||||
|
setText(extraField.protectedValue.toString())
|
||||||
|
if (fontInVisibility)
|
||||||
|
applyFontVisibility()
|
||||||
|
}
|
||||||
|
extraFieldValue?.id = View.NO_ID
|
||||||
|
extraFieldValue?.tag = "FIELD_VALUE_TAG"
|
||||||
|
if (mLastFocusedEditField?.field == extraField) {
|
||||||
|
mExtraViewToRequestFocus = extraFieldValue
|
||||||
|
}
|
||||||
|
|
||||||
|
val extraFieldEditButton: View? = itemView?.findViewById(R.id.entry_extra_field_edit)
|
||||||
|
extraFieldEditButton?.setOnClickListener {
|
||||||
|
mOnEditButtonClickListener?.invoke(extraField)
|
||||||
|
}
|
||||||
|
extraFieldEditButton?.id = View.NO_ID
|
||||||
|
|
||||||
|
return itemView
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getExtraFields(): List<Field> {
|
||||||
|
mLastFocusedEditField = null
|
||||||
|
for (index in 0 until extraFieldsListView.childCount) {
|
||||||
|
val extraFieldValue: EditText = extraFieldsListView.getChildAt(index)
|
||||||
|
.findViewWithTag("FIELD_VALUE_TAG")
|
||||||
|
val extraField = mExtraFieldsList[index]
|
||||||
|
extraField.protectedValue.stringValue = extraFieldValue.text?.toString() ?: ""
|
||||||
|
if (extraFieldValue.isFocused) {
|
||||||
|
mLastFocusedEditField = FocusedEditField().apply {
|
||||||
|
field = extraField
|
||||||
|
cursorSelectionStart = extraFieldValue.selectionStart
|
||||||
|
cursorSelectionEnd = extraFieldValue.selectionEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mExtraFieldsList
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all children and add new views for each field
|
||||||
|
*/
|
||||||
|
fun assignExtraFields(fields: List<Field>,
|
||||||
|
onEditButtonClickListener: ((item: Field)->Unit)?) {
|
||||||
|
extraFieldsContainerView.visibility = if (fields.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
// Reinit focused field
|
||||||
|
mExtraFieldsList.clear()
|
||||||
|
mExtraFieldsList.addAll(fields)
|
||||||
|
extraFieldsListView.removeAllViews()
|
||||||
|
fields.forEach {
|
||||||
|
extraFieldsListView.addView(buildViewFromField(it))
|
||||||
|
}
|
||||||
|
// Request last focus
|
||||||
|
mLastFocusedEditField?.let { focusField ->
|
||||||
|
mExtraViewToRequestFocus?.apply {
|
||||||
|
requestFocus()
|
||||||
|
setSelection(focusField.cursorSelectionStart,
|
||||||
|
focusField.cursorSelectionEnd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mLastFocusedEditField = null
|
||||||
|
mOnEditButtonClickListener = onEditButtonClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an extra field or create a new one if doesn't exists, the old value is lost
|
||||||
|
*/
|
||||||
|
fun putExtraField(extraField: Field) {
|
||||||
|
extraFieldsContainerView.visibility = View.VISIBLE
|
||||||
|
val oldField = mExtraFieldsList.firstOrNull { it.name == extraField.name }
|
||||||
|
oldField?.let {
|
||||||
|
val index = mExtraFieldsList.indexOf(oldField)
|
||||||
|
mExtraFieldsList.removeAt(index)
|
||||||
|
mExtraFieldsList.add(index, extraField)
|
||||||
|
extraFieldsListView.removeViewAt(index)
|
||||||
|
val newView = buildViewFromField(extraField)
|
||||||
|
extraFieldsListView.addView(newView, index)
|
||||||
|
newView?.requestFocus()
|
||||||
|
} ?: kotlin.run {
|
||||||
|
mExtraFieldsList.add(extraField)
|
||||||
|
val newView = buildViewFromField(extraField)
|
||||||
|
extraFieldsListView.addView(newView)
|
||||||
|
newView?.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an extra field and keep the old value
|
||||||
|
*/
|
||||||
|
fun replaceExtraField(oldExtraField: Field, newExtraField: Field) {
|
||||||
|
extraFieldsContainerView.visibility = View.VISIBLE
|
||||||
|
val index = mExtraFieldsList.indexOf(oldExtraField)
|
||||||
|
val oldValueEditText: EditText = extraFieldsListView.getChildAt(index)
|
||||||
|
.findViewWithTag("FIELD_VALUE_TAG")
|
||||||
|
val oldValue = oldValueEditText.text.toString()
|
||||||
|
val newExtraFieldWithOldValue = Field(newExtraField).apply {
|
||||||
|
this.protectedValue.stringValue = oldValue
|
||||||
|
}
|
||||||
|
mExtraFieldsList.removeAt(index)
|
||||||
|
mExtraFieldsList.add(index, newExtraFieldWithOldValue)
|
||||||
|
extraFieldsListView.removeViewAt(index)
|
||||||
|
val newView = buildViewFromField(newExtraFieldWithOldValue)
|
||||||
|
extraFieldsListView.addView(newView, index)
|
||||||
|
newView?.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeExtraField(oldExtraField: Field) {
|
||||||
|
val previousSize = mExtraFieldsList.size
|
||||||
|
val index = mExtraFieldsList.indexOf(oldExtraField)
|
||||||
|
extraFieldsListView.getChildAt(index)?.let {
|
||||||
|
it.collapse(true) {
|
||||||
|
mExtraFieldsList.removeAt(index)
|
||||||
|
extraFieldsListView.removeViewAt(index)
|
||||||
|
val newSize = mExtraFieldsList.size
|
||||||
|
|
||||||
|
if (previousSize > 0 && newSize == 0) {
|
||||||
|
extraFieldsContainerView.collapse(true)
|
||||||
|
} else if (previousSize == 0 && newSize == 1) {
|
||||||
|
extraFieldsContainerView.expand(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------
|
||||||
|
* Attachments
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun getAttachments(): List<Attachment> {
|
||||||
|
return attachmentsAdapter.itemsList.map { it.attachment }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assignAttachments(attachments: List<Attachment>,
|
||||||
|
streamDirection: StreamDirection,
|
||||||
|
onDeleteItem: (attachment: Attachment)->Unit) {
|
||||||
|
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
|
||||||
|
attachmentsAdapter.onDeleteButtonClickListener = { item ->
|
||||||
|
onDeleteItem.invoke(item.attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun containsAttachment(): Boolean {
|
||||||
|
return !attachmentsAdapter.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun containsAttachment(attachment: EntryAttachmentState): Boolean {
|
||||||
|
return attachmentsAdapter.contains(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putAttachment(attachment: EntryAttachmentState) {
|
||||||
|
attachmentsContainerView.visibility = View.VISIBLE
|
||||||
|
attachmentsAdapter.putItem(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAttachment(attachment: EntryAttachmentState) {
|
||||||
|
attachmentsAdapter.removeItem(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearAttachments() {
|
||||||
|
attachmentsAdapter.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAttachmentViewPosition(attachment: EntryAttachmentState, position: (Float) -> Unit) {
|
||||||
|
attachmentsListView.postDelayed({
|
||||||
|
position.invoke(attachmentsContainerView.y
|
||||||
|
+ attachmentsListView.y
|
||||||
|
+ (attachmentsListView.getChildAt(attachmentsAdapter.indexOf(attachment))?.y
|
||||||
|
?: 0F)
|
||||||
|
)
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
populateEntryWithViews()
|
||||||
|
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
|
||||||
|
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
|
||||||
|
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
|
||||||
|
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
|
||||||
|
|
||||||
|
fun getInstance(entryInfo: EntryInfo?): EntryEditFragment {
|
||||||
|
return EntryEditFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
|
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity to search or select entry in database,
|
||||||
|
* Commonly used with Magikeyboard
|
||||||
|
*/
|
||||||
|
class EntrySelectionLauncherActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
||||||
|
var sharedWebDomain: String? = null
|
||||||
|
|
||||||
|
when (intent?.action) {
|
||||||
|
Intent.ACTION_SEND -> {
|
||||||
|
if ("text/plain" == intent.type) {
|
||||||
|
// Retrieve web domain
|
||||||
|
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
||||||
|
sharedWebDomain = Uri.parse(it).host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build search param
|
||||||
|
val searchInfo = SearchInfo().apply {
|
||||||
|
webDomain = sharedWebDomain
|
||||||
|
}
|
||||||
|
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||||
|
searchInfo.webDomain = concreteWebDomain
|
||||||
|
launch(searchInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launch(searchInfo: SearchInfo) {
|
||||||
|
// Setting to integrate Magikeyboard
|
||||||
|
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
|
||||||
|
|
||||||
|
// If database is open
|
||||||
|
val database = Database.getInstance()
|
||||||
|
val readOnly = database.isReadOnly
|
||||||
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
|
database,
|
||||||
|
searchInfo,
|
||||||
|
{ items ->
|
||||||
|
// Items found
|
||||||
|
if (searchShareForMagikeyboard) {
|
||||||
|
if (items.size == 1) {
|
||||||
|
// Automatically populate keyboard
|
||||||
|
val entryPopulate = items[0]
|
||||||
|
populateKeyboardAndMoveAppToBackground(this,
|
||||||
|
entryPopulate,
|
||||||
|
intent)
|
||||||
|
} else {
|
||||||
|
// Select the one we want
|
||||||
|
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||||
|
readOnly,
|
||||||
|
searchInfo,
|
||||||
|
true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
GroupActivity.launchForSearchResult(this,
|
||||||
|
readOnly,
|
||||||
|
searchInfo,
|
||||||
|
true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Show the database UI to select the entry
|
||||||
|
if (readOnly || searchShareForMagikeyboard) {
|
||||||
|
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||||
|
readOnly,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
|
} else {
|
||||||
|
GroupActivity.launchForSaveResult(this,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// If database not open
|
||||||
|
if (searchShareForMagikeyboard) {
|
||||||
|
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
|
||||||
|
searchInfo)
|
||||||
|
} else {
|
||||||
|
FileDatabaseSelectActivity.launchForSearchResult(this,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
||||||
|
entry: EntryInfo,
|
||||||
|
intent: Intent,
|
||||||
|
toast: Boolean = true) {
|
||||||
|
// Populate Magikeyboard with entry
|
||||||
|
MagikIME.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
||||||
|
// Consume the selection mode
|
||||||
|
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||||
|
activity.moveTaskToBack(true)
|
||||||
|
}
|
||||||
@@ -19,21 +19,24 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
@@ -41,32 +44,38 @@ import com.google.android.material.snackbar.Snackbar
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper.KEY_SEARCH_INFO
|
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.*
|
import com.kunzisoft.keepass.utils.*
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
||||||
import kotlinx.android.synthetic.main.activity_file_selection.*
|
import kotlinx.android.synthetic.main.activity_file_selection.*
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
class FileDatabaseSelectActivity : StylishActivity(),
|
class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||||
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var fileManagerExplanationButton: View? = null
|
private var createDatabaseButtonView: View? = null
|
||||||
private var createButtonView: View? = null
|
|
||||||
private var openDatabaseButtonView: View? = null
|
private var openDatabaseButtonView: View? = null
|
||||||
|
|
||||||
|
private val databaseFilesViewModel: DatabaseFilesViewModel by viewModels()
|
||||||
|
|
||||||
// Adapter to manage database history list
|
// Adapter to manage database history list
|
||||||
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
|
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
|
||||||
|
|
||||||
@@ -74,9 +83,9 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
|
|
||||||
private var mDatabaseFileUri: Uri? = null
|
private var mDatabaseFileUri: Uri? = null
|
||||||
|
|
||||||
private var mOpenFileHelper: OpenFileHelper? = null
|
private var mSelectFileHelper: SelectFileHelper? = null
|
||||||
|
|
||||||
private var mProgressDialogThread: ProgressDialogThread? = null
|
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -90,28 +99,15 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
toolbar.title = ""
|
toolbar.title = ""
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
|
|
||||||
fileManagerExplanationButton = findViewById(R.id.file_manager_explanation_button)
|
// Create database button
|
||||||
fileManagerExplanationButton?.setOnClickListener {
|
createDatabaseButtonView = findViewById(R.id.create_database_button)
|
||||||
UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
|
createDatabaseButtonView?.setOnClickListener { createNewFile() }
|
||||||
}
|
|
||||||
|
|
||||||
// Create button
|
// Open database button
|
||||||
createButtonView = findViewById(R.id.create_database_button)
|
mSelectFileHelper = SelectFileHelper(this)
|
||||||
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
|
||||||
// There is an activity which can handle this intent.
|
|
||||||
createButtonView?.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
// No Activity found that can handle this intent.
|
|
||||||
createButtonView?.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
createButtonView?.setOnClickListener { createNewFile() }
|
|
||||||
|
|
||||||
mOpenFileHelper = OpenFileHelper(this)
|
|
||||||
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
||||||
openDatabaseButtonView?.apply {
|
openDatabaseButtonView?.apply {
|
||||||
mOpenFileHelper?.openFileOnClickViewListener?.let {
|
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
||||||
setOnClickListener(it)
|
setOnClickListener(it)
|
||||||
setOnLongClickListener(it)
|
setOnLongClickListener(it)
|
||||||
}
|
}
|
||||||
@@ -124,26 +120,25 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
(fileDatabaseHistoryRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
(fileDatabaseHistoryRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
// Construct adapter with listeners
|
// Construct adapter with listeners
|
||||||
mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this)
|
mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this)
|
||||||
|
mAdapterDatabaseHistory?.setOnDefaultDatabaseListener { databaseFile ->
|
||||||
|
databaseFilesViewModel.setDefaultDatabase(databaseFile)
|
||||||
|
}
|
||||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
||||||
UriUtil.parse(fileDatabaseHistoryEntityToOpen.databaseUri)?.let { databaseFileUri ->
|
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
|
||||||
launchPasswordActivity(
|
launchPasswordActivity(
|
||||||
databaseFileUri,
|
databaseFileUri,
|
||||||
UriUtil.parse(fileDatabaseHistoryEntityToOpen.keyFileUri))
|
fileDatabaseHistoryEntityToOpen.keyFileUri
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
||||||
// Remove from app database
|
// Remove from app database
|
||||||
mFileDatabaseHistoryAction?.deleteFileDatabaseHistory(fileDatabaseHistoryToDelete) { fileHistoryDeleted ->
|
databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete)
|
||||||
// Remove from adapter
|
|
||||||
fileHistoryDeleted?.let { databaseFileHistoryDeleted ->
|
|
||||||
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileHistoryDeleted)
|
|
||||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
mAdapterDatabaseHistory?.setOnSaveAliasListener { fileDatabaseHistoryWithNewAlias ->
|
mAdapterDatabaseHistory?.setOnSaveAliasListener { fileDatabaseHistoryWithNewAlias ->
|
||||||
mFileDatabaseHistoryAction?.addOrUpdateFileDatabaseHistory(fileDatabaseHistoryWithNewAlias)
|
// Update in app database
|
||||||
|
databaseFilesViewModel.updateDatabaseFile(fileDatabaseHistoryWithNewAlias)
|
||||||
}
|
}
|
||||||
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
|
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
|
||||||
|
|
||||||
@@ -156,7 +151,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
UriUtil.parse(databasePath)?.let { databaseFileUri ->
|
UriUtil.parse(databasePath)?.let { databaseFileUri ->
|
||||||
launchPasswordActivityWithPath(databaseFileUri)
|
launchPasswordActivityWithPath(databaseFileUri)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
Log.i(TAG, "Unable to launch Password Activity")
|
Log.i(TAG, "No default database to prepare")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,12 +161,66 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
mDatabaseFileUri = savedInstanceState.getParcelable(EXTRA_DATABASE_URI)
|
mDatabaseFileUri = savedInstanceState.getParcelable(EXTRA_DATABASE_URI)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Observe list of databases
|
||||||
|
databaseFilesViewModel.databaseFilesLoaded.observe(this, Observer { databaseFiles ->
|
||||||
|
when (databaseFiles.databaseFileAction) {
|
||||||
|
DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
|
||||||
|
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
|
||||||
|
}
|
||||||
|
DatabaseFilesViewModel.DatabaseFileAction.ADD -> {
|
||||||
|
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
|
||||||
|
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
|
||||||
|
}
|
||||||
|
GroupActivity.launch(this@FileDatabaseSelectActivity,
|
||||||
|
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity))
|
||||||
|
}
|
||||||
|
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
|
||||||
|
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
|
||||||
|
mAdapterDatabaseHistory?.updateDatabaseFileHistory(databaseFileToUpdate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DatabaseFilesViewModel.DatabaseFileAction.DELETE -> {
|
||||||
|
databaseFiles.databaseFileToActivate?.let { databaseFileToDelete ->
|
||||||
|
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileToDelete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
databaseFilesViewModel.consumeAction()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Observe default database
|
||||||
|
databaseFilesViewModel.defaultDatabase.observe(this, Observer {
|
||||||
|
// Retrieve settings for default database
|
||||||
|
mAdapterDatabaseHistory?.setDefaultDatabase(it)
|
||||||
|
})
|
||||||
|
|
||||||
// Attach the dialog thread to this activity
|
// Attach the dialog thread to this activity
|
||||||
mProgressDialogThread = ProgressDialogThread(this).apply {
|
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
|
||||||
onActionFinish = { actionTask, _ ->
|
onActionFinish = { actionTask, result ->
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_CREATE_TASK -> {
|
ACTION_DATABASE_CREATE_TASK -> {
|
||||||
GroupActivity.launch(this@FileDatabaseSelectActivity)
|
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
|
||||||
|
val keyFileUri = result.data?.getParcelable<Uri?>(KEY_FILE_URI_KEY)
|
||||||
|
databaseFilesViewModel.addDatabaseFile(databaseUri, keyFileUri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
|
val database = Database.getInstance()
|
||||||
|
if (result.isSuccess
|
||||||
|
&& database.loaded) {
|
||||||
|
launchGroupActivity(database)
|
||||||
|
} else {
|
||||||
|
var resultError = ""
|
||||||
|
val resultMessage = result.message
|
||||||
|
// Show error message
|
||||||
|
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
||||||
|
resultError = "$resultError $resultMessage"
|
||||||
|
}
|
||||||
|
Log.e(TAG, resultError)
|
||||||
|
Snackbar.make(activity_file_selection_coordinator_layout,
|
||||||
|
resultError,
|
||||||
|
Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,7 +230,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
/**
|
/**
|
||||||
* Create a new file by calling the content provider
|
* Create a new file by calling the content provider
|
||||||
*/
|
*/
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
private fun createNewFile() {
|
private fun createNewFile() {
|
||||||
createDocument(this, getString(R.string.database_file_name_default) +
|
createDocument(this, getString(R.string.database_file_name_default) +
|
||||||
getString(R.string.database_file_extension_default), "application/x-keepass")
|
getString(R.string.database_file_extension_default), "application/x-keepass")
|
||||||
@@ -196,59 +244,32 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
||||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
PasswordActivity.launch(this,
|
||||||
{
|
databaseUri,
|
||||||
try {
|
keyFile,
|
||||||
PasswordActivity.launch(this@FileDatabaseSelectActivity,
|
{ exception ->
|
||||||
databaseUri, keyFile)
|
fileNoFoundAction(exception)
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
fileNoFoundAction(e)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{ onCancelSpecialMode() },
|
||||||
try {
|
{ onLaunchActivitySpecialMode() })
|
||||||
PasswordActivity.launchForKeyboardResult(this@FileDatabaseSelectActivity,
|
|
||||||
databaseUri, keyFile)
|
|
||||||
finish()
|
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
fileNoFoundAction(e)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ assistStructure ->
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
try {
|
|
||||||
PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
|
||||||
databaseUri, keyFile,
|
|
||||||
assistStructure,
|
|
||||||
intent.getParcelableExtra(KEY_SEARCH_INFO))
|
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
fileNoFoundAction(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchGroupActivity(readOnly: Boolean) {
|
private fun launchGroupActivity(database: Database) {
|
||||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
GroupActivity.launch(this,
|
||||||
{
|
database.isReadOnly,
|
||||||
GroupActivity.launch(this@FileDatabaseSelectActivity,
|
{ onValidateSpecialMode() },
|
||||||
readOnly)
|
{ onCancelSpecialMode() },
|
||||||
},
|
{ onLaunchActivitySpecialMode() })
|
||||||
{
|
}
|
||||||
GroupActivity.launchForKeyboardSelection(this@FileDatabaseSelectActivity,
|
|
||||||
readOnly)
|
override fun onValidateSpecialMode() {
|
||||||
// Do not keep history
|
super.onValidateSpecialMode()
|
||||||
finish()
|
finish()
|
||||||
},
|
}
|
||||||
{ assistStructure ->
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
override fun onCancelSpecialMode() {
|
||||||
GroupActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
super.onCancelSpecialMode()
|
||||||
assistStructure,
|
finish()
|
||||||
intent.getParcelableExtra(KEY_SEARCH_INFO),
|
|
||||||
readOnly)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
||||||
@@ -259,42 +280,45 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
val database = Database.getInstance()
|
|
||||||
if (database.loaded) {
|
|
||||||
launchGroupActivity(database.isReadOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
// Construct adapter with listeners
|
// Show open and create button or special mode
|
||||||
if (PreferencesUtil.showRecentFiles(this)) {
|
when (mSpecialMode) {
|
||||||
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
|
SpecialMode.DEFAULT -> {
|
||||||
databaseFileHistoryList?.let { historyList ->
|
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
||||||
val hideBrokenLocations = PreferencesUtil.hideBrokenLocations(this@FileDatabaseSelectActivity)
|
// There is an activity which can handle this intent.
|
||||||
mAdapterDatabaseHistory?.addDatabaseFileHistoryList(
|
createDatabaseButtonView?.visibility = View.VISIBLE
|
||||||
// Show only uri accessible
|
} else{
|
||||||
historyList.filter {
|
// No Activity found that can handle this intent.
|
||||||
if (hideBrokenLocations) {
|
createDatabaseButtonView?.visibility = View.GONE
|
||||||
FileDatabaseInfo(this@FileDatabaseSelectActivity,
|
|
||||||
it.databaseUri).exists
|
|
||||||
} else
|
|
||||||
true
|
|
||||||
})
|
|
||||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
else -> {
|
||||||
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
// Disable create button if in selection mode or request for autofill
|
||||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
createDatabaseButtonView?.visibility = View.GONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register progress task
|
val database = Database.getInstance()
|
||||||
mProgressDialogThread?.registerProgressTask()
|
if (database.loaded) {
|
||||||
|
launchGroupActivity(database)
|
||||||
|
} else {
|
||||||
|
// Construct adapter with listeners
|
||||||
|
if (PreferencesUtil.showRecentFiles(this)) {
|
||||||
|
databaseFilesViewModel.loadListOfDatabases()
|
||||||
|
} else {
|
||||||
|
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
||||||
|
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register progress task
|
||||||
|
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
// Unregister progress task
|
// Unregister progress task
|
||||||
mProgressDialogThread?.unregisterProgressTask()
|
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
@@ -315,7 +339,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
mDatabaseFileUri?.let { databaseUri ->
|
mDatabaseFileUri?.let { databaseUri ->
|
||||||
|
|
||||||
// Create the new database
|
// Create the new database
|
||||||
mProgressDialogThread?.startDatabaseCreate(
|
mProgressDatabaseTaskProvider?.startDatabaseCreate(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
masterPasswordChecked,
|
masterPasswordChecked,
|
||||||
masterPassword,
|
masterPassword,
|
||||||
@@ -343,8 +367,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||||
) { uri ->
|
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
launchPasswordActivityWithPath(uri)
|
launchPasswordActivityWithPath(uri)
|
||||||
}
|
}
|
||||||
@@ -368,20 +391,24 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
MenuUtil.defaultMenuInflater(menuInflater, menu)
|
|
||||||
|
|
||||||
Handler().post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||||
|
MenuUtil.defaultMenuInflater(menuInflater, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
Handler(Looper.getMainLooper()).post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
|
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
|
||||||
// If no recent files
|
// If no recent files
|
||||||
val createDatabaseEducationPerformed = createButtonView != null && createButtonView!!.visibility == View.VISIBLE
|
val createDatabaseEducationPerformed =
|
||||||
|
createDatabaseButtonView != null && createDatabaseButtonView!!.visibility == View.VISIBLE
|
||||||
&& mAdapterDatabaseHistory != null
|
&& mAdapterDatabaseHistory != null
|
||||||
&& mAdapterDatabaseHistory!!.itemCount > 0
|
&& mAdapterDatabaseHistory!!.itemCount > 0
|
||||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
||||||
createButtonView!!,
|
createDatabaseButtonView!!,
|
||||||
{
|
{
|
||||||
createNewFile()
|
createNewFile()
|
||||||
},
|
},
|
||||||
@@ -396,7 +423,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
openDatabaseButtonView!!,
|
openDatabaseButtonView!!,
|
||||||
{tapTargetView ->
|
{tapTargetView ->
|
||||||
tapTargetView?.let {
|
tapTargetView?.let {
|
||||||
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it)
|
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
@@ -405,6 +432,10 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
|
||||||
|
}
|
||||||
|
|
||||||
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
|
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,18 +447,38 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* No Standard Launch, pass by PasswordActivity
|
* Standard Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
fun launch(context: Context) {
|
||||||
|
context.startActivity(Intent(context, FileDatabaseSelectActivity::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Search Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun launchForSearchResult(context: Context,
|
||||||
|
searchInfo: SearchInfo) {
|
||||||
|
EntrySelectionHelper.startActivityForSearchModeResult(context,
|
||||||
|
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Keyboard Launch
|
* Keyboard Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun launchForKeyboardSelection(activity: Activity) {
|
fun launchForKeyboardSelectionResult(activity: Activity,
|
||||||
EntrySelectionHelper.startActivityForEntrySelection(activity, Intent(activity, FileDatabaseSelectActivity::class.java))
|
searchInfo: SearchInfo? = null) {
|
||||||
|
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(activity,
|
||||||
|
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||||
|
searchInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -439,11 +490,23 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
fun launchForAutofillResult(activity: Activity,
|
fun launchForAutofillResult(activity: Activity,
|
||||||
assistStructure: AssistStructure,
|
assistStructure: AssistStructure,
|
||||||
searchInfo: SearchInfo?) {
|
searchInfo: SearchInfo? = null) {
|
||||||
AutofillHelper.startActivityForAutofillResult(activity,
|
AutofillHelper.startActivityForAutofillResult(activity,
|
||||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||||
assistStructure,
|
assistStructure,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Registration Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
fun launchForRegistration(context: Context,
|
||||||
|
registerInfo: RegisterInfo? = null) {
|
||||||
|
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
|
||||||
|
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||||
|
registerInfo)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,7 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -69,10 +70,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
|
|
||||||
|
|
||||||
private var readOnly: Boolean = false
|
private var readOnly: Boolean = false
|
||||||
get() {
|
private var specialMode: SpecialMode = SpecialMode.DEFAULT
|
||||||
return field || selectionMode
|
|
||||||
}
|
|
||||||
private var selectionMode: Boolean = false
|
|
||||||
|
|
||||||
val isEmpty: Boolean
|
val isEmpty: Boolean
|
||||||
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
|
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
|
||||||
@@ -97,6 +95,12 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
nodeClickListener = null
|
||||||
|
onScrollListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -189,7 +193,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
activity?.intent?.let {
|
activity?.intent?.let {
|
||||||
selectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(it)
|
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh data
|
// Refresh data
|
||||||
@@ -266,14 +270,15 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun actionNodesCallback(nodes: List<Node>,
|
fun actionNodesCallback(nodes: List<Node>,
|
||||||
menuListener: NodesActionMenuListener?) : ActionMode.Callback {
|
menuListener: NodesActionMenuListener?,
|
||||||
|
actionModeCallback: ActionMode.Callback) : ActionMode.Callback {
|
||||||
|
|
||||||
return object : ActionMode.Callback {
|
return object : ActionMode.Callback {
|
||||||
|
|
||||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||||
nodeActionSelectionMode = false
|
nodeActionSelectionMode = false
|
||||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||||
return true
|
return actionModeCallback.onCreateActionMode(mode, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||||
@@ -303,7 +308,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
if (readOnly
|
if (readOnly
|
||||||
|| isASearchResult
|
|| isASearchResult
|
||||||
|| nodes.any { it.type == Type.GROUP }) {
|
|| nodes.any { it.type == Type.GROUP }) {
|
||||||
// TODO COPY For Group
|
// TODO Copy For Group
|
||||||
menu?.removeItem(R.id.menu_copy)
|
menu?.removeItem(R.id.menu_copy)
|
||||||
menu?.removeItem(R.id.menu_move)
|
menu?.removeItem(R.id.menu_move)
|
||||||
}
|
}
|
||||||
@@ -318,7 +323,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
// Add the number of items selected in title
|
// Add the number of items selected in title
|
||||||
mode?.title = nodes.size.toString()
|
mode?.title = nodes.size.toString()
|
||||||
|
|
||||||
return true
|
return actionModeCallback.onPrepareActionMode(mode, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
||||||
@@ -348,7 +353,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
nodeActionSelectionMode = false
|
nodeActionSelectionMode = false
|
||||||
returnValue
|
returnValue
|
||||||
}
|
}
|
||||||
else -> false
|
else -> actionModeCallback.onActionItemClicked(mode, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,6 +363,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
mAdapter?.unselectActionNodes()
|
mAdapter?.unselectActionNodes()
|
||||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||||
nodeActionSelectionMode = false
|
nodeActionSelectionMode = false
|
||||||
|
actionModeCallback.onDestroyActionMode(mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity to select entry in database and populate it in Magikeyboard
|
||||||
|
*/
|
||||||
|
class MagikeyboardLauncherActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
val database = Database.getInstance()
|
||||||
|
val readOnly = database.isReadOnly
|
||||||
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
|
database,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
// 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.launchForKeyboardSelectionResult(this)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
finish()
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,78 +21,82 @@ package com.kunzisoft.keepass.activities
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
import android.app.backup.BackupManager
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper.KEY_SEARCH_INFO
|
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
|
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
import com.kunzisoft.keepass.biometric.BiometricUnlockDatabaseHelper
|
||||||
|
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_KEY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.MASTER_PASSWORD_KEY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.MASTER_PASSWORD_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.FileDatabaseInfo
|
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||||
import kotlinx.android.synthetic.main.activity_password.*
|
import kotlinx.android.synthetic.main.activity_password.*
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
open class PasswordActivity : StylishActivity() {
|
open class PasswordActivity : SpecialModeActivity() {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
private var containerView: View? = null
|
|
||||||
private var filenameView: TextView? = null
|
private var filenameView: TextView? = null
|
||||||
private var passwordView: EditText? = null
|
private var passwordView: EditText? = null
|
||||||
private var keyFileSelectionView: KeyFileSelectionView? = null
|
private var keyFileSelectionView: KeyFileSelectionView? = null
|
||||||
private var confirmButtonView: Button? = null
|
private var confirmButtonView: Button? = null
|
||||||
private var checkboxPasswordView: CompoundButton? = null
|
private var checkboxPasswordView: CompoundButton? = null
|
||||||
private var checkboxKeyFileView: CompoundButton? = null
|
private var checkboxKeyFileView: CompoundButton? = null
|
||||||
private var checkboxDefaultDatabaseView: CompoundButton? = null
|
|
||||||
private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null
|
private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null
|
||||||
private var infoContainerView: ViewGroup? = null
|
private var infoContainerView: ViewGroup? = null
|
||||||
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
|
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
|
||||||
|
|
||||||
|
private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||||
|
|
||||||
private var mDatabaseFileUri: Uri? = null
|
private var mDatabaseFileUri: Uri? = null
|
||||||
private var mDatabaseKeyFileUri: Uri? = null
|
private var mDatabaseKeyFileUri: Uri? = null
|
||||||
|
|
||||||
private var mRememberKeyFile: Boolean = false
|
private var mRememberKeyFile: Boolean = false
|
||||||
private var mOpenFileHelper: OpenFileHelper? = null
|
private var mSelectFileHelper: SelectFileHelper? = null
|
||||||
|
|
||||||
private var mPermissionAsked = false
|
private var mPermissionAsked = false
|
||||||
private var readOnly: Boolean = false
|
private var readOnly: Boolean = false
|
||||||
@@ -107,9 +111,10 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mProgressDialogThread: ProgressDialogThread? = null
|
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||||
|
|
||||||
private var advancedUnlockedManager: AdvancedUnlockedManager? = null
|
private var advancedUnlockedManager: AdvancedUnlockedManager? = null
|
||||||
|
private var mAllowAutoOpenBiometricPrompt: Boolean = true
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -122,23 +127,22 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
containerView = findViewById(R.id.container)
|
|
||||||
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
||||||
filenameView = findViewById(R.id.filename)
|
filenameView = findViewById(R.id.filename)
|
||||||
passwordView = findViewById(R.id.password)
|
passwordView = findViewById(R.id.password)
|
||||||
keyFileSelectionView = findViewById(R.id.keyfile_selection)
|
keyFileSelectionView = findViewById(R.id.keyfile_selection)
|
||||||
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
||||||
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
||||||
checkboxDefaultDatabaseView = findViewById(R.id.default_database)
|
|
||||||
advancedUnlockInfoView = findViewById(R.id.biometric_info)
|
advancedUnlockInfoView = findViewById(R.id.biometric_info)
|
||||||
infoContainerView = findViewById(R.id.activity_password_info_container)
|
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||||
|
|
||||||
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
||||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
||||||
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||||
|
|
||||||
mOpenFileHelper = OpenFileHelper(this@PasswordActivity)
|
mSelectFileHelper = SelectFileHelper(this@PasswordActivity)
|
||||||
keyFileSelectionView?.apply {
|
keyFileSelectionView?.apply {
|
||||||
mOpenFileHelper?.openFileOnClickViewListener?.let {
|
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
||||||
setOnClickListener(it)
|
setOnClickListener(it)
|
||||||
setOnLongClickListener(it)
|
setOnLongClickListener(it)
|
||||||
}
|
}
|
||||||
@@ -165,8 +169,34 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
|
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
|
||||||
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
|
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
|
||||||
}
|
}
|
||||||
|
if (savedInstanceState?.containsKey(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT) == true) {
|
||||||
|
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
|
||||||
|
}
|
||||||
|
|
||||||
mProgressDialogThread = ProgressDialogThread(this).apply {
|
// Observe database file change
|
||||||
|
databaseFileViewModel.databaseFileLoaded.observe(this, Observer { databaseFile ->
|
||||||
|
// Force read only if the file does not exists
|
||||||
|
mForceReadOnly = databaseFile?.let {
|
||||||
|
!it.databaseFileExists
|
||||||
|
} ?: true
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
|
||||||
|
// Post init uri with KeyFile only if needed
|
||||||
|
val keyFileUri =
|
||||||
|
if (mRememberKeyFile
|
||||||
|
&& (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) {
|
||||||
|
databaseFile?.keyFileUri
|
||||||
|
} else {
|
||||||
|
mDatabaseKeyFileUri
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define title
|
||||||
|
filenameView?.text = databaseFile?.databaseAlias ?: ""
|
||||||
|
|
||||||
|
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
|
||||||
|
})
|
||||||
|
|
||||||
|
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
|
||||||
onActionFinish = { actionTask, result ->
|
onActionFinish = { actionTask, result ->
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
@@ -203,7 +233,7 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
result.data?.let { resultData ->
|
result.data?.let { resultData ->
|
||||||
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
||||||
masterPassword = resultData.getString(MASTER_PASSWORD_KEY)
|
masterPassword = resultData.getString(MASTER_PASSWORD_KEY)
|
||||||
keyFileUri = resultData.getParcelable(KEY_FILE_KEY)
|
keyFileUri = resultData.getParcelable(KEY_FILE_URI_KEY)
|
||||||
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
||||||
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
|
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
|
||||||
}
|
}
|
||||||
@@ -225,7 +255,7 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
||||||
resultError = "$resultError $resultMessage"
|
resultError = "$resultError $resultMessage"
|
||||||
}
|
}
|
||||||
Log.e(TAG, resultError, resultException)
|
Log.e(TAG, resultError)
|
||||||
Snackbar.make(activity_password_coordinator_layout,
|
Snackbar.make(activity_password_coordinator_layout,
|
||||||
resultError,
|
resultError,
|
||||||
Snackbar.LENGTH_LONG).asError().show()
|
Snackbar.LENGTH_LONG).asError().show()
|
||||||
@@ -255,42 +285,22 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun launchGroupActivity() {
|
private fun launchGroupActivity() {
|
||||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
GroupActivity.launch(this,
|
||||||
{
|
readOnly,
|
||||||
GroupActivity.launch(this@PasswordActivity,
|
{ onValidateSpecialMode() },
|
||||||
readOnly)
|
{ onCancelSpecialMode() },
|
||||||
},
|
{ onLaunchActivitySpecialMode() }
|
||||||
{
|
)
|
||||||
GroupActivity.launchForKeyboardSelection(this@PasswordActivity,
|
}
|
||||||
readOnly)
|
|
||||||
// Do not keep history
|
override fun onValidateSpecialMode() {
|
||||||
finish()
|
super.onValidateSpecialMode()
|
||||||
},
|
finish()
|
||||||
{ assistStructure ->
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
|
override fun onCancelSpecialMode() {
|
||||||
AutofillHelper.checkAutoSearchInfo(this,
|
super.onCancelSpecialMode()
|
||||||
Database.getInstance(),
|
finish()
|
||||||
searchInfo,
|
|
||||||
{ items ->
|
|
||||||
// Response is build
|
|
||||||
AutofillHelper.buildResponse(this, items)
|
|
||||||
finish()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Here no search info found
|
|
||||||
GroupActivity.launchForAutofillResult(this@PasswordActivity,
|
|
||||||
assistStructure,
|
|
||||||
null,
|
|
||||||
readOnly)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Simply close if database not opened, normally not happened
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val onEditorActionListener = object : TextView.OnEditorActionListener {
|
private val onEditorActionListener = object : TextView.OnEditorActionListener {
|
||||||
@@ -304,95 +314,48 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
|
||||||
if (Database.getInstance().loaded)
|
|
||||||
launchGroupActivity()
|
|
||||||
|
|
||||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
|
||||||
|
|
||||||
// If the database isn't accessible make sure to clear the password field, if it
|
|
||||||
// was saved in the instance state
|
|
||||||
if (Database.getInstance().loaded) {
|
|
||||||
clearCredentialsViews()
|
|
||||||
}
|
|
||||||
|
|
||||||
// For check shutdown
|
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
mProgressDialogThread?.registerProgressTask()
|
if (Database.getInstance().loaded) {
|
||||||
|
launchGroupActivity()
|
||||||
initUriFromIntent()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
|
|
||||||
mDatabaseKeyFileUri?.let {
|
|
||||||
outState.putString(KEY_KEYFILE, it.toString())
|
|
||||||
}
|
|
||||||
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initUriFromIntent() {
|
|
||||||
/*
|
|
||||||
// "canXrite" doesn't work with Google Drive, don't really know why?
|
|
||||||
mForceReadOnly = mDatabaseFileUri?.let {
|
|
||||||
!FileDatabaseInfo(this, it).canWrite
|
|
||||||
} ?: false
|
|
||||||
*/
|
|
||||||
mForceReadOnly = mDatabaseFileUri?.let {
|
|
||||||
!FileDatabaseInfo(this, it).exists
|
|
||||||
} ?: true
|
|
||||||
|
|
||||||
// Post init uri with KeyFile if needed
|
|
||||||
if (mRememberKeyFile && (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) {
|
|
||||||
// Retrieve KeyFile in a thread
|
|
||||||
mDatabaseFileUri?.let { databaseUri ->
|
|
||||||
FileDatabaseHistoryAction.getInstance(applicationContext)
|
|
||||||
.getKeyFileUriByDatabaseUri(databaseUri) {
|
|
||||||
onPostInitUri(databaseUri, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
onPostInitUri(mDatabaseFileUri, mDatabaseKeyFileUri)
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||||
|
|
||||||
|
// If the database isn't accessible make sure to clear the password field, if it
|
||||||
|
// was saved in the instance state
|
||||||
|
if (Database.getInstance().loaded) {
|
||||||
|
clearCredentialsViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||||
|
|
||||||
|
// Back to previous keyboard is setting activated
|
||||||
|
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this)) {
|
||||||
|
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow auto open prompt if lock become when UI visible
|
||||||
|
mAllowAutoOpenBiometricPrompt = if (LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true)
|
||||||
|
false
|
||||||
|
else
|
||||||
|
mAllowAutoOpenBiometricPrompt
|
||||||
|
mDatabaseFileUri?.let { databaseFileUri ->
|
||||||
|
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPermission()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
||||||
// Define title
|
|
||||||
databaseFileUri?.let {
|
|
||||||
FileDatabaseInfo(this, it).retrieveDatabaseTitle { title ->
|
|
||||||
filenameView?.text = title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define Key File text
|
// Define Key File text
|
||||||
if (mRememberKeyFile) {
|
if (mRememberKeyFile) {
|
||||||
populateKeyFileTextView(keyFileUri)
|
populateKeyFileTextView(keyFileUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define listeners for default database checkbox and validate button
|
// Define listener for validate button
|
||||||
checkboxDefaultDatabaseView?.setOnCheckedChangeListener { _, isChecked ->
|
|
||||||
var newDefaultFileUri: Uri? = null
|
|
||||||
if (isChecked) {
|
|
||||||
newDefaultFileUri = databaseFileUri ?: newDefaultFileUri
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferencesUtil.saveDefaultDatabasePath(this, newDefaultFileUri)
|
|
||||||
|
|
||||||
val backupManager = BackupManager(this@PasswordActivity)
|
|
||||||
backupManager.dataChanged()
|
|
||||||
}
|
|
||||||
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
|
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
|
||||||
|
|
||||||
// Retrieve settings for default database
|
|
||||||
val defaultFilename = PreferencesUtil.getDefaultDatabasePath(this)
|
|
||||||
if (databaseFileUri != null
|
|
||||||
&& databaseFileUri.path != null && databaseFileUri.path!!.isNotEmpty()
|
|
||||||
&& databaseFileUri == UriUtil.parse(defaultFilename)) {
|
|
||||||
checkboxDefaultDatabaseView?.isChecked = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// If Activity is launch with a password and want to open directly
|
// If Activity is launch with a password and want to open directly
|
||||||
val intent = intent
|
val intent = intent
|
||||||
val password = intent.getStringExtra(KEY_PASSWORD)
|
val password = intent.getStringExtra(KEY_PASSWORD)
|
||||||
@@ -406,11 +369,10 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
|
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
|
||||||
} else {
|
} else {
|
||||||
// Init Biometric elements
|
// Init Biometric elements
|
||||||
var biometricInitialize = false
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
if (PreferencesUtil.isBiometricUnlockEnable(this)) {
|
if (PreferencesUtil.isBiometricUnlockEnable(this)) {
|
||||||
|
if (advancedUnlockedManager == null
|
||||||
if (advancedUnlockedManager == null && databaseFileUri != null) {
|
&& databaseFileUri != null) {
|
||||||
advancedUnlockedManager = AdvancedUnlockedManager(this,
|
advancedUnlockedManager = AdvancedUnlockedManager(this,
|
||||||
databaseFileUri,
|
databaseFileUri,
|
||||||
advancedUnlockInfoView,
|
advancedUnlockInfoView,
|
||||||
@@ -436,13 +398,16 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
advancedUnlockedManager?.isBiometricPromptAutoOpenEnable =
|
||||||
|
mAllowAutoOpenBiometricPrompt && mProgressDatabaseTaskProvider?.isBinded() != true
|
||||||
advancedUnlockedManager?.checkBiometricAvailability()
|
advancedUnlockedManager?.checkBiometricAvailability()
|
||||||
biometricInitialize = true
|
|
||||||
} else {
|
} else {
|
||||||
|
advancedUnlockInfoView?.visibility = View.GONE
|
||||||
advancedUnlockedManager?.destroy()
|
advancedUnlockedManager?.destroy()
|
||||||
|
advancedUnlockedManager = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!biometricInitialize) {
|
if (advancedUnlockedManager == null) {
|
||||||
checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
||||||
}
|
}
|
||||||
checkboxKeyFileView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
checkboxKeyFileView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
||||||
@@ -495,16 +460,28 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
mProgressDialogThread?.unregisterProgressTask()
|
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
advancedUnlockedManager?.destroy()
|
||||||
|
advancedUnlockedManager = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reinit locking activity UI variable
|
||||||
|
LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
||||||
|
mAllowAutoOpenBiometricPrompt = true
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
|
||||||
advancedUnlockedManager?.destroy()
|
mDatabaseKeyFileUri?.let {
|
||||||
|
outState.putString(KEY_KEYFILE, it.toString())
|
||||||
}
|
}
|
||||||
super.onDestroy()
|
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
||||||
|
outState.putBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT, false)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||||
@@ -540,15 +517,25 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
clearCredentialsViews()
|
clearCredentialsViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
databaseFileUri?.let { databaseUri ->
|
if (readOnly && (
|
||||||
// Show the progress dialog and load the database
|
mSpecialMode == SpecialMode.SAVE
|
||||||
showProgressDialogAndLoadDatabase(
|
|| mSpecialMode == SpecialMode.REGISTRATION)
|
||||||
databaseUri,
|
) {
|
||||||
password,
|
Log.e(TAG, getString(R.string.autofill_read_only_save))
|
||||||
keyFileUri,
|
Snackbar.make(activity_password_coordinator_layout,
|
||||||
readOnly,
|
R.string.autofill_read_only_save,
|
||||||
cipherDatabaseEntity,
|
Snackbar.LENGTH_LONG).asError().show()
|
||||||
false)
|
} else {
|
||||||
|
databaseFileUri?.let { databaseUri ->
|
||||||
|
// Show the progress dialog and load the database
|
||||||
|
showProgressDialogAndLoadDatabase(
|
||||||
|
databaseUri,
|
||||||
|
password,
|
||||||
|
keyFileUri,
|
||||||
|
readOnly,
|
||||||
|
cipherDatabaseEntity,
|
||||||
|
false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,7 +545,7 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
cipherDatabaseEntity: CipherDatabaseEntity?,
|
cipherDatabaseEntity: CipherDatabaseEntity?,
|
||||||
fixDuplicateUUID: Boolean) {
|
fixDuplicateUUID: Boolean) {
|
||||||
mProgressDialogThread?.startDatabaseLoad(
|
mProgressDatabaseTaskProvider?.startDatabaseLoad(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
password,
|
password,
|
||||||
keyFile,
|
keyFile,
|
||||||
@@ -578,14 +565,15 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
val inflater = menuInflater
|
val inflater = menuInflater
|
||||||
// Read menu
|
// Read menu
|
||||||
inflater.inflate(R.menu.open_file, menu)
|
inflater.inflate(R.menu.open_file, menu)
|
||||||
|
|
||||||
if (mForceReadOnly) {
|
if (mForceReadOnly) {
|
||||||
menu.removeItem(R.id.menu_open_file_read_mode_key)
|
menu.removeItem(R.id.menu_open_file_read_mode_key)
|
||||||
} else {
|
} else {
|
||||||
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
|
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuUtil.defaultMenuInflater(inflater, menu)
|
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||||
|
MenuUtil.defaultMenuInflater(inflater, menu)
|
||||||
|
}
|
||||||
|
|
||||||
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
// biometric menu
|
// biometric menu
|
||||||
@@ -594,15 +582,13 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
|
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
|
|
||||||
launchEducation(menu) {
|
launchEducation(menu)
|
||||||
launchCheckPermission()
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check permission
|
// Check permission
|
||||||
private fun launchCheckPermission() {
|
private fun checkPermission() {
|
||||||
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
val permissions = arrayOf(writePermission)
|
val permissions = arrayOf(writePermission)
|
||||||
if (Build.VERSION.SDK_INT >= 23
|
if (Build.VERSION.SDK_INT >= 23
|
||||||
@@ -632,26 +618,25 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
|
|
||||||
// To fix multiple view education
|
// To fix multiple view education
|
||||||
private var performedEductionInProgress = false
|
private var performedEductionInProgress = false
|
||||||
private fun launchEducation(menu: Menu, onEducationFinished: (()-> Unit)? = null) {
|
private fun launchEducation(menu: Menu) {
|
||||||
if (!performedEductionInProgress) {
|
if (!performedEductionInProgress) {
|
||||||
performedEductionInProgress = true
|
performedEductionInProgress = true
|
||||||
// Show education views
|
// Show education views
|
||||||
Handler().post { performedNextEducation(PasswordActivityEducation(this), menu, onEducationFinished) }
|
Handler(Looper.getMainLooper()).post { performedNextEducation(PasswordActivityEducation(this), menu) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
|
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
|
||||||
menu: Menu,
|
menu: Menu) {
|
||||||
onEducationFinished: (()-> Unit)? = null) {
|
|
||||||
val educationToolbar = toolbar
|
val educationToolbar = toolbar
|
||||||
val unlockEducationPerformed = educationToolbar != null
|
val unlockEducationPerformed = educationToolbar != null
|
||||||
&& passwordActivityEducation.checkAndPerformedUnlockEducation(
|
&& passwordActivityEducation.checkAndPerformedUnlockEducation(
|
||||||
educationToolbar,
|
educationToolbar,
|
||||||
{
|
{
|
||||||
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
})
|
})
|
||||||
if (!unlockEducationPerformed) {
|
if (!unlockEducationPerformed) {
|
||||||
val readOnlyEducationPerformed =
|
val readOnlyEducationPerformed =
|
||||||
@@ -659,31 +644,31 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
|
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
|
||||||
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
|
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
|
||||||
{
|
{
|
||||||
onOptionsItemSelected(menu.findItem(R.id.menu_open_file_read_mode_key))
|
try {
|
||||||
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
|
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, onEducationFinished)
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!readOnlyEducationPerformed) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||||
val biometricCanAuthenticate = BiometricManager.from(this).canAuthenticate()
|
&& !readOnlyEducationPerformed) {
|
||||||
val biometricEducationPerformed =
|
val biometricCanAuthenticate = BiometricUnlockDatabaseHelper.canAuthenticate(this)
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
PreferencesUtil.isBiometricUnlockEnable(applicationContext)
|
||||||
&& PreferencesUtil.isBiometricUnlockEnable(applicationContext)
|
|
||||||
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
||||||
&& advancedUnlockInfoView != null && advancedUnlockInfoView?.unlockIconImageView != null
|
&& advancedUnlockInfoView != null && advancedUnlockInfoView?.visibility == View.VISIBLE
|
||||||
|
&& advancedUnlockInfoView?.unlockIconImageView != null
|
||||||
&& passwordActivityEducation.checkAndPerformedBiometricEducation(advancedUnlockInfoView?.unlockIconImageView!!,
|
&& passwordActivityEducation.checkAndPerformedBiometricEducation(advancedUnlockInfoView?.unlockIconImageView!!,
|
||||||
{
|
{
|
||||||
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(passwordActivityEducation, menu, onEducationFinished)
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!biometricEducationPerformed) {
|
|
||||||
onEducationFinished?.invoke()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -721,13 +706,15 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
data: Intent?) {
|
data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
|
mAllowAutoOpenBiometricPrompt = false
|
||||||
|
|
||||||
// To get entry in result
|
// To get entry in result
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
var keyFileResult = false
|
var keyFileResult = false
|
||||||
mOpenFileHelper?.let {
|
mSelectFileHelper?.let {
|
||||||
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
||||||
) { uri ->
|
) { uri ->
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
@@ -739,9 +726,12 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
if (!keyFileResult) {
|
if (!keyFileResult) {
|
||||||
// this block if not a key file response
|
// this block if not a key file response
|
||||||
when (resultCode) {
|
when (resultCode) {
|
||||||
LockingActivity.RESULT_EXIT_LOCK, Activity.RESULT_CANCELED -> {
|
LockingActivity.RESULT_EXIT_LOCK -> {
|
||||||
|
clearCredentialsViews()
|
||||||
|
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this))
|
||||||
|
}
|
||||||
|
Activity.RESULT_CANCELED -> {
|
||||||
clearCredentialsViews()
|
clearCredentialsViews()
|
||||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -760,6 +750,8 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"
|
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"
|
||||||
private const val WRITE_EXTERNAL_STORAGE_REQUEST = 647
|
private const val WRITE_EXTERNAL_STORAGE_REQUEST = 647
|
||||||
|
|
||||||
|
private const val ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT = "ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT"
|
||||||
|
|
||||||
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
|
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
|
||||||
intentBuildLauncher: (Intent) -> Unit) {
|
intentBuildLauncher: (Intent) -> Unit) {
|
||||||
val intent = Intent(activity, PasswordActivity::class.java)
|
val intent = Intent(activity, PasswordActivity::class.java)
|
||||||
@@ -776,15 +768,33 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
fun launch(
|
fun launch(activity: Activity,
|
||||||
activity: Activity,
|
databaseFile: Uri,
|
||||||
databaseFile: Uri,
|
keyFile: Uri?) {
|
||||||
keyFile: Uri?) {
|
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
activity.startActivity(intent)
|
activity.startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Share Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launchForSearchResult(activity: Activity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
searchInfo: SearchInfo) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
|
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Keyboard Launch
|
* Keyboard Launch
|
||||||
@@ -792,12 +802,15 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
fun launchForKeyboardResult(
|
fun launchForKeyboardResult(activity: Activity,
|
||||||
activity: Activity,
|
databaseFile: Uri,
|
||||||
databaseFile: Uri,
|
keyFile: Uri?,
|
||||||
keyFile: Uri?) {
|
searchInfo: SearchInfo?) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
EntrySelectionHelper.startActivityForEntrySelection(activity, intent)
|
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
searchInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -809,22 +822,90 @@ open class PasswordActivity : StylishActivity() {
|
|||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
fun launchForAutofillResult(
|
fun launchForAutofillResult(activity: Activity,
|
||||||
activity: Activity,
|
databaseFile: Uri,
|
||||||
databaseFile: Uri,
|
keyFile: Uri?,
|
||||||
keyFile: Uri?,
|
assistStructure: AssistStructure,
|
||||||
assistStructure: AssistStructure?,
|
searchInfo: SearchInfo?) {
|
||||||
searchInfo: SearchInfo?) {
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
if (assistStructure != null) {
|
AutofillHelper.startActivityForAutofillResult(
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
activity,
|
||||||
AutofillHelper.startActivityForAutofillResult(
|
intent,
|
||||||
activity,
|
assistStructure,
|
||||||
intent,
|
searchInfo)
|
||||||
assistStructure,
|
}
|
||||||
searchInfo)
|
}
|
||||||
}
|
|
||||||
} else {
|
/*
|
||||||
launch(activity, databaseFile, keyFile)
|
* -------------------------
|
||||||
|
* Registration Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
fun launchForRegistration(activity: Activity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
registerInfo: RegisterInfo?) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
registerInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Global Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
fun launch(activity: Activity,
|
||||||
|
databaseUri: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
||||||
|
onCancelSpecialMode: () -> Unit,
|
||||||
|
onLaunchActivitySpecialMode: () -> Unit) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||||
|
{
|
||||||
|
PasswordActivity.launch(activity,
|
||||||
|
databaseUri, keyFile)
|
||||||
|
},
|
||||||
|
{ searchInfo -> // Search Action
|
||||||
|
PasswordActivity.launchForSearchResult(activity,
|
||||||
|
databaseUri, keyFile,
|
||||||
|
searchInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
{ // Save Action
|
||||||
|
// Not directly used, a search is performed before
|
||||||
|
},
|
||||||
|
{ searchInfo -> // Keyboard Selection Action
|
||||||
|
PasswordActivity.launchForKeyboardResult(activity,
|
||||||
|
databaseUri, keyFile,
|
||||||
|
searchInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
{ searchInfo, assistStructure -> // Autofill Selection Action
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
PasswordActivity.launchForAutofillResult(activity,
|
||||||
|
databaseUri, keyFile,
|
||||||
|
assistStructure,
|
||||||
|
searchInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
} else {
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ registerInfo -> // Registration Action
|
||||||
|
PasswordActivity.launchForRegistration(activity,
|
||||||
|
databaseUri, keyFile,
|
||||||
|
registerInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
fileNoFoundAction(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,16 +25,19 @@ import android.content.DialogInterface
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
|
|
||||||
class AssignMasterKeyDialogFragment : DialogFragment() {
|
class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||||
@@ -56,7 +59,11 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
private var mListener: AssignPasswordDialogListener? = null
|
private var mListener: AssignPasswordDialogListener? = null
|
||||||
|
|
||||||
private var mOpenFileHelper: OpenFileHelper? = null
|
private var mSelectFileHelper: SelectFileHelper? = null
|
||||||
|
|
||||||
|
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
|
||||||
|
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
||||||
|
private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null
|
||||||
|
|
||||||
private val passwordTextWatcher = object : TextWatcher {
|
private val passwordTextWatcher = object : TextWatcher {
|
||||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||||
@@ -85,6 +92,17 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
mEmptyPasswordConfirmationDialog?.dismiss()
|
||||||
|
mEmptyPasswordConfirmationDialog = null
|
||||||
|
mNoKeyConfirmationDialog?.dismiss()
|
||||||
|
mNoKeyConfirmationDialog = null
|
||||||
|
mEmptyKeyFileConfirmationDialog?.dismiss()
|
||||||
|
mEmptyKeyFileConfirmationDialog = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
|
|
||||||
@@ -99,11 +117,15 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
rootView = inflater.inflate(R.layout.fragment_set_password, null)
|
rootView = inflater.inflate(R.layout.fragment_set_password, null)
|
||||||
builder.setView(rootView)
|
builder.setView(rootView)
|
||||||
.setTitle(R.string.assign_master_key)
|
|
||||||
// Add action buttons
|
// Add action buttons
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
|
||||||
|
val credentialsInfo: ImageView? = rootView?.findViewById(R.id.credentials_information)
|
||||||
|
credentialsInfo?.setOnClickListener {
|
||||||
|
UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
|
||||||
|
}
|
||||||
|
|
||||||
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
||||||
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
|
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
|
||||||
passwordView = rootView?.findViewById(R.id.pass_password)
|
passwordView = rootView?.findViewById(R.id.pass_password)
|
||||||
@@ -113,10 +135,10 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
||||||
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
||||||
|
|
||||||
mOpenFileHelper = OpenFileHelper(this)
|
mSelectFileHelper = SelectFileHelper(this)
|
||||||
keyFileSelectionView?.apply {
|
keyFileSelectionView?.apply {
|
||||||
setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener)
|
setOnClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
||||||
setOnLongClickListener(mOpenFileHelper?.openFileOnClickViewListener)
|
setOnLongClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
val dialog = builder.create()
|
val dialog = builder.create()
|
||||||
@@ -129,7 +151,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
mMasterPassword = ""
|
mMasterPassword = ""
|
||||||
mKeyFile = null
|
mKeyFile = null
|
||||||
|
|
||||||
var error = verifyPassword() || verifyFile()
|
var error = verifyPassword() || verifyKeyFile()
|
||||||
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
|
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
|
||||||
error = true
|
error = true
|
||||||
if (allowNoMasterKey)
|
if (allowNoMasterKey)
|
||||||
@@ -199,7 +221,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
return error
|
return error
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyFile(): Boolean {
|
private fun verifyKeyFile(): Boolean {
|
||||||
var error = false
|
var error = false
|
||||||
if (keyFileCheckBox != null
|
if (keyFileCheckBox != null
|
||||||
&& keyFileCheckBox!!.isChecked) {
|
&& keyFileCheckBox!!.isChecked) {
|
||||||
@@ -219,7 +241,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
val builder = AlertDialog.Builder(it)
|
val builder = AlertDialog.Builder(it)
|
||||||
builder.setMessage(R.string.warning_empty_password)
|
builder.setMessage(R.string.warning_empty_password)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
if (!verifyFile()) {
|
if (!verifyKeyFile()) {
|
||||||
mListener?.onAssignKeyDialogPositiveClick(
|
mListener?.onAssignKeyDialogPositiveClick(
|
||||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
passwordCheckBox!!.isChecked, mMasterPassword,
|
||||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
keyFileCheckBox!!.isChecked, mKeyFile)
|
||||||
@@ -227,7 +249,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
builder.create().show()
|
mEmptyPasswordConfirmationDialog = builder.create()
|
||||||
|
mEmptyPasswordConfirmationDialog?.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,18 +265,44 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
this@AssignMasterKeyDialogFragment.dismiss()
|
this@AssignMasterKeyDialogFragment.dismiss()
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
builder.create().show()
|
mNoKeyConfirmationDialog = builder.create()
|
||||||
|
mNoKeyConfirmationDialog?.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showEmptyKeyFileConfirmationDialog() {
|
||||||
|
activity?.let {
|
||||||
|
val builder = AlertDialog.Builder(it)
|
||||||
|
builder.setMessage(SpannableStringBuilder().apply {
|
||||||
|
append(getString(R.string.warning_empty_keyfile))
|
||||||
|
append("\n\n")
|
||||||
|
append(getString(R.string.warning_empty_keyfile_explanation))
|
||||||
|
append("\n\n")
|
||||||
|
append(getString(R.string.warning_sure_add_file))
|
||||||
|
})
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
keyFileCheckBox?.isChecked = false
|
||||||
|
keyFileSelectionView?.uri = null
|
||||||
|
}
|
||||||
|
mEmptyKeyFileConfirmationDialog = builder.create()
|
||||||
|
mEmptyKeyFileConfirmationDialog?.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||||
) { uri ->
|
|
||||||
uri?.let { pathUri ->
|
uri?.let { pathUri ->
|
||||||
keyFileCheckBox?.isChecked = true
|
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
||||||
keyFileSelectionView?.uri = pathUri
|
keyFileSelectionView?.error = null
|
||||||
|
keyFileCheckBox?.isChecked = true
|
||||||
|
keyFileSelectionView?.uri = pathUri
|
||||||
|
if (lengthFile <= 0L) {
|
||||||
|
showEmptyKeyFileConfirmationDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ class DatePickerFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
// Create a new instance of DatePickerDialog and return it
|
// Create a new instance of DatePickerDialog and return it
|
||||||
return context?.let {
|
return context?.let {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
|||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
||||||
|
|
||||||
class DeleteNodesDialogFragment : DialogFragment() {
|
open class DeleteNodesDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
private var mNodesToDelete: List<Node> = ArrayList()
|
private var mNodesToDelete: List<Node> = ArrayList()
|
||||||
private var mListener: DeleteNodeListener? = null
|
private var mListener: DeleteNodeListener? = null
|
||||||
@@ -46,6 +46,15 @@ class DeleteNodesDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun retrieveMessage(): String {
|
||||||
|
return getString(R.string.warning_permanently_delete_nodes)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
@@ -63,11 +72,11 @@ class DeleteNodesDialogFragment : DialogFragment() {
|
|||||||
// Use the Builder class for convenient dialog construction
|
// Use the Builder class for convenient dialog construction
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|
||||||
builder.setMessage(getString(R.string.warning_permanently_delete_nodes))
|
builder.setMessage(retrieveMessage())
|
||||||
builder.setPositiveButton(android.R.string.yes) { _, _ ->
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
mListener?.permanentlyDeleteNodes(mNodesToDelete)
|
mListener?.permanentlyDeleteNodes(mNodesToDelete)
|
||||||
}
|
}
|
||||||
builder.setNegativeButton(android.R.string.no) { _, _ -> dismiss() }
|
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||||
// Create the AlertDialog object and return it
|
// Create the AlertDialog object and return it
|
||||||
return builder.create()
|
return builder.create()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||||
|
|
||||||
|
class EmptyRecycleBinDialogFragment : DeleteNodesDialogFragment() {
|
||||||
|
|
||||||
|
override fun retrieveMessage(): String {
|
||||||
|
return getString(R.string.warning_empty_recycle_bin)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getInstance(nodesToDelete: List<Node>): EmptyRecycleBinDialogFragment {
|
||||||
|
return EmptyRecycleBinDialogFragment().apply {
|
||||||
|
arguments = getBundleFromListNodes(nodesToDelete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
|
import com.kunzisoft.keepass.model.Field
|
||||||
|
|
||||||
|
|
||||||
|
class EntryCustomFieldDialogFragment: DialogFragment() {
|
||||||
|
|
||||||
|
private var oldField: Field? = null
|
||||||
|
|
||||||
|
private var entryCustomFieldListener: EntryCustomFieldListener? = null
|
||||||
|
|
||||||
|
private var customFieldLabelContainer: TextInputLayout? = null
|
||||||
|
private var customFieldLabel: TextView? = null
|
||||||
|
private var customFieldDeleteButton: ImageView? = null
|
||||||
|
private var customFieldProtectionButton: CompoundButton? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
try {
|
||||||
|
entryCustomFieldListener = context as EntryCustomFieldListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
// The activity doesn't implement the interface, throw exception
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + EntryCustomFieldListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
entryCustomFieldListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
val root = activity.layoutInflater.inflate(R.layout.fragment_entry_new_field, null)
|
||||||
|
customFieldLabelContainer = root?.findViewById(R.id.entry_custom_field_label_container)
|
||||||
|
customFieldLabel = root?.findViewById(R.id.entry_custom_field_label)
|
||||||
|
customFieldDeleteButton = root?.findViewById(R.id.entry_custom_field_delete)
|
||||||
|
customFieldProtectionButton = root?.findViewById(R.id.entry_custom_field_protection)
|
||||||
|
|
||||||
|
oldField = arguments?.getParcelable(KEY_FIELD)
|
||||||
|
oldField?.let { oldCustomField ->
|
||||||
|
customFieldLabel?.text = oldCustomField.name
|
||||||
|
customFieldProtectionButton?.isChecked = oldCustomField.protectedValue.isProtected
|
||||||
|
|
||||||
|
customFieldDeleteButton?.visibility = View.VISIBLE
|
||||||
|
customFieldDeleteButton?.setOnClickListener {
|
||||||
|
entryCustomFieldListener?.onDeleteCustomFieldApproved(oldCustomField)
|
||||||
|
(dialog as AlertDialog?)?.dismiss()
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
customFieldDeleteButton?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
builder.setView(root)
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
val dialogCreated = builder.create()
|
||||||
|
|
||||||
|
customFieldLabel?.requestFocus()
|
||||||
|
customFieldLabel?.imeOptions = EditorInfo.IME_ACTION_DONE
|
||||||
|
customFieldLabel?.setOnEditorActionListener { _, actionId, _ ->
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||||
|
approveIfValid()
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogCreated.window?.setSoftInputMode(SOFT_INPUT_STATE_VISIBLE)
|
||||||
|
return dialogCreated
|
||||||
|
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// To prevent auto dismiss
|
||||||
|
val d = dialog as AlertDialog?
|
||||||
|
if (d != null) {
|
||||||
|
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
|
||||||
|
positiveButton.setOnClickListener {
|
||||||
|
approveIfValid()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun approveIfValid() {
|
||||||
|
if (isValid()) {
|
||||||
|
oldField?.let {
|
||||||
|
// New property with old value
|
||||||
|
entryCustomFieldListener?.onEditCustomFieldApproved(it,
|
||||||
|
Field(customFieldLabel?.text?.toString() ?: "",
|
||||||
|
ProtectedString(customFieldProtectionButton?.isChecked == true,
|
||||||
|
it.protectedValue.stringValue))
|
||||||
|
)
|
||||||
|
} ?: run {
|
||||||
|
entryCustomFieldListener?.onNewCustomFieldApproved(
|
||||||
|
Field(customFieldLabel?.text?.toString() ?: "",
|
||||||
|
ProtectedString(customFieldProtectionButton?.isChecked == true))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
(dialog as AlertDialog?)?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isValid(): Boolean {
|
||||||
|
return if (customFieldLabel?.text?.toString()?.isNotEmpty() != true) {
|
||||||
|
setError(R.string.error_string_key)
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
setError(null)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setError(@StringRes errorId: Int?) {
|
||||||
|
customFieldLabelContainer?.error = if (errorId == null) null else {
|
||||||
|
requireContext().getString(errorId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntryCustomFieldListener {
|
||||||
|
fun onNewCustomFieldApproved(newField: Field)
|
||||||
|
fun onEditCustomFieldApproved(oldField: Field, newField: Field)
|
||||||
|
fun onDeleteCustomFieldApproved(oldField: Field)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val KEY_FIELD = "KEY_FIELD"
|
||||||
|
|
||||||
|
fun getInstance(): EntryCustomFieldDialogFragment {
|
||||||
|
return EntryCustomFieldDialogFragment()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getInstance(field: Field): EntryCustomFieldDialogFragment {
|
||||||
|
return EntryCustomFieldDialogFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putParcelable(KEY_FIELD, field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ import android.widget.TextView
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
class BrowserDialogFragment : DialogFragment() {
|
class FileManagerDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
@@ -41,15 +41,8 @@ class BrowserDialogFragment : DialogFragment() {
|
|||||||
val textDescription = root.findViewById<TextView>(R.id.file_manager_install_description)
|
val textDescription = root.findViewById<TextView>(R.id.file_manager_install_description)
|
||||||
textDescription.text = getString(R.string.file_manager_install_description)
|
textDescription.text = getString(R.string.file_manager_install_description)
|
||||||
|
|
||||||
val market = root.findViewById<Button>(R.id.file_manager_install_play_store)
|
root.findViewById<Button>(R.id.file_manager_button).setOnClickListener {
|
||||||
market.setOnClickListener {
|
UriUtil.gotoUrl(requireContext(), R.string.file_manager_explanation_url)
|
||||||
UriUtil.gotoUrl(context!!, R.string.file_manager_play_store)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
val web = root.findViewById<Button>(R.id.file_manager_install_f_droid)
|
|
||||||
web.setOnClickListener {
|
|
||||||
UriUtil.gotoUrl(context!!, R.string.file_manager_f_droid)
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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.fragment.app.DialogFragment
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Button
|
import android.widget.*
|
||||||
import android.widget.CompoundButton
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.SeekBar
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.password.PasswordGenerator
|
import com.kunzisoft.keepass.password.PasswordGenerator
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||||
|
|
||||||
class GeneratePasswordDialogFragment : DialogFragment() {
|
class GeneratePasswordDialogFragment : DialogFragment() {
|
||||||
@@ -64,6 +62,11 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
@@ -73,6 +76,15 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
passwordInputLayoutView = root?.findViewById(R.id.password_input_layout)
|
passwordInputLayoutView = root?.findViewById(R.id.password_input_layout)
|
||||||
passwordView = root?.findViewById(R.id.password)
|
passwordView = root?.findViewById(R.id.password)
|
||||||
passwordView?.applyFontVisibility()
|
passwordView?.applyFontVisibility()
|
||||||
|
val passwordCopyView: ImageView? = root?.findViewById(R.id.password_copy_button)
|
||||||
|
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)
|
lengthTextView = root?.findViewById(R.id.length)
|
||||||
|
|
||||||
@@ -167,7 +179,6 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
var password = ""
|
var password = ""
|
||||||
try {
|
try {
|
||||||
val length = Integer.valueOf(root?.findViewById<EditText>(R.id.length)?.text.toString())
|
val length = Integer.valueOf(root?.findViewById<EditText>(R.id.length)?.text.toString())
|
||||||
|
|
||||||
password = PasswordGenerator(resources).generatePassword(length,
|
password = PasswordGenerator(resources).generatePassword(length,
|
||||||
uppercaseBox?.isChecked == true,
|
uppercaseBox?.isChecked == true,
|
||||||
lowercaseBox?.isChecked == true,
|
lowercaseBox?.isChecked == true,
|
||||||
@@ -178,6 +189,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
specialsBox?.isChecked == true,
|
specialsBox?.isChecked == true,
|
||||||
bracketsBox?.isChecked == true,
|
bracketsBox?.isChecked == true,
|
||||||
extendedBox?.isChecked == true)
|
extendedBox?.isChecked == true)
|
||||||
|
passwordInputLayoutView?.error = null
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) {
|
||||||
passwordInputLayoutView?.error = getString(R.string.error_wrong_length)
|
passwordInputLayoutView?.error = getString(R.string.error_wrong_length)
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
@@ -193,7 +205,6 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val KEY_PASSWORD_ID = "KEY_PASSWORD_ID"
|
const val KEY_PASSWORD_ID = "KEY_PASSWORD_ID"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,11 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
throw ClassCastException(context.toString()
|
throw ClassCastException(context.toString()
|
||||||
+ " must implement " + GroupEditDialogFragment::class.java.name)
|
+ " must implement " + GroupEditDialogFragment::class.java.name)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
editGroupListener = null
|
||||||
|
super.onDetach()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
@@ -130,9 +134,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
}
|
}
|
||||||
|
|
||||||
iconButtonView?.setOnClickListener { _ ->
|
iconButtonView?.setOnClickListener { _ ->
|
||||||
fragmentManager?.let {
|
IconPickerDialogFragment().show(parentFragmentManager, "IconPickerDialogFragment")
|
||||||
IconPickerDialogFragment().show(it, "IconPickerDialogFragment")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.create()
|
return builder.create()
|
||||||
|
|||||||
@@ -24,17 +24,18 @@ import android.content.Context
|
|||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.core.widget.ImageViewCompat
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.BaseAdapter
|
import android.widget.BaseAdapter
|
||||||
import android.widget.GridView
|
import android.widget.GridView
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.widget.ImageViewCompat
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
import com.kunzisoft.keepass.icons.IconPack
|
import com.kunzisoft.keepass.icons.IconPack
|
||||||
import com.kunzisoft.keepass.icons.IconPackChooser
|
import com.kunzisoft.keepass.icons.IconPackChooser
|
||||||
@@ -56,11 +57,16 @@ class IconPickerDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
iconPickerListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|
||||||
iconPack = IconPackChooser.getSelectedIconPack(context!!)
|
iconPack = IconPackChooser.getSelectedIconPack(requireContext())
|
||||||
|
|
||||||
// Inflate and set the layout for the dialog
|
// Inflate and set the layout for the dialog
|
||||||
// Pass null as the parent view because its going in the dialog layout
|
// Pass null as the parent view because its going in the dialog layout
|
||||||
@@ -132,7 +138,7 @@ class IconPickerDialogFragment : DialogFragment() {
|
|||||||
return bundle.getParcelable(KEY_ICON_STANDARD)
|
return bundle.getParcelable(KEY_ICON_STANDARD)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launch(activity: StylishActivity) {
|
fun launch(activity: FragmentActivity) {
|
||||||
// Create an instance of the dialog fragment and show it
|
// Create an instance of the dialog fragment and show it
|
||||||
val dialog = IconPickerDialogFragment()
|
val dialog = IconPickerDialogFragment()
|
||||||
dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment")
|
dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment")
|
||||||
|
|||||||
@@ -21,20 +21,51 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.DialogInterface
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
class PasswordEncodingDialogFragment : DialogFragment() {
|
class PasswordEncodingDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
var positiveButtonClickListener: DialogInterface.OnClickListener? = null
|
private var mListener: Listener? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
try {
|
||||||
|
mListener = context as Listener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + Listener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|
||||||
|
val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY)
|
||||||
|
val masterPasswordChecked: Boolean = savedInstanceState?.getBoolean(MASTER_PASSWORD_CHECKED_KEY) ?: false
|
||||||
|
val masterPassword: String? = savedInstanceState?.getString(MASTER_PASSWORD_KEY)
|
||||||
|
val keyFileChecked: Boolean = savedInstanceState?.getBoolean(KEY_FILE_CHECKED_KEY) ?: false
|
||||||
|
val keyFile: Uri? = savedInstanceState?.getParcelable(KEY_FILE_URI_KEY)
|
||||||
|
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning)
|
builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning)
|
||||||
builder.setPositiveButton(android.R.string.ok, positiveButtonClickListener)
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
mListener?.onPasswordEncodingValidateListener(
|
||||||
|
databaseUri,
|
||||||
|
masterPasswordChecked,
|
||||||
|
masterPassword,
|
||||||
|
keyFileChecked,
|
||||||
|
keyFile
|
||||||
|
)
|
||||||
|
}
|
||||||
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
|
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
|
||||||
|
|
||||||
return builder.create()
|
return builder.create()
|
||||||
@@ -42,5 +73,36 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
|||||||
return super.onCreateDialog(savedInstanceState)
|
return super.onCreateDialog(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun onPasswordEncodingValidateListener(databaseUri: Uri?,
|
||||||
|
masterPasswordChecked: Boolean,
|
||||||
|
masterPassword: String?,
|
||||||
|
keyFileChecked: Boolean,
|
||||||
|
keyFile: Uri?)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
|
||||||
|
private const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY"
|
||||||
|
private const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY"
|
||||||
|
private const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY"
|
||||||
|
private const val KEY_FILE_URI_KEY = "KEY_FILE_URI_KEY"
|
||||||
|
|
||||||
|
fun getInstance(databaseUri: Uri,
|
||||||
|
masterPasswordChecked: Boolean,
|
||||||
|
masterPassword: String?,
|
||||||
|
keyFileChecked: Boolean,
|
||||||
|
keyFile: Uri?): SortDialogFragment {
|
||||||
|
val fragment = SortDialogFragment()
|
||||||
|
fragment.arguments = Bundle().apply {
|
||||||
|
putParcelable(DATABASE_URI_KEY, databaseUri)
|
||||||
|
putBoolean(MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
||||||
|
putString(MASTER_PASSWORD_KEY, masterPassword)
|
||||||
|
putBoolean(KEY_FILE_CHECKED_KEY, keyFileChecked)
|
||||||
|
putParcelable(KEY_FILE_URI_KEY, keyFile)
|
||||||
|
}
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,13 +45,13 @@ class ProFeatureDialogFragment : DialogFragment() {
|
|||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_ad_free), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_ad_free), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_buy_pro), FROM_HTML_MODE_LEGACY))
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_buy_pro), FROM_HTML_MODE_LEGACY))
|
||||||
builder.setPositiveButton(R.string.download) { _, _ ->
|
builder.setPositiveButton(R.string.download) { _, _ ->
|
||||||
UriUtil.gotoUrl(context!!, R.string.app_pro_url)
|
UriUtil.gotoUrl(requireContext(), R.string.app_pro_url)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_feature_generosity), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_feature_generosity), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_donation), FROM_HTML_MODE_LEGACY))
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_donation), FROM_HTML_MODE_LEGACY))
|
||||||
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||||
UriUtil.gotoUrl(context!!, R.string.contribution_url)
|
UriUtil.gotoUrl(requireContext(), R.string.contribution_url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
builder.setMessage(stringBuilder)
|
builder.setMessage(stringBuilder)
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Dialog to confirm big file to upload
|
||||||
|
*/
|
||||||
|
class ReplaceFileDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
|
private var mActionChooseListener: ActionChooseListener? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
// Verify that the host activity implements the callback interface
|
||||||
|
try {
|
||||||
|
mActionChooseListener = context as ActionChooseListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + ActionChooseListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mActionChooseListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
// Use the Builder class for convenient dialog construction
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
builder.setMessage(SpannableStringBuilder().apply {
|
||||||
|
append(getString(R.string.warning_replace_file))
|
||||||
|
append("\n\n")
|
||||||
|
append(getString(R.string.warning_sure_add_file))
|
||||||
|
})
|
||||||
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
mActionChooseListener?.onValidateReplaceFile(
|
||||||
|
arguments?.getParcelable(KEY_FILE_URI),
|
||||||
|
arguments?.getParcelable(KEY_ENTRY_ATTACHMENT))
|
||||||
|
}
|
||||||
|
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
// Create the AlertDialog object and return it
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionChooseListener {
|
||||||
|
fun onValidateReplaceFile(attachmentToUploadUri: Uri?, attachment: Attachment?)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY_FILE_URI = "KEY_FILE_URI"
|
||||||
|
private const val KEY_ENTRY_ATTACHMENT = "KEY_ENTRY_ATTACHMENT"
|
||||||
|
|
||||||
|
fun build(attachmentToUploadUri: Uri,
|
||||||
|
attachment: Attachment): ReplaceFileDialogFragment {
|
||||||
|
val fragment = ReplaceFileDialogFragment()
|
||||||
|
fragment.arguments = Bundle().apply {
|
||||||
|
putParcelable(KEY_FILE_URI, attachmentToUploadUri)
|
||||||
|
putParcelable(KEY_ENTRY_ATTACHMENT, attachment)
|
||||||
|
}
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import android.text.TextWatcher
|
|||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
@@ -89,9 +90,9 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var mSecretWellFormed = false
|
private var mSecretWellFormed = false
|
||||||
private var mCounterWellFormed = true
|
private var mCounterWellFormed = false
|
||||||
private var mPeriodWellFormed = true
|
private var mPeriodWellFormed = false
|
||||||
private var mDigitsWellFormed = true
|
private var mDigitsWellFormed = false
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
@@ -106,6 +107,11 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mCreateOTPElementListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|
||||||
@@ -152,6 +158,28 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
otpCounterTextView?.setOnTouchListener(mOnTouchListener)
|
otpCounterTextView?.setOnTouchListener(mOnTouchListener)
|
||||||
otpDigitsTextView?.setOnTouchListener(mOnTouchListener)
|
otpDigitsTextView?.setOnTouchListener(mOnTouchListener)
|
||||||
|
|
||||||
|
// To manage focus
|
||||||
|
otpPeriodTextView?.setOnEditorActionListener { _, actionId, _ ->
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_NEXT) {
|
||||||
|
otpDigitsTextView?.requestFocus()
|
||||||
|
true
|
||||||
|
} else
|
||||||
|
false
|
||||||
|
}
|
||||||
|
otpCounterTextView?.setOnEditorActionListener { _, actionId, _ ->
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_NEXT) {
|
||||||
|
otpDigitsTextView?.requestFocus()
|
||||||
|
true
|
||||||
|
} else
|
||||||
|
false
|
||||||
|
}
|
||||||
|
otpCounterTextView?.setOnEditorActionListener { _, actionId, _ ->
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_NEXT) {
|
||||||
|
root?.requestFocus(View.FOCUS_DOWN)
|
||||||
|
true
|
||||||
|
} else
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
// HOTP / TOTP Type selection
|
// HOTP / TOTP Type selection
|
||||||
val otpTypeArray = OtpType.values()
|
val otpTypeArray = OtpType.values()
|
||||||
@@ -365,14 +393,26 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
private fun upgradeParameters() {
|
private fun upgradeParameters() {
|
||||||
otpAlgorithmSpinner?.setSelection(TokenCalculator.HashAlgorithm.values()
|
otpAlgorithmSpinner?.setSelection(TokenCalculator.HashAlgorithm.values()
|
||||||
.indexOf(mOtpElement.algorithm))
|
.indexOf(mOtpElement.algorithm))
|
||||||
|
|
||||||
|
val secret = mOtpElement.getBase32Secret()
|
||||||
otpSecretTextView?.apply {
|
otpSecretTextView?.apply {
|
||||||
setText(mOtpElement.getBase32Secret())
|
setText(secret)
|
||||||
// Cursor at end
|
// Cursor at end
|
||||||
setSelection(this.text.length)
|
setSelection(this.text.length)
|
||||||
}
|
}
|
||||||
otpCounterTextView?.setText(mOtpElement.counter.toString())
|
mSecretWellFormed = OtpElement.isValidBase32(secret)
|
||||||
otpPeriodTextView?.setText(mOtpElement.period.toString())
|
|
||||||
otpDigitsTextView?.setText(mOtpElement.digits.toString())
|
val counter = mOtpElement.counter
|
||||||
|
otpCounterTextView?.setText(counter.toString())
|
||||||
|
mCounterWellFormed = OtpElement.isValidCounter(counter)
|
||||||
|
|
||||||
|
val period = mOtpElement.period
|
||||||
|
otpPeriodTextView?.setText(period.toString())
|
||||||
|
mPeriodWellFormed = OtpElement.isValidPeriod(period)
|
||||||
|
|
||||||
|
val digits = mOtpElement.digits
|
||||||
|
otpDigitsTextView?.setText(digits.toString())
|
||||||
|
mDigitsWellFormed = OtpElement.isValidDigits(digits)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ class SortDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ class TimePickerFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
// Create a new instance of DatePickerDialog and return it
|
// Create a new instance of DatePickerDialog and return it
|
||||||
return context?.let {
|
return context?.let {
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import android.text.Html
|
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
|
|||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_buy_pro), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
|
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_buy_pro), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
|
||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||||
builder.setPositiveButton(R.string.download) { _, _ ->
|
builder.setPositiveButton(R.string.download) { _, _ ->
|
||||||
UriUtil.gotoUrl(context!!, R.string.app_pro_url)
|
UriUtil.gotoUrl(requireContext(), R.string.app_pro_url)
|
||||||
}
|
}
|
||||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
|
|||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_contibute), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_contibute), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||||
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||||
UriUtil.gotoUrl(context!!, R.string.contribution_url)
|
UriUtil.gotoUrl(requireContext(), R.string.contribution_url)
|
||||||
}
|
}
|
||||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,47 +24,186 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
object EntrySelectionHelper {
|
object EntrySelectionHelper {
|
||||||
|
|
||||||
private const val EXTRA_ENTRY_SELECTION_MODE = "com.kunzisoft.keepass.extra.ENTRY_SELECTION_MODE"
|
private const val KEY_SPECIAL_MODE = "com.kunzisoft.keepass.extra.SPECIAL_MODE"
|
||||||
private const val DEFAULT_ENTRY_SELECTION_MODE = false
|
private const val KEY_TYPE_MODE = "com.kunzisoft.keepass.extra.TYPE_MODE"
|
||||||
|
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 startActivityForEntrySelection(context: Context, intent: Intent) {
|
fun startActivityForSearchModeResult(context: Context,
|
||||||
addEntrySelectionModeExtraInIntent(intent)
|
intent: Intent,
|
||||||
// only to avoid visible flickering when redirecting
|
searchInfo: SearchInfo) {
|
||||||
|
addSpecialModeInIntent(intent, SpecialMode.SEARCH)
|
||||||
|
addSearchInfoInIntent(intent, searchInfo)
|
||||||
|
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addEntrySelectionModeExtraInIntent(intent: Intent) {
|
fun startActivityForSaveModeResult(context: Context,
|
||||||
intent.putExtra(EXTRA_ENTRY_SELECTION_MODE, true)
|
intent: Intent,
|
||||||
|
searchInfo: SearchInfo) {
|
||||||
|
addSpecialModeInIntent(intent, SpecialMode.SAVE)
|
||||||
|
addTypeModeInIntent(intent, TypeMode.DEFAULT)
|
||||||
|
addSearchInfoInIntent(intent, searchInfo)
|
||||||
|
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeEntrySelectionModeFromIntent(intent: Intent) {
|
fun startActivityForKeyboardSelectionModeResult(context: Context,
|
||||||
intent.removeExtra(EXTRA_ENTRY_SELECTION_MODE)
|
intent: Intent,
|
||||||
|
searchInfo: SearchInfo?) {
|
||||||
|
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||||
|
addTypeModeInIntent(intent, TypeMode.MAGIKEYBOARD)
|
||||||
|
addSearchInfoInIntent(intent, searchInfo)
|
||||||
|
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retrieveEntrySelectionModeFromIntent(intent: Intent): Boolean {
|
fun startActivityForRegistrationModeResult(context: Context,
|
||||||
return intent.getBooleanExtra(EXTRA_ENTRY_SELECTION_MODE, DEFAULT_ENTRY_SELECTION_MODE)
|
intent: Intent,
|
||||||
|
registerInfo: RegisterInfo?) {
|
||||||
|
addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
|
||||||
|
// At the moment, only autofill for registration
|
||||||
|
addTypeModeInIntent(intent, TypeMode.AUTOFILL)
|
||||||
|
addRegisterInfoInIntent(intent, registerInfo)
|
||||||
|
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun doEntrySelectionAction(intent: Intent,
|
fun addSearchInfoInIntent(intent: Intent, searchInfo: SearchInfo?) {
|
||||||
standardAction: () -> Unit,
|
searchInfo?.let {
|
||||||
keyboardAction: () -> Unit,
|
intent.putExtra(KEY_SEARCH_INFO, it)
|
||||||
autofillAction: (assistStructure: AssistStructure) -> Unit) {
|
|
||||||
var assistStructureInit = false
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
AutofillHelper.retrieveAssistStructure(intent)?.let { assistStructure ->
|
|
||||||
autofillAction.invoke(assistStructure)
|
|
||||||
assistStructureInit = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!assistStructureInit) {
|
}
|
||||||
if (intent.getBooleanExtra(EXTRA_ENTRY_SELECTION_MODE, DEFAULT_ENTRY_SELECTION_MODE)) {
|
|
||||||
intent.removeExtra(EXTRA_ENTRY_SELECTION_MODE)
|
fun retrieveSearchInfoFromIntent(intent: Intent): SearchInfo? {
|
||||||
keyboardAction.invoke()
|
return intent.getParcelableExtra(KEY_SEARCH_INFO)
|
||||||
} else {
|
}
|
||||||
standardAction.invoke()
|
|
||||||
|
fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
|
||||||
|
registerInfo?.let {
|
||||||
|
intent.putExtra(KEY_REGISTER_INFO, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun retrieveRegisterInfoFromIntent(intent: Intent): RegisterInfo? {
|
||||||
|
return intent.getParcelableExtra(KEY_REGISTER_INFO)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeInfoFromIntent(intent: Intent) {
|
||||||
|
intent.removeExtra(KEY_SEARCH_INFO)
|
||||||
|
intent.removeExtra(KEY_REGISTER_INFO)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
|
||||||
|
intent.putExtra(KEY_SPECIAL_MODE, specialMode as Serializable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
if (AutofillHelper.retrieveAssistStructure(intent) != null)
|
||||||
|
return SpecialMode.SELECTION
|
||||||
|
}
|
||||||
|
return intent.getSerializableExtra(KEY_SPECIAL_MODE) as SpecialMode?
|
||||||
|
?: SpecialMode.DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
|
||||||
|
intent.putExtra(KEY_TYPE_MODE, typeMode as Serializable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
if (AutofillHelper.retrieveAssistStructure(intent) != null)
|
||||||
|
return TypeMode.AUTOFILL
|
||||||
|
}
|
||||||
|
return intent.getSerializableExtra(KEY_TYPE_MODE) as TypeMode? ?: TypeMode.DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeModesFromIntent(intent: Intent) {
|
||||||
|
intent.removeExtra(KEY_SPECIAL_MODE)
|
||||||
|
intent.removeExtra(KEY_TYPE_MODE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doSpecialAction(intent: Intent,
|
||||||
|
defaultAction: () -> Unit,
|
||||||
|
searchAction: (searchInfo: SearchInfo) -> Unit,
|
||||||
|
saveAction: (searchInfo: SearchInfo) -> Unit,
|
||||||
|
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
||||||
|
autofillSelectionAction: (searchInfo: SearchInfo?,
|
||||||
|
assistStructure: AssistStructure) -> Unit,
|
||||||
|
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
|
||||||
|
|
||||||
|
when (retrieveSpecialModeFromIntent(intent)) {
|
||||||
|
SpecialMode.DEFAULT -> {
|
||||||
|
removeModesFromIntent(intent)
|
||||||
|
removeInfoFromIntent(intent)
|
||||||
|
defaultAction.invoke()
|
||||||
|
}
|
||||||
|
SpecialMode.SEARCH -> {
|
||||||
|
val searchInfo = retrieveSearchInfoFromIntent(intent)
|
||||||
|
removeModesFromIntent(intent)
|
||||||
|
removeInfoFromIntent(intent)
|
||||||
|
if (searchInfo != null)
|
||||||
|
searchAction.invoke(searchInfo)
|
||||||
|
else {
|
||||||
|
defaultAction.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SpecialMode.SAVE -> {
|
||||||
|
val searchInfo = retrieveSearchInfoFromIntent(intent)
|
||||||
|
removeModesFromIntent(intent)
|
||||||
|
removeInfoFromIntent(intent)
|
||||||
|
if (searchInfo != null)
|
||||||
|
saveAction.invoke(searchInfo)
|
||||||
|
else {
|
||||||
|
defaultAction.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SpecialMode.SELECTION -> {
|
||||||
|
val searchInfo: SearchInfo? = retrieveSearchInfoFromIntent(intent)
|
||||||
|
var assistStructureInit = false
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
AutofillHelper.retrieveAssistStructure(intent)?.let { assistStructure ->
|
||||||
|
autofillSelectionAction.invoke(searchInfo, assistStructure)
|
||||||
|
assistStructureInit = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!assistStructureInit) {
|
||||||
|
if (intent.getSerializableExtra(KEY_SPECIAL_MODE) != null) {
|
||||||
|
when (retrieveTypeModeFromIntent(intent)) {
|
||||||
|
TypeMode.DEFAULT -> {
|
||||||
|
removeModesFromIntent(intent)
|
||||||
|
if (searchInfo != null)
|
||||||
|
searchAction.invoke(searchInfo)
|
||||||
|
else
|
||||||
|
defaultAction.invoke()
|
||||||
|
}
|
||||||
|
TypeMode.MAGIKEYBOARD -> keyboardSelectionAction.invoke(searchInfo)
|
||||||
|
else -> {
|
||||||
|
// In this case, error
|
||||||
|
removeModesFromIntent(intent)
|
||||||
|
removeInfoFromIntent(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (searchInfo != null)
|
||||||
|
searchAction.invoke(searchInfo)
|
||||||
|
else
|
||||||
|
defaultAction.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SpecialMode.REGISTRATION -> {
|
||||||
|
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent)
|
||||||
|
removeModesFromIntent(intent)
|
||||||
|
removeInfoFromIntent(intent)
|
||||||
|
autofillRegistrationAction.invoke(registerInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,19 +28,20 @@ import android.content.pm.PackageManager
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
class OpenFileHelper {
|
class SelectFileHelper {
|
||||||
|
|
||||||
private var activity: Activity? = null
|
private var activity: Activity? = null
|
||||||
private var fragment: Fragment? = null
|
private var fragment: Fragment? = null
|
||||||
|
|
||||||
val openFileOnClickViewListener: OpenFileOnClickViewListener
|
val selectFileOnClickViewListener: SelectFileOnClickViewListener
|
||||||
get() = OpenFileOnClickViewListener()
|
get() = SelectFileOnClickViewListener()
|
||||||
|
|
||||||
constructor(context: Activity) {
|
constructor(context: Activity) {
|
||||||
this.activity = context
|
this.activity = context
|
||||||
@@ -52,7 +53,10 @@ class OpenFileHelper {
|
|||||||
this.fragment = context
|
this.fragment = context
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class OpenFileOnClickViewListener : View.OnClickListener, View.OnLongClickListener {
|
inner class SelectFileOnClickViewListener :
|
||||||
|
View.OnClickListener,
|
||||||
|
View.OnLongClickListener,
|
||||||
|
MenuItem.OnMenuItemClickListener {
|
||||||
|
|
||||||
private fun onAbstractClick(longClick: Boolean = false) {
|
private fun onAbstractClick(longClick: Boolean = false) {
|
||||||
try {
|
try {
|
||||||
@@ -85,17 +89,22 @@ class OpenFileHelper {
|
|||||||
onAbstractClick(true)
|
onAbstractClick(true)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
||||||
|
onAbstractClick()
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
private fun openActivityWithActionOpenDocument() {
|
private fun openActivityWithActionOpenDocument() {
|
||||||
val intentOpenDocument = Intent(APP_ACTION_OPEN_DOCUMENT).apply {
|
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
type = "*/*"
|
type = "*/*"
|
||||||
flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
|
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or
|
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
}
|
}
|
||||||
if (fragment != null)
|
if (fragment != null)
|
||||||
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||||
@@ -108,10 +117,10 @@ class OpenFileHelper {
|
|||||||
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
type = "*/*"
|
type = "*/*"
|
||||||
flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
|
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or
|
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
}
|
}
|
||||||
if (fragment != null)
|
if (fragment != null)
|
||||||
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
|
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||||
@@ -164,11 +173,10 @@ class OpenFileHelper {
|
|||||||
*/
|
*/
|
||||||
private fun showBrowserDialog() {
|
private fun showBrowserDialog() {
|
||||||
try {
|
try {
|
||||||
val browserDialogFragment = BrowserDialogFragment()
|
val fileManagerDialogFragment = FileManagerDialogFragment()
|
||||||
if (fragment != null && fragment!!.fragmentManager != null)
|
fragment?.let {
|
||||||
browserDialogFragment.show(fragment!!.fragmentManager!!, "browserDialog")
|
fileManagerDialogFragment.show(it.parentFragmentManager, "browserDialog")
|
||||||
else
|
} ?: fileManagerDialogFragment.show((activity as FragmentActivity).supportFragmentManager, "browserDialog")
|
||||||
browserDialogFragment.show((activity as FragmentActivity).supportFragmentManager, "browserDialog")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Can't open BrowserDialog", e)
|
Log.e(TAG, "Can't open BrowserDialog", e)
|
||||||
}
|
}
|
||||||
@@ -227,12 +235,6 @@ class OpenFileHelper {
|
|||||||
|
|
||||||
private const val TAG = "OpenFileHelper"
|
private const val TAG = "OpenFileHelper"
|
||||||
|
|
||||||
private var APP_ACTION_OPEN_DOCUMENT: String = try {
|
|
||||||
Intent::class.java.getField("ACTION_OPEN_DOCUMENT").get(null) as String
|
|
||||||
} catch (e: Exception) {
|
|
||||||
"android.intent.action.OPEN_DOCUMENT"
|
|
||||||
}
|
|
||||||
|
|
||||||
const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE"
|
const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE"
|
||||||
|
|
||||||
private const val GET_CONTENT = 25745
|
private const val GET_CONTENT = 25745
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.helpers
|
||||||
|
|
||||||
|
enum class SpecialMode {
|
||||||
|
DEFAULT,
|
||||||
|
SEARCH,
|
||||||
|
SAVE,
|
||||||
|
SELECTION,
|
||||||
|
REGISTRATION;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.helpers
|
||||||
|
|
||||||
|
enum class TypeMode {
|
||||||
|
DEFAULT, MAGIKEYBOARD, AUTOFILL
|
||||||
|
}
|
||||||
@@ -19,32 +19,25 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.lock
|
package com.kunzisoft.keepass.activities.lock
|
||||||
|
|
||||||
import android.app.Activity
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||||
|
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.*
|
import com.kunzisoft.keepass.utils.*
|
||||||
|
|
||||||
abstract class LockingActivity : StylishActivity() {
|
abstract class LockingActivity : SpecialModeActivity() {
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val TAG = "LockingActivity"
|
|
||||||
|
|
||||||
const val RESULT_EXIT_LOCK = 1450
|
|
||||||
|
|
||||||
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
|
||||||
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
|
||||||
}
|
|
||||||
|
|
||||||
protected var mTimeoutEnable: Boolean = true
|
protected var mTimeoutEnable: Boolean = true
|
||||||
|
|
||||||
@@ -52,14 +45,17 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
private var mExitLock: Boolean = false
|
private var mExitLock: Boolean = false
|
||||||
|
|
||||||
// Force readOnly if Entry Selection mode
|
// Force readOnly if Entry Selection mode
|
||||||
protected var mReadOnly: Boolean = false
|
protected var mReadOnly: Boolean
|
||||||
get() {
|
get() {
|
||||||
return field || mSelectionMode
|
return mReadOnlyToSave
|
||||||
}
|
}
|
||||||
protected var mSelectionMode: Boolean = false
|
set(value) {
|
||||||
|
mReadOnlyToSave = value
|
||||||
|
}
|
||||||
|
private var mReadOnlyToSave: Boolean = false
|
||||||
protected var mAutoSaveEnable: Boolean = true
|
protected var mAutoSaveEnable: Boolean = true
|
||||||
|
|
||||||
var mProgressDialogThread: ProgressDialogThread? = null
|
var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -75,15 +71,20 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
|
|
||||||
if (mTimeoutEnable) {
|
if (mTimeoutEnable) {
|
||||||
mLockReceiver = LockReceiver {
|
mLockReceiver = LockReceiver {
|
||||||
lockAndExit()
|
closeDatabase()
|
||||||
|
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
|
||||||
|
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
|
||||||
|
// Add onActivityForResult response
|
||||||
|
setResult(RESULT_EXIT_LOCK)
|
||||||
|
closeOptionsMenu()
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
registerLockReceiver(mLockReceiver)
|
registerLockReceiver(mLockReceiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
mExitLock = false
|
mExitLock = false
|
||||||
mReadOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState, intent)
|
|
||||||
|
|
||||||
mProgressDialogThread = ProgressDialogThread(this)
|
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
@@ -99,10 +100,19 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
mProgressDialogThread?.registerProgressTask()
|
// If in ave or registration mode, don't allow read only
|
||||||
|
if ((mSpecialMode == SpecialMode.SAVE
|
||||||
|
|| mSpecialMode == SpecialMode.REGISTRATION)
|
||||||
|
&& mReadOnly) {
|
||||||
|
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
|
||||||
|
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||||
|
|
||||||
// To refresh when back to normal workflow from selection workflow
|
// To refresh when back to normal workflow from selection workflow
|
||||||
mSelectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(intent)
|
mReadOnlyToSave = ReadOnlyHelper.retrieveReadOnlyFromIntent(intent)
|
||||||
mAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(this)
|
mAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(this)
|
||||||
|
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
@@ -122,16 +132,19 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
if (!mExitLock)
|
if (!mExitLock)
|
||||||
TimeoutHelper.recordTime(this)
|
TimeoutHelper.recordTime(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOCKING_ACTIVITY_UI_VISIBLE = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
ReadOnlyHelper.onSaveInstanceState(outState, mReadOnly)
|
|
||||||
outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable)
|
outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable)
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
mProgressDialogThread?.unregisterProgressTask()
|
LOCKING_ACTIVITY_UI_VISIBLE = false
|
||||||
|
|
||||||
|
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
@@ -148,17 +161,26 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
|
|
||||||
protected fun lockAndExit() {
|
protected fun lockAndExit() {
|
||||||
sendBroadcast(Intent(LOCK_ACTION))
|
sendBroadcast(Intent(LOCK_ACTION))
|
||||||
lock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To reset the app timeout when a view is focused or changed
|
* To reset the app timeout when a view is focused or changed
|
||||||
*/
|
*/
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
protected fun resetAppTimeoutWhenViewFocusedOrChanged(vararg views: View?) {
|
protected fun resetAppTimeoutWhenViewFocusedOrChanged(vararg views: View?) {
|
||||||
views.forEach {
|
views.forEach {
|
||||||
|
it?.setOnTouchListener { _, event ->
|
||||||
|
when (event.action) {
|
||||||
|
MotionEvent.ACTION_DOWN -> {
|
||||||
|
// Log.d(TAG, "View touched, try to reset app timeout")
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
it?.setOnFocusChangeListener { _, hasFocus ->
|
it?.setOnFocusChangeListener { _, hasFocus ->
|
||||||
if (hasFocus) {
|
if (hasFocus) {
|
||||||
Log.d(TAG, "View focused, reset app timeout")
|
// Log.d(TAG, "View focused, try to reset app timeout")
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,12 +201,17 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun Activity.lock() {
|
companion object {
|
||||||
closeDatabase()
|
|
||||||
|
|
||||||
// Add onActivityForResult response
|
private const val TAG = "LockingActivity"
|
||||||
setResult(LockingActivity.RESULT_EXIT_LOCK)
|
|
||||||
finish()
|
const val RESULT_EXIT_LOCK = 1450
|
||||||
|
|
||||||
|
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
||||||
|
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
||||||
|
|
||||||
|
private var LOCKING_ACTIVITY_UI_VISIBLE = false
|
||||||
|
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.selection
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.TypeMode
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.view.SpecialModeView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity to manage special mode (ie: selection mode)
|
||||||
|
*/
|
||||||
|
abstract class SpecialModeActivity : StylishActivity() {
|
||||||
|
|
||||||
|
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
|
||||||
|
protected var mTypeMode: TypeMode = TypeMode.DEFAULT
|
||||||
|
|
||||||
|
private var mSpecialModeView: SpecialModeView? = null
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (mSpecialMode != SpecialMode.DEFAULT)
|
||||||
|
onCancelSpecialMode()
|
||||||
|
else
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To call the regular onBackPressed() method in special mode
|
||||||
|
*/
|
||||||
|
protected fun onRegularBackPressed() {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intent sender uses special retains data in callback
|
||||||
|
*/
|
||||||
|
private fun isIntentSender(): Boolean {
|
||||||
|
return (mSpecialMode == SpecialMode.SELECTION
|
||||||
|
&& mTypeMode == TypeMode.AUTOFILL)
|
||||||
|
/* TODO Registration callback #765
|
||||||
|
|| (mSpecialMode == SpecialMode.REGISTRATION
|
||||||
|
&& mTypeMode == TypeMode.AUTOFILL
|
||||||
|
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onLaunchActivitySpecialMode() {
|
||||||
|
if (!isIntentSender()) {
|
||||||
|
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||||
|
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun onValidateSpecialMode() {
|
||||||
|
if (isIntentSender()) {
|
||||||
|
super.finish()
|
||||||
|
} else {
|
||||||
|
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||||
|
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||||
|
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||||
|
// To move the app in background
|
||||||
|
moveTaskToBack(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun onCancelSpecialMode() {
|
||||||
|
if (isIntentSender()) {
|
||||||
|
// To get the app caller, only for IntentSender
|
||||||
|
super.onBackPressed()
|
||||||
|
} else {
|
||||||
|
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||||
|
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||||
|
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||||
|
// To move the app in background
|
||||||
|
moveTaskToBack(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun backToTheAppCaller() {
|
||||||
|
if (isIntentSender()) {
|
||||||
|
// To get the app caller, only for IntentSender
|
||||||
|
super.onBackPressed()
|
||||||
|
} else {
|
||||||
|
// To move the app in background
|
||||||
|
moveTaskToBack(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
||||||
|
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
||||||
|
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
||||||
|
val searchInfo: SearchInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)?.searchInfo
|
||||||
|
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
||||||
|
|
||||||
|
// To show the selection mode
|
||||||
|
mSpecialModeView = findViewById(R.id.special_mode_view)
|
||||||
|
mSpecialModeView?.apply {
|
||||||
|
// Populate title
|
||||||
|
val selectionModeStringId = when (mSpecialMode) {
|
||||||
|
SpecialMode.DEFAULT, // Not important because hidden
|
||||||
|
SpecialMode.SEARCH -> R.string.search_mode
|
||||||
|
SpecialMode.SAVE -> R.string.save_mode
|
||||||
|
SpecialMode.SELECTION -> R.string.selection_mode
|
||||||
|
SpecialMode.REGISTRATION -> R.string.registration_mode
|
||||||
|
}
|
||||||
|
val typeModeStringId = when (mTypeMode) {
|
||||||
|
TypeMode.DEFAULT, // Not important because hidden
|
||||||
|
TypeMode.MAGIKEYBOARD -> R.string.magic_keyboard_title
|
||||||
|
TypeMode.AUTOFILL -> R.string.autofill
|
||||||
|
}
|
||||||
|
title = getString(selectionModeStringId)
|
||||||
|
if (mTypeMode != TypeMode.DEFAULT)
|
||||||
|
title = "$title (${getString(typeModeStringId)})"
|
||||||
|
// Populate subtitle
|
||||||
|
subtitle = searchInfo?.getName(resources)
|
||||||
|
|
||||||
|
// Show the toolbar or not
|
||||||
|
visible = when (mSpecialMode) {
|
||||||
|
SpecialMode.DEFAULT -> false
|
||||||
|
SpecialMode.SEARCH -> true
|
||||||
|
SpecialMode.SAVE -> true
|
||||||
|
SpecialMode.SELECTION -> true
|
||||||
|
SpecialMode.REGISTRATION -> true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add back listener
|
||||||
|
onCancelButtonClickListener = View.OnClickListener {
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create menu
|
||||||
|
menu.clear()
|
||||||
|
if (mTypeMode == TypeMode.AUTOFILL) {
|
||||||
|
menuInflater.inflate(R.menu.autofill, menu)
|
||||||
|
setOnMenuItemClickListener { menuItem ->
|
||||||
|
when (menuItem.itemId) {
|
||||||
|
R.id.menu_block_autofill -> {
|
||||||
|
blockAutofill(searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// To hide home button from the regular toolbar in special mode
|
||||||
|
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
||||||
|
supportActionBar?.setDisplayShowHomeEnabled(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun blockAutofill(searchInfo: SearchInfo?) {
|
||||||
|
val webDomain = searchInfo?.webDomain
|
||||||
|
val applicationId = searchInfo?.applicationId
|
||||||
|
if (webDomain != null) {
|
||||||
|
PreferencesUtil.addWebDomainToBlocklist(this,
|
||||||
|
webDomain)
|
||||||
|
} else if (applicationId != null) {
|
||||||
|
PreferencesUtil.addApplicationIdToBlocklist(this,
|
||||||
|
applicationId)
|
||||||
|
}
|
||||||
|
onCancelSpecialMode()
|
||||||
|
Toast.makeText(this.applicationContext,
|
||||||
|
R.string.autofill_block_restart,
|
||||||
|
Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,8 +41,9 @@ abstract class StylishActivity : AppCompatActivity() {
|
|||||||
*/
|
*/
|
||||||
override fun startActivity(intent: Intent) {
|
override fun startActivity(intent: Intent) {
|
||||||
try {
|
try {
|
||||||
if (intent.component != null && intent.component!!.shortClassName == ".HtcLinkifyDispatcherActivity") {
|
intent.component?.let {
|
||||||
intent.component = null
|
if (it.shortClassName == ".HtcLinkifyDispatcherActivity")
|
||||||
|
intent.component = null
|
||||||
}
|
}
|
||||||
super.startActivity(intent)
|
super.startActivity(intent)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
|||||||
@@ -44,14 +44,21 @@ abstract class StylishFragment : Fragment() {
|
|||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
// To fix status bar color
|
// To fix status bar color
|
||||||
if (activity != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
val window = activity!!.window
|
val window = requireActivity().window
|
||||||
|
|
||||||
val attrColorPrimaryDark = intArrayOf(android.R.attr.colorPrimaryDark)
|
|
||||||
val taColorPrimaryDark = contextThemed?.theme?.obtainStyledAttributes(attrColorPrimaryDark)
|
|
||||||
val defaultColor = Color.BLACK
|
val defaultColor = Color.BLACK
|
||||||
window.statusBarColor = taColorPrimaryDark?.getColor(0, defaultColor) ?: defaultColor
|
|
||||||
taColorPrimaryDark?.recycle()
|
try {
|
||||||
|
val taStatusBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.statusBarColor))
|
||||||
|
window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor
|
||||||
|
taStatusBarColor?.recycle()
|
||||||
|
} catch (e: Exception) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor))
|
||||||
|
window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor
|
||||||
|
taNavigationBarColor?.recycle()
|
||||||
|
} catch (e: Exception) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onCreateView(inflater, container, savedInstanceState)
|
return super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.kunzisoft.keepass.view.collapse
|
||||||
|
|
||||||
|
abstract class AnimatedItemsAdapter<Item, T: RecyclerView.ViewHolder>(val context: Context)
|
||||||
|
: RecyclerView.Adapter<T>() {
|
||||||
|
|
||||||
|
protected val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
|
var itemsList: MutableList<Item> = ArrayList()
|
||||||
|
private set
|
||||||
|
|
||||||
|
var onDeleteButtonClickListener: ((item: Item)->Unit)? = null
|
||||||
|
private var mItemToRemove: Item? = null
|
||||||
|
|
||||||
|
var onListSizeChangedListener: ((previousSize: Int, newSize: Int)->Unit)? = null
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return itemsList.size
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun assignItems(items: List<Item>) {
|
||||||
|
val previousSize = itemsList.size
|
||||||
|
itemsList.apply {
|
||||||
|
clear()
|
||||||
|
addAll(items)
|
||||||
|
}
|
||||||
|
notifyDataSetChanged()
|
||||||
|
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun isEmpty(): Boolean {
|
||||||
|
return itemsList.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun contains(item: Item): Boolean {
|
||||||
|
return itemsList.contains(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun indexOf(item: Item): Int {
|
||||||
|
return itemsList.indexOf(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun putItem(item: Item) {
|
||||||
|
val previousSize = itemsList.size
|
||||||
|
if (itemsList.contains(item)) {
|
||||||
|
val index = itemsList.indexOf(item)
|
||||||
|
itemsList.removeAt(index)
|
||||||
|
itemsList.add(index, item)
|
||||||
|
notifyItemChanged(index)
|
||||||
|
} else {
|
||||||
|
itemsList.add(item)
|
||||||
|
notifyItemInserted(itemsList.indexOf(item))
|
||||||
|
}
|
||||||
|
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only replace [oldItem] by [newItem] if [oldItem] exists
|
||||||
|
*/
|
||||||
|
open fun replaceItem(oldItem: Item, newItem: Item) {
|
||||||
|
if (itemsList.contains(oldItem)) {
|
||||||
|
val index = itemsList.indexOf(oldItem)
|
||||||
|
itemsList.removeAt(index)
|
||||||
|
itemsList.add(index, newItem)
|
||||||
|
notifyItemChanged(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only remove [item] if doesn't exists
|
||||||
|
*/
|
||||||
|
open fun removeItem(item: Item) {
|
||||||
|
if (itemsList.contains(item)) {
|
||||||
|
mItemToRemove = item
|
||||||
|
notifyItemChanged(itemsList.indexOf(item))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun performDeletion(holder: T, item: Item): Boolean {
|
||||||
|
val effectivelyDeletionPerformed = mItemToRemove == item
|
||||||
|
if (effectivelyDeletionPerformed) {
|
||||||
|
holder.itemView.collapse(true) {
|
||||||
|
deleteItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return effectivelyDeletionPerformed
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun onBindDeleteButton(holder: T, deleteButton: View, item: Item, position: Int) {
|
||||||
|
deleteButton.apply {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
if (performDeletion(holder, item)) {
|
||||||
|
setOnClickListener(null)
|
||||||
|
} else {
|
||||||
|
setOnClickListener {
|
||||||
|
onDeleteButtonClickListener?.invoke(item)
|
||||||
|
mItemToRemove = item
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteItem(item: Item) {
|
||||||
|
val previousSize = itemsList.size
|
||||||
|
val position = itemsList.indexOf(item)
|
||||||
|
if (position >= 0) {
|
||||||
|
itemsList.removeAt(position)
|
||||||
|
notifyItemRemoved(position)
|
||||||
|
mItemToRemove = null
|
||||||
|
for (i in 0 until itemsList.size) {
|
||||||
|
notifyItemChanged(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
itemsList.clear()
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.adapters
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.text.format.Formatter
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.ProgressBar
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
|
||||||
import com.kunzisoft.keepass.model.AttachmentState
|
|
||||||
import com.kunzisoft.keepass.model.EntryAttachment
|
|
||||||
|
|
||||||
class EntryAttachmentsAdapter(val context: Context) : RecyclerView.Adapter<EntryAttachmentsAdapter.EntryBinariesViewHolder>() {
|
|
||||||
|
|
||||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
|
||||||
var entryAttachmentsList: MutableList<EntryAttachment> = ArrayList()
|
|
||||||
var onItemClickListener: ((item: EntryAttachment, position: Int)->Unit)? = null
|
|
||||||
|
|
||||||
private val mDatabase = Database.getInstance()
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryBinariesViewHolder {
|
|
||||||
return EntryBinariesViewHolder(inflater.inflate(R.layout.item_attachment, parent, false))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: EntryBinariesViewHolder, position: Int) {
|
|
||||||
val entryAttachment = entryAttachmentsList[position]
|
|
||||||
|
|
||||||
holder.binaryFileTitle.text = entryAttachment.name
|
|
||||||
holder.binaryFileSize.text = Formatter.formatFileSize(context,
|
|
||||||
entryAttachment.binaryAttachment.length())
|
|
||||||
holder.binaryFileCompression.apply {
|
|
||||||
if (mDatabase.compressionAlgorithm == CompressionAlgorithm.GZip
|
|
||||||
|| entryAttachment.binaryAttachment.isCompressed == true) {
|
|
||||||
text = CompressionAlgorithm.GZip.getName(context.resources)
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
text = ""
|
|
||||||
visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
holder.binaryFileProgress.apply {
|
|
||||||
visibility = when (entryAttachment.downloadState) {
|
|
||||||
AttachmentState.NULL, AttachmentState.COMPLETE, AttachmentState.ERROR -> View.GONE
|
|
||||||
AttachmentState.START, AttachmentState.IN_PROGRESS -> View.VISIBLE
|
|
||||||
}
|
|
||||||
progress = entryAttachment.downloadProgression
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.itemView.setOnClickListener {
|
|
||||||
onItemClickListener?.invoke(entryAttachment, position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return entryAttachmentsList.size
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateProgress(entryAttachment: EntryAttachment) {
|
|
||||||
val indexEntryAttachment = entryAttachmentsList.indexOfLast { current -> current.name == entryAttachment.name }
|
|
||||||
if (indexEntryAttachment != -1) {
|
|
||||||
entryAttachmentsList[indexEntryAttachment] = entryAttachment
|
|
||||||
notifyItemChanged(indexEntryAttachment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clear() {
|
|
||||||
entryAttachmentsList.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
|
||||||
|
|
||||||
var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title)
|
|
||||||
var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size)
|
|
||||||
var binaryFileCompression: TextView = itemView.findViewById(R.id.item_attachment_compression)
|
|
||||||
var binaryFileProgress: ProgressBar = itemView.findViewById(R.id.item_attachment_progress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.TypedArray
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.text.format.Formatter
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.model.AttachmentState
|
||||||
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
|
|
||||||
|
|
||||||
|
class EntryAttachmentsItemsAdapter(context: Context)
|
||||||
|
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
|
||||||
|
|
||||||
|
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
|
||||||
|
|
||||||
|
private var mTitleColor: Int
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Get the primary text color of the theme
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
context.theme.resolveAttribute(android.R.attr.textColorPrimary, typedValue, true)
|
||||||
|
val typedArray: TypedArray = context.obtainStyledAttributes(typedValue.data, intArrayOf(
|
||||||
|
android.R.attr.textColor))
|
||||||
|
mTitleColor = typedArray.getColor(0, -1)
|
||||||
|
typedArray.recycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryBinariesViewHolder {
|
||||||
|
return EntryBinariesViewHolder(inflater.inflate(R.layout.item_attachment, parent, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: EntryBinariesViewHolder, position: Int) {
|
||||||
|
val entryAttachmentState = itemsList[position]
|
||||||
|
|
||||||
|
holder.itemView.visibility = View.VISIBLE
|
||||||
|
holder.binaryFileBroken.apply {
|
||||||
|
setColorFilter(Color.RED)
|
||||||
|
visibility = if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
holder.binaryFileTitle.text = entryAttachmentState.attachment.name
|
||||||
|
if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) {
|
||||||
|
holder.binaryFileTitle.setTextColor(Color.RED)
|
||||||
|
} else {
|
||||||
|
holder.binaryFileTitle.setTextColor(mTitleColor)
|
||||||
|
}
|
||||||
|
holder.binaryFileSize.text = Formatter.formatFileSize(context,
|
||||||
|
entryAttachmentState.attachment.binaryAttachment.length())
|
||||||
|
holder.binaryFileCompression.apply {
|
||||||
|
if (entryAttachmentState.attachment.binaryAttachment.isCompressed) {
|
||||||
|
text = CompressionAlgorithm.GZip.getName(context.resources)
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
text = ""
|
||||||
|
visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
when (entryAttachmentState.streamDirection) {
|
||||||
|
StreamDirection.UPLOAD -> {
|
||||||
|
holder.binaryFileProgressIcon.isActivated = true
|
||||||
|
when (entryAttachmentState.downloadState) {
|
||||||
|
AttachmentState.START,
|
||||||
|
AttachmentState.IN_PROGRESS -> {
|
||||||
|
holder.binaryFileProgressContainer.visibility = View.VISIBLE
|
||||||
|
holder.binaryFileProgress.apply {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
progress = entryAttachmentState.downloadProgression
|
||||||
|
}
|
||||||
|
holder.binaryFileDeleteButton.apply {
|
||||||
|
visibility = View.GONE
|
||||||
|
setOnClickListener(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AttachmentState.NULL,
|
||||||
|
AttachmentState.ERROR,
|
||||||
|
AttachmentState.COMPLETE -> {
|
||||||
|
holder.binaryFileProgressContainer.visibility = View.GONE
|
||||||
|
holder.binaryFileProgress.visibility = View.GONE
|
||||||
|
holder.binaryFileDeleteButton.apply {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
onBindDeleteButton(holder, this, entryAttachmentState, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
holder.itemView.setOnClickListener(null)
|
||||||
|
}
|
||||||
|
StreamDirection.DOWNLOAD -> {
|
||||||
|
holder.binaryFileProgressIcon.isActivated = false
|
||||||
|
holder.binaryFileProgressContainer.visibility = View.VISIBLE
|
||||||
|
holder.binaryFileDeleteButton.visibility = View.GONE
|
||||||
|
holder.binaryFileProgress.apply {
|
||||||
|
visibility = when (entryAttachmentState.downloadState) {
|
||||||
|
AttachmentState.NULL, AttachmentState.COMPLETE, AttachmentState.ERROR -> View.GONE
|
||||||
|
AttachmentState.START, AttachmentState.IN_PROGRESS -> View.VISIBLE
|
||||||
|
}
|
||||||
|
progress = entryAttachmentState.downloadProgression
|
||||||
|
}
|
||||||
|
holder.itemView.setOnClickListener {
|
||||||
|
onItemClickListener?.invoke(entryAttachmentState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
|
var binaryFileBroken: ImageView = itemView.findViewById(R.id.item_attachment_broken)
|
||||||
|
var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title)
|
||||||
|
var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size)
|
||||||
|
var binaryFileCompression: TextView = itemView.findViewById(R.id.item_attachment_compression)
|
||||||
|
var binaryFileProgressContainer: View = itemView.findViewById(R.id.item_attachment_progress_container)
|
||||||
|
var binaryFileProgressIcon: ImageView = itemView.findViewById(R.id.item_attachment_icon)
|
||||||
|
var binaryFileProgress: ProgressBar = itemView.findViewById(R.id.item_attachment_progress)
|
||||||
|
var binaryFileDeleteButton: View = itemView.findViewById(R.id.item_attachment_delete_button)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,31 +22,33 @@ package com.kunzisoft.keepass.adapters
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.PorterDuff
|
import android.graphics.PorterDuff
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.*
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.*
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.ViewSwitcher
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryEntity
|
import com.kunzisoft.keepass.model.DatabaseFile
|
||||||
import com.kunzisoft.keepass.utils.FileDatabaseInfo
|
import com.kunzisoft.keepass.view.collapse
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.view.expand
|
||||||
|
|
||||||
class FileDatabaseHistoryAdapter(private val context: Context)
|
class FileDatabaseHistoryAdapter(context: Context)
|
||||||
: RecyclerView.Adapter<FileDatabaseHistoryAdapter.FileDatabaseHistoryViewHolder>() {
|
: RecyclerView.Adapter<FileDatabaseHistoryAdapter.FileDatabaseHistoryViewHolder>() {
|
||||||
|
|
||||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
private var fileItemOpenListener: ((FileDatabaseHistoryEntity)->Unit)? = null
|
private var defaultDatabaseListener: ((DatabaseFile?) -> Unit)? = null
|
||||||
private var fileSelectClearListener: ((FileDatabaseHistoryEntity)->Boolean)? = null
|
private var fileItemOpenListener: ((DatabaseFile)->Unit)? = null
|
||||||
private var saveAliasListener: ((FileDatabaseHistoryEntity)->Unit)? = null
|
private var fileSelectClearListener: ((DatabaseFile)->Boolean)? = null
|
||||||
|
private var saveAliasListener: ((DatabaseFile)->Unit)? = null
|
||||||
|
|
||||||
private val listDatabaseFiles = ArrayList<FileDatabaseHistoryEntity>()
|
private val listDatabaseFiles = ArrayList<DatabaseFile>()
|
||||||
|
|
||||||
private var mExpandedPosition = -1
|
private var mDefaultDatabaseFile: DatabaseFile? = null
|
||||||
private var mPreviousExpandedPosition = -1
|
private var mExpandedDatabaseFile: DatabaseFile? = null
|
||||||
|
private var mPreviousExpandedDatabaseFile: DatabaseFile? = null
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private val defaultColor: Int
|
private val defaultColor: Int
|
||||||
@@ -63,43 +65,49 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileDatabaseHistoryViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileDatabaseHistoryViewHolder {
|
||||||
val view = inflater.inflate(R.layout.item_file_row, parent, false)
|
val view = inflater.inflate(R.layout.item_file_info, parent, false)
|
||||||
return FileDatabaseHistoryViewHolder(view)
|
return FileDatabaseHistoryViewHolder(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) {
|
||||||
// Get info from position
|
// Get info from position
|
||||||
val fileHistoryEntity = listDatabaseFiles[position]
|
val databaseFile = listDatabaseFiles[position]
|
||||||
val fileDatabaseInfo = FileDatabaseInfo(context, fileHistoryEntity.databaseUri)
|
|
||||||
|
|
||||||
// Click item to open file
|
// Click item to open file
|
||||||
if (fileItemOpenListener != null)
|
holder.fileContainer.setOnClickListener {
|
||||||
holder.fileContainer.setOnClickListener {
|
fileItemOpenListener?.invoke(databaseFile)
|
||||||
fileItemOpenListener?.invoke(fileHistoryEntity)
|
}
|
||||||
|
|
||||||
|
// Default database
|
||||||
|
holder.defaultFileButton.apply {
|
||||||
|
this.isChecked = mDefaultDatabaseFile == databaseFile
|
||||||
|
setOnClickListener {
|
||||||
|
defaultDatabaseListener?.invoke(if (isChecked) databaseFile else null)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// File alias
|
// File alias
|
||||||
holder.fileAlias.text = fileDatabaseInfo.retrieveDatabaseAlias(fileHistoryEntity.databaseAlias)
|
holder.fileAlias.text = databaseFile.databaseAlias
|
||||||
|
|
||||||
// File path
|
// File path
|
||||||
holder.filePath.text = UriUtil.decode(fileDatabaseInfo.fileUri?.toString())
|
holder.filePath.text = databaseFile.databaseDecodedPath
|
||||||
|
|
||||||
if (fileDatabaseInfo.exists) {
|
if (databaseFile.databaseFileExists) {
|
||||||
holder.fileInformation.clearColorFilter()
|
holder.fileInformationButton.clearColorFilter()
|
||||||
} else {
|
} else {
|
||||||
holder.fileInformation.setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)
|
holder.fileInformationButton.setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modification
|
// Modification
|
||||||
fileDatabaseInfo.getModificationString()?.let {
|
databaseFile.databaseLastModified?.let {
|
||||||
holder.fileModification.text = it
|
holder.fileModification.text = it
|
||||||
holder.fileModification.visibility = View.VISIBLE
|
holder.fileModificationContainer.visibility = View.VISIBLE
|
||||||
} ?: run {
|
} ?: run {
|
||||||
holder.fileModification.visibility = View.GONE
|
holder.fileModificationContainer.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
// Size
|
// Size
|
||||||
fileDatabaseInfo.getSizeString()?.let {
|
databaseFile.databaseSize?.let {
|
||||||
holder.fileSize.text = it
|
holder.fileSize.text = it
|
||||||
holder.fileSize.visibility = View.VISIBLE
|
holder.fileSize.visibility = View.VISIBLE
|
||||||
} ?: run {
|
} ?: run {
|
||||||
@@ -107,15 +115,24 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Click on information
|
// Click on information
|
||||||
val isExpanded = position == mExpandedPosition
|
val isExpanded = databaseFile == mExpandedDatabaseFile
|
||||||
//This line hides or shows the layout in question
|
// Hides or shows info
|
||||||
holder.fileExpandContainer.visibility = if (isExpanded) View.VISIBLE else View.GONE
|
holder.fileExpandContainer.apply {
|
||||||
|
if (isExpanded) {
|
||||||
|
if (visibility != View.VISIBLE) {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
expand(true, resources.getDimensionPixelSize(R.dimen.item_file_info_height))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
collapse(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Save alias modification
|
// Save alias modification
|
||||||
holder.fileAliasCloseButton.setOnClickListener {
|
holder.fileAliasCloseButton.setOnClickListener {
|
||||||
// Change the alias
|
// Change the alias
|
||||||
fileHistoryEntity.databaseAlias = holder.fileAliasEdit.text.toString()
|
databaseFile.databaseAlias = holder.fileAliasEdit.text.toString()
|
||||||
saveAliasListener?.invoke(fileHistoryEntity)
|
saveAliasListener?.invoke(databaseFile)
|
||||||
|
|
||||||
// Finish save mode
|
// Finish save mode
|
||||||
holder.fileMainSwitcher.showPrevious()
|
holder.fileMainSwitcher.showPrevious()
|
||||||
@@ -130,20 +147,22 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
holder.fileDeleteButton.setOnClickListener {
|
holder.fileDeleteButton.setOnClickListener {
|
||||||
fileSelectClearListener?.invoke(fileHistoryEntity)
|
fileSelectClearListener?.invoke(databaseFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isExpanded) {
|
if (isExpanded) {
|
||||||
mPreviousExpandedPosition = position
|
mPreviousExpandedDatabaseFile = databaseFile
|
||||||
}
|
}
|
||||||
|
holder.fileInformationButton.apply {
|
||||||
holder.fileInformation.setOnClickListener {
|
animate().rotation(if (isExpanded) 180F else 0F).start()
|
||||||
mExpandedPosition = if (isExpanded) -1 else position
|
setOnClickListener {
|
||||||
|
mExpandedDatabaseFile = if (isExpanded) null else databaseFile
|
||||||
// Notify change
|
// Notify change
|
||||||
if (mPreviousExpandedPosition < itemCount)
|
val previousExpandedPosition = listDatabaseFiles.indexOf(mPreviousExpandedDatabaseFile)
|
||||||
notifyItemChanged(mPreviousExpandedPosition)
|
notifyItemChanged(previousExpandedPosition)
|
||||||
notifyItemChanged(position)
|
val expandedPosition = listDatabaseFiles.indexOf(mExpandedDatabaseFile)
|
||||||
|
notifyItemChanged(expandedPosition)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh View / Close alias modification if not contains fileAlias
|
// Refresh View / Close alias modification if not contains fileAlias
|
||||||
@@ -160,33 +179,68 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
|||||||
listDatabaseFiles.clear()
|
listDatabaseFiles.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<FileDatabaseHistoryEntity>) {
|
fun addDatabaseFileHistory(fileDatabaseHistoryToAdd: DatabaseFile) {
|
||||||
listDatabaseFiles.clear()
|
listDatabaseFiles.add(0, fileDatabaseHistoryToAdd)
|
||||||
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
|
notifyItemInserted(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: FileDatabaseHistoryEntity) {
|
fun updateDatabaseFileHistory(fileDatabaseHistoryToUpdate: DatabaseFile) {
|
||||||
listDatabaseFiles.remove(fileDatabaseHistoryToDelete)
|
val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToUpdate)
|
||||||
|
if (listDatabaseFiles.remove(fileDatabaseHistoryToUpdate)) {
|
||||||
|
listDatabaseFiles.add(index, fileDatabaseHistoryToUpdate)
|
||||||
|
notifyItemChanged(index)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setOnFileDatabaseHistoryOpenListener(listener : ((FileDatabaseHistoryEntity)->Unit)?) {
|
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: DatabaseFile) {
|
||||||
|
val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToDelete)
|
||||||
|
if (listDatabaseFiles.remove(fileDatabaseHistoryToDelete)) {
|
||||||
|
notifyItemRemoved(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun replaceAllDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<DatabaseFile>) {
|
||||||
|
if (listDatabaseFiles.isEmpty()) {
|
||||||
|
listFileDatabaseHistoryToAdd.forEach {
|
||||||
|
listDatabaseFiles.add(it)
|
||||||
|
notifyItemInserted(listDatabaseFiles.size)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
listDatabaseFiles.clear()
|
||||||
|
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDefaultDatabase(databaseUri: Uri?) {
|
||||||
|
val defaultDatabaseFile = listDatabaseFiles.firstOrNull { it.databaseUri == databaseUri }
|
||||||
|
mDefaultDatabaseFile = defaultDatabaseFile
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOnDefaultDatabaseListener(listener: ((DatabaseFile?) -> Unit)?) {
|
||||||
|
this.defaultDatabaseListener = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOnFileDatabaseHistoryOpenListener(listener : ((DatabaseFile)->Unit)?) {
|
||||||
this.fileItemOpenListener = listener
|
this.fileItemOpenListener = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setOnFileDatabaseHistoryDeleteListener(listener : ((FileDatabaseHistoryEntity)->Boolean)?) {
|
fun setOnFileDatabaseHistoryDeleteListener(listener : ((DatabaseFile)->Boolean)?) {
|
||||||
this.fileSelectClearListener = listener
|
this.fileSelectClearListener = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setOnSaveAliasListener(listener : ((FileDatabaseHistoryEntity)->Unit)?) {
|
fun setOnSaveAliasListener(listener : ((DatabaseFile)->Unit)?) {
|
||||||
this.saveAliasListener = listener
|
this.saveAliasListener = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
var fileContainer: ViewGroup = itemView.findViewById(R.id.file_container_basic_info)
|
var fileContainer: ViewGroup = itemView.findViewById(R.id.file_container_basic_info)
|
||||||
|
|
||||||
|
var defaultFileButton: CompoundButton = itemView.findViewById(R.id.default_file_button)
|
||||||
var fileAlias: TextView = itemView.findViewById(R.id.file_alias)
|
var fileAlias: TextView = itemView.findViewById(R.id.file_alias)
|
||||||
var fileInformation: ImageView = itemView.findViewById(R.id.file_information)
|
var fileInformationButton: ImageView = itemView.findViewById(R.id.file_information_button)
|
||||||
|
|
||||||
var fileMainSwitcher: ViewSwitcher = itemView.findViewById(R.id.file_main_switcher)
|
var fileMainSwitcher: ViewSwitcher = itemView.findViewById(R.id.file_main_switcher)
|
||||||
var fileAliasEdit: EditText = itemView.findViewById(R.id.file_alias_edit)
|
var fileAliasEdit: EditText = itemView.findViewById(R.id.file_alias_edit)
|
||||||
@@ -196,6 +250,7 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
|||||||
var fileModifyButton: ImageView = itemView.findViewById(R.id.file_modify_button)
|
var fileModifyButton: ImageView = itemView.findViewById(R.id.file_modify_button)
|
||||||
var fileDeleteButton: ImageView = itemView.findViewById(R.id.file_delete_button)
|
var fileDeleteButton: ImageView = itemView.findViewById(R.id.file_delete_button)
|
||||||
var filePath: TextView = itemView.findViewById(R.id.file_path)
|
var filePath: TextView = itemView.findViewById(R.id.file_path)
|
||||||
|
var fileModificationContainer: ViewGroup = itemView.findViewById(R.id.file_modification_container)
|
||||||
var fileModification: TextView = itemView.findViewById(R.id.file_modification)
|
var fileModification: TextView = itemView.findViewById(R.id.file_modification)
|
||||||
var fileSize: TextView = itemView.findViewById(R.id.file_size)
|
var fileSize: TextView = itemView.findViewById(R.id.file_size)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SortedList
|
import androidx.recyclerview.widget.SortedList
|
||||||
import androidx.recyclerview.widget.SortedListAdapterCallback
|
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||||
@@ -51,64 +52,69 @@ import java.util.*
|
|||||||
class NodeAdapter (private val context: Context)
|
class NodeAdapter (private val context: Context)
|
||||||
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
|
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
|
||||||
|
|
||||||
private var nodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
|
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
|
||||||
private val nodeSortedListCallback: NodeSortedListCallback
|
private val mNodeSortedListCallback: NodeSortedListCallback
|
||||||
private val nodeSortedList: SortedList<Node>
|
private val mNodeSortedList: SortedList<Node>
|
||||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
private val mInflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
|
|
||||||
private var calculateViewTypeTextSize = Array(2) { true} // number of view type
|
private var mCalculateViewTypeTextSize = Array(2) { true} // number of view type
|
||||||
private var textSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
||||||
private var prefSizeMultiplier: Float = 0F
|
private var mPrefSizeMultiplier: Float = 0F
|
||||||
private var subtextDefaultDimension: Float = 0F
|
private var mSubtextDefaultDimension: Float = 0F
|
||||||
private var infoTextDefaultDimension: Float = 0F
|
private var mInfoTextDefaultDimension: Float = 0F
|
||||||
private var numberChildrenTextDefaultDimension: Float = 0F
|
private var mNumberChildrenTextDefaultDimension: Float = 0F
|
||||||
private var iconDefaultDimension: Float = 0F
|
private var mIconDefaultDimension: Float = 0F
|
||||||
|
|
||||||
private var showUserNames: Boolean = true
|
private var mShowUserNames: Boolean = true
|
||||||
private var showNumberEntries: Boolean = true
|
private var mShowNumberEntries: Boolean = true
|
||||||
private var entryFilters = arrayOf<Group.ChildFilter>()
|
private var mEntryFilters = arrayOf<Group.ChildFilter>()
|
||||||
|
|
||||||
private var actionNodesList = LinkedList<Node>()
|
private var mActionNodesList = LinkedList<Node>()
|
||||||
private var nodeClickCallback: NodeClickCallback? = null
|
private var mNodeClickCallback: NodeClickCallback? = null
|
||||||
|
|
||||||
private val mDatabase: Database
|
private val mDatabase: Database
|
||||||
|
|
||||||
private val iconGroupColor: Int
|
@ColorInt
|
||||||
private val iconEntryColor: Int
|
private val mContentSelectionColor: Int
|
||||||
|
@ColorInt
|
||||||
|
private val mIconGroupColor: Int
|
||||||
|
@ColorInt
|
||||||
|
private val mIconEntryColor: Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the adapter contains or not any element
|
* Determine if the adapter contains or not any element
|
||||||
* @return true if the list is empty
|
* @return true if the list is empty
|
||||||
*/
|
*/
|
||||||
val isEmpty: Boolean
|
val isEmpty: Boolean
|
||||||
get() = nodeSortedList.size() <= 0
|
get() = mNodeSortedList.size() <= 0
|
||||||
|
|
||||||
init {
|
init {
|
||||||
this.infoTextDefaultDimension = context.resources.getDimension(R.dimen.list_medium_size_default)
|
this.mIconDefaultDimension = context.resources.getDimension(R.dimen.list_icon_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)
|
|
||||||
|
|
||||||
assignPreferences()
|
assignPreferences()
|
||||||
|
|
||||||
this.nodeSortedListCallback = NodeSortedListCallback()
|
this.mNodeSortedListCallback = NodeSortedListCallback()
|
||||||
this.nodeSortedList = SortedList(Node::class.java, nodeSortedListCallback)
|
this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback)
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
this.mDatabase = Database.getInstance()
|
this.mDatabase = Database.getInstance()
|
||||||
|
|
||||||
|
// Color of content selection
|
||||||
|
val taContentSelectionColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
||||||
|
this.mContentSelectionColor = taContentSelectionColor.getColor(0, Color.WHITE)
|
||||||
|
taContentSelectionColor.recycle()
|
||||||
// Retrieve the color to tint the icon
|
// Retrieve the color to tint the icon
|
||||||
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
||||||
this.iconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
|
this.mIconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
|
||||||
taTextColorPrimary.recycle()
|
taTextColorPrimary.recycle()
|
||||||
// In two times to fix bug compilation
|
// In two times to fix bug compilation
|
||||||
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
this.iconEntryColor = taTextColor.getColor(0, Color.BLACK)
|
this.mIconEntryColor = taTextColor.getColor(0, Color.BLACK)
|
||||||
taTextColor.recycle()
|
taTextColor.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun assignPreferences() {
|
fun assignPreferences() {
|
||||||
this.prefSizeMultiplier = PreferencesUtil.getListTextSize(context)
|
this.mPrefSizeMultiplier = PreferencesUtil.getListTextSize(context)
|
||||||
|
|
||||||
notifyChangeSort(
|
notifyChangeSort(
|
||||||
PreferencesUtil.getListSort(context),
|
PreferencesUtil.getListSort(context),
|
||||||
@@ -119,13 +125,13 @@ class NodeAdapter (private val context: Context)
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
this.showUserNames = PreferencesUtil.showUsernamesListEntries(context)
|
this.mShowUserNames = PreferencesUtil.showUsernamesListEntries(context)
|
||||||
this.showNumberEntries = PreferencesUtil.showNumberEntries(context)
|
this.mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
|
||||||
|
|
||||||
this.entryFilters = Group.ChildFilter.getDefaults(context)
|
this.mEntryFilters = Group.ChildFilter.getDefaults(context)
|
||||||
|
|
||||||
// Reinit textSize for all view type
|
// Reinit textSize for all view type
|
||||||
calculateViewTypeTextSize.forEachIndexed { index, _ -> calculateViewTypeTextSize[index] = true }
|
mCalculateViewTypeTextSize.forEachIndexed { index, _ -> mCalculateViewTypeTextSize[index] = true }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -133,13 +139,12 @@ class NodeAdapter (private val context: Context)
|
|||||||
*/
|
*/
|
||||||
fun rebuildList(group: Group) {
|
fun rebuildList(group: Group) {
|
||||||
assignPreferences()
|
assignPreferences()
|
||||||
nodeSortedList.replaceAll(group.getFilteredChildren(*entryFilters)
|
mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class NodeSortedListCallback: SortedListAdapterCallback<Node>(this) {
|
private inner class NodeSortedListCallback: SortedListAdapterCallback<Node>(this) {
|
||||||
override fun compare(item1: Node, item2: Node): Int {
|
override fun compare(item1: Node, item2: Node): Int {
|
||||||
return nodeComparator!!.compare(item1, item2)
|
return mNodeComparator!!.compare(item1, item2)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
|
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
|
||||||
@@ -154,7 +159,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun contains(node: Node): Boolean {
|
fun contains(node: Node): Boolean {
|
||||||
return nodeSortedList.indexOf(node) != SortedList.INVALID_POSITION
|
return mNodeSortedList.indexOf(node) != SortedList.INVALID_POSITION
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -162,7 +167,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
* @param node Node to add
|
* @param node Node to add
|
||||||
*/
|
*/
|
||||||
fun addNode(node: Node) {
|
fun addNode(node: Node) {
|
||||||
nodeSortedList.add(node)
|
mNodeSortedList.add(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -170,7 +175,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
* @param nodes Nodes to add
|
* @param nodes Nodes to add
|
||||||
*/
|
*/
|
||||||
fun addNodes(nodes: List<Node>) {
|
fun addNodes(nodes: List<Node>) {
|
||||||
nodeSortedList.addAll(nodes)
|
mNodeSortedList.addAll(nodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -178,7 +183,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
* @param node Node to delete
|
* @param node Node to delete
|
||||||
*/
|
*/
|
||||||
fun removeNode(node: Node) {
|
fun removeNode(node: Node) {
|
||||||
nodeSortedList.remove(node)
|
mNodeSortedList.remove(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -187,7 +192,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
*/
|
*/
|
||||||
fun removeNodes(nodes: List<Node>) {
|
fun removeNodes(nodes: List<Node>) {
|
||||||
nodes.forEach { node ->
|
nodes.forEach { node ->
|
||||||
nodeSortedList.remove(node)
|
mNodeSortedList.remove(node)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,9 +200,9 @@ class NodeAdapter (private val context: Context)
|
|||||||
* Remove a node at [position] in the list
|
* Remove a node at [position] in the list
|
||||||
*/
|
*/
|
||||||
fun removeNodeAt(position: Int) {
|
fun removeNodeAt(position: Int) {
|
||||||
nodeSortedList.removeItemAt(position)
|
mNodeSortedList.removeItemAt(position)
|
||||||
// Refresh all the next items
|
// Refresh all the next items
|
||||||
notifyItemRangeChanged(position, nodeSortedList.size() - position)
|
notifyItemRangeChanged(position, mNodeSortedList.size() - position)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -218,10 +223,10 @@ class NodeAdapter (private val context: Context)
|
|||||||
* @param newNode Node after the update
|
* @param newNode Node after the update
|
||||||
*/
|
*/
|
||||||
fun updateNode(oldNode: Node, newNode: Node) {
|
fun updateNode(oldNode: Node, newNode: Node) {
|
||||||
nodeSortedList.beginBatchedUpdates()
|
mNodeSortedList.beginBatchedUpdates()
|
||||||
nodeSortedList.remove(oldNode)
|
mNodeSortedList.remove(oldNode)
|
||||||
nodeSortedList.add(newNode)
|
mNodeSortedList.add(newNode)
|
||||||
nodeSortedList.endBatchedUpdates()
|
mNodeSortedList.endBatchedUpdates()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -230,30 +235,30 @@ class NodeAdapter (private val context: Context)
|
|||||||
* @param newNodes Node after the update
|
* @param newNodes Node after the update
|
||||||
*/
|
*/
|
||||||
fun updateNodes(oldNodes: List<Node>, newNodes: List<Node>) {
|
fun updateNodes(oldNodes: List<Node>, newNodes: List<Node>) {
|
||||||
nodeSortedList.beginBatchedUpdates()
|
mNodeSortedList.beginBatchedUpdates()
|
||||||
oldNodes.forEach { oldNode ->
|
oldNodes.forEach { oldNode ->
|
||||||
nodeSortedList.remove(oldNode)
|
mNodeSortedList.remove(oldNode)
|
||||||
}
|
}
|
||||||
nodeSortedList.addAll(newNodes)
|
mNodeSortedList.addAll(newNodes)
|
||||||
nodeSortedList.endBatchedUpdates()
|
mNodeSortedList.endBatchedUpdates()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun notifyNodeChanged(node: Node) {
|
fun notifyNodeChanged(node: Node) {
|
||||||
notifyItemChanged(nodeSortedList.indexOf(node))
|
notifyItemChanged(mNodeSortedList.indexOf(node))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setActionNodes(actionNodes: List<Node>) {
|
fun setActionNodes(actionNodes: List<Node>) {
|
||||||
this.actionNodesList.apply {
|
this.mActionNodesList.apply {
|
||||||
clear()
|
clear()
|
||||||
addAll(actionNodes)
|
addAll(actionNodes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unselectActionNodes() {
|
fun unselectActionNodes() {
|
||||||
actionNodesList.forEach {
|
mActionNodesList.forEach {
|
||||||
notifyItemChanged(nodeSortedList.indexOf(it))
|
notifyItemChanged(mNodeSortedList.indexOf(it))
|
||||||
}
|
}
|
||||||
this.actionNodesList.apply {
|
this.mActionNodesList.apply {
|
||||||
clear()
|
clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,42 +268,55 @@ class NodeAdapter (private val context: Context)
|
|||||||
*/
|
*/
|
||||||
fun notifyChangeSort(sortNodeEnum: SortNodeEnum,
|
fun notifyChangeSort(sortNodeEnum: SortNodeEnum,
|
||||||
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
||||||
this.nodeComparator = sortNodeEnum.getNodeComparator(sortNodeParameters)
|
this.mNodeComparator = sortNodeEnum.getNodeComparator(sortNodeParameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getItemViewType(position: Int): Int {
|
||||||
return nodeSortedList.get(position).type.ordinal
|
return mNodeSortedList.get(position).type.ordinal
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NodeViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NodeViewHolder {
|
||||||
val view: View = if (viewType == Type.GROUP.ordinal) {
|
val view: View = if (viewType == Type.GROUP.ordinal) {
|
||||||
inflater.inflate(R.layout.item_list_nodes_group, parent, false)
|
mInflater.inflate(R.layout.item_list_nodes_group, parent, false)
|
||||||
} else {
|
} else {
|
||||||
inflater.inflate(R.layout.item_list_nodes_entry, parent, false)
|
mInflater.inflate(R.layout.item_list_nodes_entry, parent, false)
|
||||||
}
|
}
|
||||||
return NodeViewHolder(view)
|
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) {
|
override fun onBindViewHolder(holder: NodeViewHolder, position: Int) {
|
||||||
val subNode = nodeSortedList.get(position)
|
val subNode = mNodeSortedList.get(position)
|
||||||
|
|
||||||
|
// Node selection
|
||||||
|
holder.container.isSelected = mActionNodesList.contains(subNode)
|
||||||
|
|
||||||
// Assign image
|
// Assign image
|
||||||
val iconColor = when (subNode.type) {
|
val iconColor = if (holder.container.isSelected)
|
||||||
Type.GROUP -> iconGroupColor
|
mContentSelectionColor
|
||||||
Type.ENTRY -> iconEntryColor
|
else when (subNode.type) {
|
||||||
|
Type.GROUP -> mIconGroupColor
|
||||||
|
Type.ENTRY -> mIconEntryColor
|
||||||
}
|
}
|
||||||
|
holder.imageIdentifier?.setColorFilter(iconColor)
|
||||||
holder.icon.apply {
|
holder.icon.apply {
|
||||||
assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
|
assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
|
||||||
// Relative size of the icon
|
// Relative size of the icon
|
||||||
layoutParams?.apply {
|
layoutParams?.apply {
|
||||||
height = (iconDefaultDimension * prefSizeMultiplier).toInt()
|
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
|
||||||
width = (iconDefaultDimension * prefSizeMultiplier).toInt()
|
width = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign text
|
// Assign text
|
||||||
holder.text.apply {
|
holder.text.apply {
|
||||||
text = subNode.title
|
text = subNode.title
|
||||||
setTextSize(textSizeUnit, infoTextDefaultDimension, prefSizeMultiplier)
|
setTextSize(mTextSizeUnit, mInfoTextDefaultDimension, mPrefSizeMultiplier)
|
||||||
strikeOut(subNode.isCurrentlyExpires)
|
strikeOut(subNode.isCurrentlyExpires)
|
||||||
}
|
}
|
||||||
// Add subText with username
|
// Add subText with username
|
||||||
@@ -316,24 +334,27 @@ class NodeAdapter (private val context: Context)
|
|||||||
holder.text.text = entry.getVisualTitle()
|
holder.text.text = entry.getVisualTitle()
|
||||||
holder.subText.apply {
|
holder.subText.apply {
|
||||||
val username = entry.username
|
val username = entry.username
|
||||||
if (showUserNames && username.isNotEmpty()) {
|
if (mShowUserNames && username.isNotEmpty()) {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
text = username
|
text = username
|
||||||
setTextSize(textSizeUnit, subtextDefaultDimension, prefSizeMultiplier)
|
setTextSize(mTextSizeUnit, mSubtextDefaultDimension, mPrefSizeMultiplier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
holder.attachmentIcon?.visibility =
|
||||||
|
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
mDatabase.stopManageEntry(entry)
|
mDatabase.stopManageEntry(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add number of entries in groups
|
// Add number of entries in groups
|
||||||
if (subNode.type == Type.GROUP) {
|
if (subNode.type == Type.GROUP) {
|
||||||
if (showNumberEntries) {
|
if (mShowNumberEntries) {
|
||||||
holder.numberChildren?.apply {
|
holder.numberChildren?.apply {
|
||||||
text = (subNode as Group)
|
text = (subNode as Group)
|
||||||
.getNumberOfChildEntries(*entryFilters)
|
.getNumberOfChildEntries(mEntryFilters)
|
||||||
.toString()
|
.toString()
|
||||||
setTextSize(textSizeUnit, numberChildrenTextDefaultDimension, prefSizeMultiplier)
|
setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier)
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -343,24 +364,22 @@ class NodeAdapter (private val context: Context)
|
|||||||
|
|
||||||
// Assign click
|
// Assign click
|
||||||
holder.container.setOnClickListener {
|
holder.container.setOnClickListener {
|
||||||
nodeClickCallback?.onNodeClick(subNode)
|
mNodeClickCallback?.onNodeClick(subNode)
|
||||||
}
|
}
|
||||||
holder.container.setOnLongClickListener {
|
holder.container.setOnLongClickListener {
|
||||||
nodeClickCallback?.onNodeLongClick(subNode) ?: false
|
mNodeClickCallback?.onNodeLongClick(subNode) ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.container.isSelected = actionNodesList.contains(subNode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
return nodeSortedList.size()
|
return mNodeSortedList.size()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assign a listener when a node is clicked
|
* Assign a listener when a node is clicked
|
||||||
*/
|
*/
|
||||||
fun setOnNodeClickListener(nodeClickCallback: NodeClickCallback?) {
|
fun setOnNodeClickListener(nodeClickCallback: NodeClickCallback?) {
|
||||||
this.nodeClickCallback = nodeClickCallback
|
this.mNodeClickCallback = nodeClickCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -373,10 +392,12 @@ class NodeAdapter (private val context: Context)
|
|||||||
|
|
||||||
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
var container: View = itemView.findViewById(R.id.node_container)
|
var container: View = itemView.findViewById(R.id.node_container)
|
||||||
|
var imageIdentifier: ImageView? = itemView.findViewById(R.id.node_image_identifier)
|
||||||
var icon: ImageView = itemView.findViewById(R.id.node_icon)
|
var icon: ImageView = itemView.findViewById(R.id.node_icon)
|
||||||
var text: TextView = itemView.findViewById(R.id.node_text)
|
var text: TextView = itemView.findViewById(R.id.node_text)
|
||||||
var subText: TextView = itemView.findViewById(R.id.node_subtext)
|
var subText: TextView = itemView.findViewById(R.id.node_subtext)
|
||||||
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
||||||
|
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
|
|
||||||
private val cursorInflater: LayoutInflater? = context.getSystemService(
|
private val cursorInflater: LayoutInflater? = context.getSystemService(
|
||||||
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||||
private var displayUsername: Boolean = false
|
private var mDisplayUsername: Boolean = false
|
||||||
|
private var mOmitBackup: Boolean = true
|
||||||
private val iconColor: Int
|
private val iconColor: Int
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -59,7 +60,8 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun reInit(context: Context) {
|
fun reInit(context: Context) {
|
||||||
this.displayUsername = PreferencesUtil.showUsernamesListEntries(context)
|
this.mDisplayUsername = PreferencesUtil.showUsernamesListEntries(context)
|
||||||
|
this.mOmitBackup = PreferencesUtil.omitBackup(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
|
override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
|
||||||
@@ -93,7 +95,7 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
// Assign subtitle
|
// Assign subtitle
|
||||||
viewHolder.textViewSubTitle?.apply {
|
viewHolder.textViewSubTitle?.apply {
|
||||||
val entryUsername = currentEntry.username
|
val entryUsername = currentEntry.username
|
||||||
text = if (displayUsername && entryUsername.isNotEmpty()) {
|
text = if (mDisplayUsername && entryUsername.isNotEmpty()) {
|
||||||
String.format("(%s)", entryUsername)
|
String.format("(%s)", entryUsername)
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
@@ -129,10 +131,12 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
if (database.type == DatabaseKDBX.TYPE)
|
if (database.type == DatabaseKDBX.TYPE)
|
||||||
cursorKDBX = EntryCursorKDBX()
|
cursorKDBX = EntryCursorKDBX()
|
||||||
|
|
||||||
val searchGroup = database.createVirtualGroupFromSearch(query, SearchHelper.MAX_SEARCH_ENTRY)
|
val searchGroup = database.createVirtualGroupFromSearch(query,
|
||||||
|
mOmitBackup,
|
||||||
|
SearchHelper.MAX_SEARCH_ENTRY)
|
||||||
if (searchGroup != null) {
|
if (searchGroup != null) {
|
||||||
// Search in hide entries but not meta-stream
|
// Search in hide entries but not meta-stream
|
||||||
for (entry in searchGroup.getFilteredChildEntries(*Group.ChildFilter.getDefaults(context))) {
|
for (entry in searchGroup.getFilteredChildEntries(Group.ChildFilter.getDefaults(context))) {
|
||||||
entry.entryKDB?.let {
|
entry.entryKDB?.let {
|
||||||
cursorKDB?.addEntry(it)
|
cursorKDB?.addEntry(it)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.app
|
|||||||
import androidx.multidex.MultiDexApplication
|
import androidx.multidex.MultiDexApplication
|
||||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
class App : MultiDexApplication() {
|
class App : MultiDexApplication() {
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ class App : MultiDexApplication() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onTerminate() {
|
override fun onTerminate() {
|
||||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this))
|
||||||
super.onTerminate()
|
super.onTerminate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ package com.kunzisoft.keepass.app;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Process;
|
import android.os.Process;
|
||||||
|
|
||||||
import com.kunzisoft.keepass.utils.StringUtil;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
@@ -66,7 +64,7 @@ public final class PRNGFixes {
|
|||||||
|
|
||||||
private static boolean supportedOnThisDevice() {
|
private static boolean supportedOnThisDevice() {
|
||||||
// Blacklist on samsung devices
|
// Blacklist on samsung devices
|
||||||
if (StringUtil.INSTANCE.indexOfIgnoreCase(Build.MANUFACTURER, "samsung", Locale.ENGLISH) >= 0) {
|
if (Build.MANUFACTURER.toLowerCase(Locale.ENGLISH).contains("samsung")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class CipherDatabaseAction(applicationContext: Context) {
|
|||||||
|
|
||||||
fun getCipherDatabase(databaseUri: Uri,
|
fun getCipherDatabase(databaseUri: Uri,
|
||||||
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
|
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
|
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
|
||||||
},
|
},
|
||||||
@@ -51,7 +51,7 @@ class CipherDatabaseAction(applicationContext: Context) {
|
|||||||
|
|
||||||
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
|
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
|
||||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
|
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ class CipherDatabaseAction(applicationContext: Context) {
|
|||||||
|
|
||||||
fun deleteByDatabaseUri(databaseUri: Uri,
|
fun deleteByDatabaseUri(databaseUri: Uri,
|
||||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
|
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
|
||||||
},
|
},
|
||||||
@@ -81,7 +81,7 @@ class CipherDatabaseAction(applicationContext: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAll() {
|
fun deleteAll() {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
cipherDatabaseDao.deleteAll()
|
cipherDatabaseDao.deleteAll()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,31 +21,45 @@ package com.kunzisoft.keepass.app.database
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import com.kunzisoft.keepass.model.DatabaseFile
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
|
||||||
|
|
||||||
class FileDatabaseHistoryAction(applicationContext: Context) {
|
class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||||
|
|
||||||
private val databaseFileHistoryDao =
|
private val databaseFileHistoryDao =
|
||||||
AppDatabase
|
AppDatabase
|
||||||
.getDatabase(applicationContext)
|
.getDatabase(applicationContext)
|
||||||
.fileDatabaseHistoryDao()
|
.fileDatabaseHistoryDao()
|
||||||
|
|
||||||
fun getFileDatabaseHistory(databaseUri: Uri,
|
fun getDatabaseFile(databaseUri: Uri,
|
||||||
fileHistoryResultListener: (fileDatabaseHistoryResult: FileDatabaseHistoryEntity?) -> Unit) {
|
databaseFileResult: (DatabaseFile?) -> Unit) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
val fileDatabaseHistoryEntity = databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||||
|
val fileDatabaseInfo = FileDatabaseInfo(applicationContext, databaseUri)
|
||||||
|
DatabaseFile(
|
||||||
|
databaseUri,
|
||||||
|
UriUtil.parse(fileDatabaseHistoryEntity?.keyFileUri),
|
||||||
|
UriUtil.decode(fileDatabaseHistoryEntity?.databaseUri),
|
||||||
|
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias ?: ""),
|
||||||
|
fileDatabaseInfo.exists,
|
||||||
|
fileDatabaseInfo.getModificationString(),
|
||||||
|
fileDatabaseInfo.getSizeString()
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fileHistoryResultListener.invoke(it)
|
databaseFileResult.invoke(it)
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getKeyFileUriByDatabaseUri(databaseUri: Uri,
|
fun getKeyFileUriByDatabaseUri(databaseUri: Uri,
|
||||||
keyFileUriResultListener: (Uri?) -> Unit) {
|
keyFileUriResultListener: (Uri?) -> Unit) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||||
},
|
},
|
||||||
@@ -59,61 +73,124 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
|
|||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAllFileDatabaseHistories(fileHistoryResultListener: (fileDatabaseHistoryResult: List<FileDatabaseHistoryEntity>?) -> Unit) {
|
fun getDatabaseFileList(databaseFileListResult: (List<DatabaseFile>) -> Unit) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.getAll()
|
val hideBrokenLocations = PreferencesUtil.hideBrokenLocations(applicationContext)
|
||||||
|
// Show only uri accessible
|
||||||
|
val databaseFileListLoaded = ArrayList<DatabaseFile>()
|
||||||
|
databaseFileHistoryDao.getAll().forEach { fileDatabaseHistoryEntity ->
|
||||||
|
val fileDatabaseInfo = FileDatabaseInfo(applicationContext, fileDatabaseHistoryEntity.databaseUri)
|
||||||
|
if (hideBrokenLocations && fileDatabaseInfo.exists
|
||||||
|
|| !hideBrokenLocations) {
|
||||||
|
databaseFileListLoaded.add(
|
||||||
|
DatabaseFile(
|
||||||
|
UriUtil.parse(fileDatabaseHistoryEntity.databaseUri),
|
||||||
|
UriUtil.parse(fileDatabaseHistoryEntity.keyFileUri),
|
||||||
|
UriUtil.decode(fileDatabaseHistoryEntity.databaseUri),
|
||||||
|
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
|
||||||
|
fileDatabaseInfo.exists,
|
||||||
|
fileDatabaseInfo.getModificationString(),
|
||||||
|
fileDatabaseInfo.getSizeString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
databaseFileListLoaded
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fileHistoryResultListener.invoke(it)
|
databaseFileList ->
|
||||||
}
|
databaseFileList?.let {
|
||||||
).execute()
|
databaseFileListResult.invoke(it)
|
||||||
}
|
|
||||||
|
|
||||||
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null) {
|
|
||||||
addOrUpdateFileDatabaseHistory(FileDatabaseHistoryEntity(
|
|
||||||
databaseUri.toString(),
|
|
||||||
"",
|
|
||||||
keyFileUri?.toString(),
|
|
||||||
System.currentTimeMillis()
|
|
||||||
), true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addOrUpdateFileDatabaseHistory(fileDatabaseHistory: FileDatabaseHistoryEntity, unmodifiedAlias: Boolean = false) {
|
|
||||||
ActionDatabaseAsyncTask(
|
|
||||||
{
|
|
||||||
val fileDatabaseHistoryRetrieve = databaseFileHistoryDao.getByDatabaseUri(fileDatabaseHistory.databaseUri)
|
|
||||||
|
|
||||||
if (unmodifiedAlias) {
|
|
||||||
fileDatabaseHistory.databaseAlias = fileDatabaseHistoryRetrieve?.databaseAlias ?: ""
|
|
||||||
}
|
|
||||||
// Update values if history element not yet in the database
|
|
||||||
if (fileDatabaseHistoryRetrieve == null) {
|
|
||||||
databaseFileHistoryDao.add(fileDatabaseHistory)
|
|
||||||
} else {
|
|
||||||
databaseFileHistoryDao.update(fileDatabaseHistory)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteFileDatabaseHistory(fileDatabaseHistory: FileDatabaseHistoryEntity,
|
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null,
|
||||||
fileHistoryDeletedResult: (FileDatabaseHistoryEntity?) -> Unit) {
|
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
|
||||||
ActionDatabaseAsyncTask(
|
addOrUpdateDatabaseFile(DatabaseFile(
|
||||||
|
databaseUri,
|
||||||
|
keyFileUri
|
||||||
|
), databaseFileAddedOrUpdatedResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addOrUpdateDatabaseFile(databaseFileToAddOrUpdate: DatabaseFile,
|
||||||
|
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
|
||||||
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.delete(fileDatabaseHistory)
|
databaseFileToAddOrUpdate.databaseUri?.let { databaseUri ->
|
||||||
|
// Try to get info in database first
|
||||||
|
val fileDatabaseHistoryRetrieve = databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||||
|
|
||||||
|
// Complete alias if not exists
|
||||||
|
val fileDatabaseHistory = FileDatabaseHistoryEntity(
|
||||||
|
databaseUri.toString(),
|
||||||
|
databaseFileToAddOrUpdate.databaseAlias
|
||||||
|
?: fileDatabaseHistoryRetrieve?.databaseAlias
|
||||||
|
?: "",
|
||||||
|
databaseFileToAddOrUpdate.keyFileUri?.toString(),
|
||||||
|
System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update values if history element not yet in the database
|
||||||
|
try {
|
||||||
|
if (fileDatabaseHistoryRetrieve == null) {
|
||||||
|
databaseFileHistoryDao.add(fileDatabaseHistory)
|
||||||
|
} else {
|
||||||
|
databaseFileHistoryDao.update(fileDatabaseHistory)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to add or update database history", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
val fileDatabaseInfo = FileDatabaseInfo(applicationContext,
|
||||||
|
fileDatabaseHistory.databaseUri)
|
||||||
|
DatabaseFile(
|
||||||
|
UriUtil.parse(fileDatabaseHistory.databaseUri),
|
||||||
|
UriUtil.parse(fileDatabaseHistory.keyFileUri),
|
||||||
|
UriUtil.decode(fileDatabaseHistory.databaseUri),
|
||||||
|
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
|
||||||
|
fileDatabaseInfo.exists,
|
||||||
|
fileDatabaseInfo.getModificationString(),
|
||||||
|
fileDatabaseInfo.getSizeString()
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
if (it != null && it > 0)
|
databaseFileAddedOrUpdatedResult?.invoke(it)
|
||||||
fileHistoryDeletedResult.invoke(fileDatabaseHistory)
|
}
|
||||||
else
|
).execute()
|
||||||
fileHistoryDeletedResult.invoke(null)
|
}
|
||||||
|
|
||||||
|
fun deleteDatabaseFile(databaseFileToDelete: DatabaseFile,
|
||||||
|
databaseFileDeletedResult: (DatabaseFile?) -> Unit) {
|
||||||
|
IOActionTask(
|
||||||
|
{
|
||||||
|
databaseFileToDelete.databaseUri?.let { databaseUri ->
|
||||||
|
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())?.let { fileDatabaseHistory ->
|
||||||
|
val returnValue = databaseFileHistoryDao.delete(fileDatabaseHistory)
|
||||||
|
if (returnValue > 0) {
|
||||||
|
DatabaseFile(
|
||||||
|
UriUtil.parse(fileDatabaseHistory.databaseUri),
|
||||||
|
UriUtil.parse(fileDatabaseHistory.keyFileUri),
|
||||||
|
UriUtil.decode(fileDatabaseHistory.databaseUri),
|
||||||
|
databaseFileToDelete.databaseAlias
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
databaseFileDeletedResult.invoke(it)
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteKeyFileByDatabaseUri(databaseUri: Uri) {
|
fun deleteKeyFileByDatabaseUri(databaseUri: Uri) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
|
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
|
||||||
}
|
}
|
||||||
@@ -121,7 +198,7 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAllKeyFiles() {
|
fun deleteAllKeyFiles() {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.deleteAllKeyFiles()
|
databaseFileHistoryDao.deleteAllKeyFiles()
|
||||||
}
|
}
|
||||||
@@ -129,12 +206,14 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAll() {
|
fun deleteAll() {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.deleteAll()
|
databaseFileHistoryDao.deleteAll()
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : SingletonHolderParameter<FileDatabaseHistoryAction, Context>(::FileDatabaseHistoryAction)
|
companion object : SingletonHolderParameter<FileDatabaseHistoryAction, Context>(::FileDatabaseHistoryAction) {
|
||||||
|
private val TAG = FileDatabaseHistoryAction::class.java.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,21 +19,27 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.app.database
|
package com.kunzisoft.keepass.app.database
|
||||||
|
|
||||||
import android.os.AsyncTask
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private class to invoke each method in a separate thread
|
* Class to invoke action in a separate IO thread
|
||||||
*/
|
*/
|
||||||
class ActionDatabaseAsyncTask<T>(
|
class IOActionTask<T>(
|
||||||
private val action: () -> T ,
|
private val action: () -> T ,
|
||||||
private val afterActionDatabaseListener: ((T?) -> Unit)? = null
|
private val afterActionDatabaseListener: ((T?) -> Unit)? = null) {
|
||||||
) : AsyncTask<Void, Void, T>() {
|
|
||||||
|
|
||||||
override fun doInBackground(vararg args: Void?): T? {
|
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||||
return action.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPostExecute(result: T?) {
|
fun execute() {
|
||||||
afterActionDatabaseListener?.invoke(result)
|
mainScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val asyncResult: Deferred<T?> = async {
|
||||||
|
action.invoke()
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
afterActionDatabaseListener?.invoke(asyncResult.await())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,14 +34,12 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
@@ -50,7 +48,6 @@ object AutofillHelper {
|
|||||||
private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165
|
private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165
|
||||||
|
|
||||||
private const val ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
|
private const val ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
|
||||||
const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
|
||||||
|
|
||||||
fun retrieveAssistStructure(intent: Intent?): AssistStructure? {
|
fun retrieveAssistStructure(intent: Intent?): AssistStructure? {
|
||||||
intent?.let {
|
intent?.let {
|
||||||
@@ -122,60 +119,34 @@ object AutofillHelper {
|
|||||||
* Build the Autofill response for many entry
|
* Build the Autofill response for many entry
|
||||||
*/
|
*/
|
||||||
fun buildResponse(activity: Activity, entriesInfo: List<EntryInfo>) {
|
fun buildResponse(activity: Activity, entriesInfo: List<EntryInfo>) {
|
||||||
var setResultOk = false
|
if (entriesInfo.isEmpty()) {
|
||||||
activity.intent?.extras?.let { extras ->
|
activity.setResult(Activity.RESULT_CANCELED)
|
||||||
if (extras.containsKey(ASSIST_STRUCTURE)) {
|
|
||||||
activity.intent?.getParcelableExtra<AssistStructure>(ASSIST_STRUCTURE)?.let { structure ->
|
|
||||||
StructureParser(structure).parse()?.let { result ->
|
|
||||||
// New Response
|
|
||||||
val responseBuilder = FillResponse.Builder()
|
|
||||||
entriesInfo.forEach {
|
|
||||||
responseBuilder.addDataset(buildDataset(activity, it, result))
|
|
||||||
}
|
|
||||||
val mReplyIntent = Intent()
|
|
||||||
Log.d(activity.javaClass.name, "Successed Autofill auth.")
|
|
||||||
mReplyIntent.putExtra(
|
|
||||||
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
|
||||||
responseBuilder.build())
|
|
||||||
setResultOk = true
|
|
||||||
activity.setResult(Activity.RESULT_OK, mReplyIntent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!setResultOk) {
|
|
||||||
Log.w(activity.javaClass.name, "Failed Autofill auth.")
|
|
||||||
activity.setResult(Activity.RESULT_CANCELED)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility method to perform actions if item is found or not after an auto search in [database]
|
|
||||||
*/
|
|
||||||
fun checkAutoSearchInfo(context: Context,
|
|
||||||
database: Database,
|
|
||||||
searchInfo: SearchInfo?,
|
|
||||||
onItemsFound: (items: List<EntryInfo>) -> Unit,
|
|
||||||
onItemNotFound: () -> Unit,
|
|
||||||
onDatabaseClosed: () -> Unit) {
|
|
||||||
if (database.loaded && TimeoutHelper.checkTime(context)) {
|
|
||||||
var searchWithoutUI = false
|
|
||||||
if (PreferencesUtil.isAutofillAutoSearchEnable(context)
|
|
||||||
&& searchInfo != null) {
|
|
||||||
// If search provide results
|
|
||||||
database.createVirtualGroupFromSearch(searchInfo, SearchHelper.MAX_SEARCH_ENTRY)?.let { searchGroup ->
|
|
||||||
if (searchGroup.getNumberOfChildEntries() > 0) {
|
|
||||||
searchWithoutUI = true
|
|
||||||
onItemsFound.invoke(
|
|
||||||
searchGroup.getChildEntriesInfo(database))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!searchWithoutUI) {
|
|
||||||
onItemNotFound.invoke()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
onDatabaseClosed.invoke()
|
var setResultOk = false
|
||||||
|
activity.intent?.extras?.let { extras ->
|
||||||
|
if (extras.containsKey(ASSIST_STRUCTURE)) {
|
||||||
|
activity.intent?.getParcelableExtra<AssistStructure>(ASSIST_STRUCTURE)?.let { structure ->
|
||||||
|
StructureParser(structure).parse()?.let { result ->
|
||||||
|
// New Response
|
||||||
|
val responseBuilder = FillResponse.Builder()
|
||||||
|
entriesInfo.forEach {
|
||||||
|
responseBuilder.addDataset(buildDataset(activity, it, result))
|
||||||
|
}
|
||||||
|
val mReplyIntent = Intent()
|
||||||
|
Log.d(activity.javaClass.name, "Successed Autofill auth.")
|
||||||
|
mReplyIntent.putExtra(
|
||||||
|
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
||||||
|
responseBuilder.build())
|
||||||
|
setResultOk = true
|
||||||
|
activity.setResult(Activity.RESULT_OK, mReplyIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!setResultOk) {
|
||||||
|
Log.w(activity.javaClass.name, "Failed Autofill auth.")
|
||||||
|
activity.setResult(Activity.RESULT_CANCELED)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,11 +157,9 @@ object AutofillHelper {
|
|||||||
intent: Intent,
|
intent: Intent,
|
||||||
assistStructure: AssistStructure,
|
assistStructure: AssistStructure,
|
||||||
searchInfo: SearchInfo?) {
|
searchInfo: SearchInfo?) {
|
||||||
EntrySelectionHelper.addEntrySelectionModeExtraInIntent(intent)
|
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||||
intent.putExtra(ASSIST_STRUCTURE, assistStructure)
|
intent.putExtra(ASSIST_STRUCTURE, assistStructure)
|
||||||
searchInfo?.let {
|
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
|
||||||
intent.putExtra(KEY_SEARCH_INFO, it)
|
|
||||||
}
|
|
||||||
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
|
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,99 +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.autofill
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentSender
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
|
||||||
import com.kunzisoft.keepass.activities.GroupActivity
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
class AutofillLauncherActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
|
||||||
val assistStructure = AutofillHelper.retrieveAssistStructure(intent)
|
|
||||||
if (assistStructure != null) {
|
|
||||||
// Build search param
|
|
||||||
val searchInfo = SearchInfo().apply {
|
|
||||||
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
|
|
||||||
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
|
|
||||||
}
|
|
||||||
// If database is open
|
|
||||||
AutofillHelper.checkAutoSearchInfo(this,
|
|
||||||
Database.getInstance(),
|
|
||||||
searchInfo,
|
|
||||||
{ items ->
|
|
||||||
// Items found
|
|
||||||
AutofillHelper.buildResponse(this, items)
|
|
||||||
finish()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Show the database UI to select the entry
|
|
||||||
GroupActivity.launchForAutofillResult(this,
|
|
||||||
assistStructure)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// If database not open
|
|
||||||
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
|
||||||
assistStructure, searchInfo)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
setResult(Activity.RESULT_CANCELED)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
|
|
||||||
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
|
|
||||||
|
|
||||||
fun getAuthIntentSenderForResponse(context: Context,
|
|
||||||
searchInfo: SearchInfo? = null): IntentSender {
|
|
||||||
return PendingIntent.getActivity(context, 0,
|
|
||||||
// Doesn't work with Parcelable (don't know why?)
|
|
||||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
|
||||||
searchInfo?.let {
|
|
||||||
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
|
|
||||||
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -23,55 +23,91 @@ import android.os.Build
|
|||||||
import android.os.CancellationSignal
|
import android.os.CancellationSignal
|
||||||
import android.service.autofill.*
|
import android.service.autofill.*
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.autofill.AutofillId
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
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.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class KeeAutofillService : AutofillService() {
|
class KeeAutofillService : AutofillService() {
|
||||||
|
|
||||||
|
var applicationIdBlocklist: Set<String>? = null
|
||||||
|
var webDomainBlocklist: Set<String>? = null
|
||||||
|
var askToSaveData: Boolean = false
|
||||||
|
private var mLock = AtomicBoolean()
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this)
|
||||||
|
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this)
|
||||||
|
askToSaveData = PreferencesUtil.askToSaveAutofillData(this) // TODO apply when changed
|
||||||
|
}
|
||||||
|
|
||||||
override fun onFillRequest(request: FillRequest,
|
override fun onFillRequest(request: FillRequest,
|
||||||
cancellationSignal: CancellationSignal,
|
cancellationSignal: CancellationSignal,
|
||||||
callback: FillCallback) {
|
callback: FillCallback) {
|
||||||
val fillContexts = request.fillContexts
|
|
||||||
val latestStructure = fillContexts[fillContexts.size - 1].structure
|
|
||||||
|
|
||||||
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
|
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
|
||||||
|
|
||||||
// Check user's settings for authenticating Responses and Datasets.
|
// Lock
|
||||||
StructureParser(latestStructure).parse()?.let { parseResult ->
|
if (!mLock.get()) {
|
||||||
|
mLock.set(true)
|
||||||
|
// Check user's settings for authenticating Responses and Datasets.
|
||||||
|
val latestStructure = request.fillContexts.last().structure
|
||||||
|
StructureParser(latestStructure).parse()?.let { parseResult ->
|
||||||
|
|
||||||
val searchInfo = SearchInfo().apply {
|
// Build search info only if applicationId or webDomain are not blocked
|
||||||
applicationId = parseResult.applicationId
|
if (autofillAllowedFor(parseResult.applicationId, applicationIdBlocklist)
|
||||||
webDomain = parseResult.domain
|
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
|
||||||
}
|
val searchInfo = SearchInfo().apply {
|
||||||
|
applicationId = parseResult.applicationId
|
||||||
AutofillHelper.checkAutoSearchInfo(this,
|
webDomain = parseResult.webDomain
|
||||||
Database.getInstance(),
|
webScheme = parseResult.webScheme
|
||||||
searchInfo,
|
|
||||||
{ items ->
|
|
||||||
val responseBuilder = FillResponse.Builder()
|
|
||||||
AutofillHelper.addHeader(responseBuilder, packageName,
|
|
||||||
parseResult.domain, parseResult.applicationId)
|
|
||||||
items.forEach {
|
|
||||||
responseBuilder.addDataset(AutofillHelper.buildDataset(this, it, parseResult))
|
|
||||||
}
|
|
||||||
callback.onSuccess(responseBuilder.build())
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Show UI if no search result
|
|
||||||
showUIForEntrySelection(parseResult, searchInfo, callback)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Show UI if database not open
|
|
||||||
showUIForEntrySelection(parseResult, searchInfo, callback)
|
|
||||||
}
|
}
|
||||||
)
|
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
|
||||||
|
searchInfo.webDomain = webDomainWithoutSubDomain
|
||||||
|
launchSelection(searchInfo, parseResult, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun launchSelection(searchInfo: SearchInfo,
|
||||||
|
parseResult: StructureParser.Result,
|
||||||
|
callback: FillCallback) {
|
||||||
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
|
Database.getInstance(),
|
||||||
|
searchInfo,
|
||||||
|
{ items ->
|
||||||
|
val responseBuilder = FillResponse.Builder()
|
||||||
|
AutofillHelper.addHeader(responseBuilder, packageName,
|
||||||
|
parseResult.webDomain, parseResult.applicationId)
|
||||||
|
items.forEach {
|
||||||
|
responseBuilder.addDataset(AutofillHelper.buildDataset(this, it, parseResult))
|
||||||
|
}
|
||||||
|
callback.onSuccess(responseBuilder.build())
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Show UI if no search result
|
||||||
|
showUIForEntrySelection(parseResult, searchInfo, callback)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Show UI if database not open
|
||||||
|
showUIForEntrySelection(parseResult, searchInfo, callback)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
||||||
searchInfo: SearchInfo,
|
searchInfo: SearchInfo,
|
||||||
callback: FillCallback) {
|
callback: FillCallback) {
|
||||||
@@ -79,12 +115,12 @@ class KeeAutofillService : AutofillService() {
|
|||||||
if (autofillIds.isNotEmpty()) {
|
if (autofillIds.isNotEmpty()) {
|
||||||
// If the entire Autofill Response is authenticated, AuthActivity is used
|
// If the entire Autofill Response is authenticated, AuthActivity is used
|
||||||
// to generate Response.
|
// to generate Response.
|
||||||
val sender = AutofillLauncherActivity.getAuthIntentSenderForResponse(this,
|
val intentSender = AutofillLauncherActivity.getAuthIntentSenderForSelection(this,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
val responseBuilder = FillResponse.Builder()
|
val responseBuilder = FillResponse.Builder()
|
||||||
val remoteViewsUnlock: RemoteViews = if (!parseResult.domain.isNullOrEmpty()) {
|
val remoteViewsUnlock: RemoteViews = if (!parseResult.webDomain.isNullOrEmpty()) {
|
||||||
RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply {
|
RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply {
|
||||||
setTextViewText(R.id.autofill_web_domain_text, parseResult.domain)
|
setTextViewText(R.id.autofill_web_domain_text, parseResult.webDomain)
|
||||||
}
|
}
|
||||||
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
||||||
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
|
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
|
||||||
@@ -93,15 +129,63 @@ class KeeAutofillService : AutofillService() {
|
|||||||
} else {
|
} else {
|
||||||
RemoteViews(packageName, R.layout.item_autofill_unlock)
|
RemoteViews(packageName, R.layout.item_autofill_unlock)
|
||||||
}
|
}
|
||||||
responseBuilder.setAuthentication(autofillIds, sender, remoteViewsUnlock)
|
|
||||||
|
// Tell to service the interest to save credentials
|
||||||
|
if (askToSaveData) {
|
||||||
|
var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC
|
||||||
|
val info = ArrayList<AutofillId>()
|
||||||
|
// Only if at least a password
|
||||||
|
parseResult.passwordId?.let { passwordInfo ->
|
||||||
|
parseResult.usernameId?.let { usernameInfo ->
|
||||||
|
types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME
|
||||||
|
info.add(usernameInfo)
|
||||||
|
}
|
||||||
|
types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD
|
||||||
|
info.add(passwordInfo)
|
||||||
|
}
|
||||||
|
if (info.isNotEmpty()) {
|
||||||
|
responseBuilder.setSaveInfo(
|
||||||
|
SaveInfo.Builder(types, info.toTypedArray()).build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Build response
|
||||||
|
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
|
||||||
callback.onSuccess(responseBuilder.build())
|
callback.onSuccess(responseBuilder.build())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
|
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
|
||||||
// TODO Save autofill
|
if (askToSaveData) {
|
||||||
//callback.onFailure(getString(R.string.autofill_not_support_save));
|
val latestStructure = request.fillContexts.last().structure
|
||||||
|
StructureParser(latestStructure).parse(true)?.let { parseResult ->
|
||||||
|
|
||||||
|
if (autofillAllowedFor(parseResult.applicationId, applicationIdBlocklist)
|
||||||
|
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
|
||||||
|
Log.d(TAG, "autofill onSaveRequest password")
|
||||||
|
|
||||||
|
// Show UI to save data
|
||||||
|
val registerInfo = RegisterInfo(SearchInfo().apply {
|
||||||
|
applicationId = parseResult.applicationId
|
||||||
|
webDomain = parseResult.webDomain
|
||||||
|
webScheme = parseResult.webScheme
|
||||||
|
},
|
||||||
|
parseResult.usernameValue?.textValue?.toString(),
|
||||||
|
parseResult.passwordValue?.textValue?.toString())
|
||||||
|
// TODO Callback in each activity #765
|
||||||
|
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,
|
||||||
|
// registerInfo))
|
||||||
|
//} else {
|
||||||
|
AutofillLauncherActivity.launchForRegistration(this, registerInfo)
|
||||||
|
callback.onSuccess()
|
||||||
|
//}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback.onFailure("Saving form values is not allowed")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConnected() {
|
override fun onConnected() {
|
||||||
@@ -109,10 +193,24 @@ class KeeAutofillService : AutofillService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDisconnected() {
|
override fun onDisconnected() {
|
||||||
|
mLock.set(false)
|
||||||
Log.d(TAG, "onDisconnected")
|
Log.d(TAG, "onDisconnected")
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = KeeAutofillService::class.java.name
|
private val TAG = KeeAutofillService::class.java.name
|
||||||
|
|
||||||
|
fun autofillAllowedFor(element: String?, blockList: Set<String>?): Boolean {
|
||||||
|
element?.let { elementNotNull ->
|
||||||
|
if (blockList?.any { appIdBlocked ->
|
||||||
|
elementNotNull.contains(appIdBlocked)
|
||||||
|
} == true
|
||||||
|
) {
|
||||||
|
Log.d(TAG, "Autofill not allowed for $elementNotNull")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import androidx.annotation.RequiresApi
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.autofill.AutofillId
|
import android.view.autofill.AutofillId
|
||||||
|
import android.view.autofill.AutofillValue
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
@@ -34,13 +35,19 @@ import java.util.*
|
|||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
internal class StructureParser(private val structure: AssistStructure) {
|
internal class StructureParser(private val structure: AssistStructure) {
|
||||||
private var result: Result? = null
|
private var result: Result? = null
|
||||||
private var usernameCandidate: AutofillId? = null
|
|
||||||
|
|
||||||
fun parse(): Result? {
|
private var usernameNeeded = true
|
||||||
|
|
||||||
|
private var usernameCandidate: AutofillId? = null
|
||||||
|
private var usernameValueCandidate: AutofillValue? = null
|
||||||
|
|
||||||
|
fun parse(saveValue: Boolean = false): Result? {
|
||||||
try {
|
try {
|
||||||
result = Result()
|
result = Result()
|
||||||
result?.apply {
|
result?.apply {
|
||||||
|
allowSaveValues = saveValue
|
||||||
usernameCandidate = null
|
usernameCandidate = null
|
||||||
|
usernameValueCandidate = null
|
||||||
mainLoop@ for (i in 0 until structure.windowNodeCount) {
|
mainLoop@ for (i in 0 until structure.windowNodeCount) {
|
||||||
val windowNode = structure.getWindowNodeAt(i)
|
val windowNode = structure.getWindowNodeAt(i)
|
||||||
applicationId = windowNode.title.toString().split("/")[0]
|
applicationId = windowNode.title.toString().split("/")[0]
|
||||||
@@ -50,12 +57,16 @@ internal class StructureParser(private val structure: AssistStructure) {
|
|||||||
break@mainLoop
|
break@mainLoop
|
||||||
}
|
}
|
||||||
// If not explicit username field found, add the field just before password field.
|
// If not explicit username field found, add the field just before password field.
|
||||||
if (usernameId == null && passwordId != null && usernameCandidate != null)
|
if (usernameId == null && passwordId != null && usernameCandidate != null) {
|
||||||
usernameId = usernameCandidate
|
usernameId = usernameCandidate
|
||||||
|
if (allowSaveValues) {
|
||||||
|
usernameValue = usernameValueCandidate
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the result only if password field is retrieved
|
// Return the result only if password field is retrieved
|
||||||
return if (result?.usernameId != null
|
return if ((!usernameNeeded || result?.usernameId != null)
|
||||||
&& result?.passwordId != null)
|
&& result?.passwordId != null)
|
||||||
result
|
result
|
||||||
else
|
else
|
||||||
@@ -67,11 +78,23 @@ internal class StructureParser(private val structure: AssistStructure) {
|
|||||||
|
|
||||||
private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
|
private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
|
||||||
// Get the domain of a web app
|
// Get the domain of a web app
|
||||||
node.webDomain?.let {
|
node.webDomain?.let { webDomain ->
|
||||||
result?.domain = it
|
if (webDomain.isNotEmpty()) {
|
||||||
Log.d(TAG, "Autofill domain: $it")
|
result?.webDomain = webDomain
|
||||||
|
Log.d(TAG, "Autofill domain: $webDomain")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
node.webScheme?.let { webScheme ->
|
||||||
|
if (webScheme.isNotEmpty()) {
|
||||||
|
result?.webScheme = webScheme
|
||||||
|
Log.d(TAG, "Autofill scheme: $webScheme")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val domainNotEmpty = result?.webDomain?.isNotEmpty() == true
|
||||||
|
|
||||||
|
var returnValue = false
|
||||||
// Only parse visible nodes
|
// Only parse visible nodes
|
||||||
if (node.visibility == View.VISIBLE) {
|
if (node.visibility == View.VISIBLE) {
|
||||||
if (node.autofillId != null
|
if (node.autofillId != null
|
||||||
@@ -80,36 +103,44 @@ internal class StructureParser(private val structure: AssistStructure) {
|
|||||||
val hints = node.autofillHints
|
val hints = node.autofillHints
|
||||||
if (hints != null && hints.isNotEmpty()) {
|
if (hints != null && hints.isNotEmpty()) {
|
||||||
if (parseNodeByAutofillHint(node))
|
if (parseNodeByAutofillHint(node))
|
||||||
return true
|
returnValue = true
|
||||||
} else if (parseNodeByHtmlAttributes(node))
|
} else if (parseNodeByHtmlAttributes(node))
|
||||||
return true
|
returnValue = true
|
||||||
else if (parseNodeByAndroidInput(node))
|
else if (parseNodeByAndroidInput(node))
|
||||||
return true
|
returnValue = true
|
||||||
}
|
}
|
||||||
|
// Optimized return but only if domain not empty
|
||||||
|
if (domainNotEmpty && returnValue)
|
||||||
|
return true
|
||||||
// Recursive method to process each node
|
// Recursive method to process each node
|
||||||
for (i in 0 until node.childCount) {
|
for (i in 0 until node.childCount) {
|
||||||
if (parseViewNode(node.getChildAt(i)))
|
if (parseViewNode(node.getChildAt(i)))
|
||||||
|
returnValue = true
|
||||||
|
if (domainNotEmpty && returnValue)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return returnValue
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseNodeByAutofillHint(node: AssistStructure.ViewNode): Boolean {
|
private fun parseNodeByAutofillHint(node: AssistStructure.ViewNode): Boolean {
|
||||||
val autofillId = node.autofillId
|
val autofillId = node.autofillId
|
||||||
node.autofillHints?.forEach {
|
node.autofillHints?.forEach {
|
||||||
when {
|
when {
|
||||||
it.equals(View.AUTOFILL_HINT_USERNAME, true)
|
it.contains(View.AUTOFILL_HINT_USERNAME, true)
|
||||||
|| it.equals(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|
|| it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|
||||||
|| it.equals(View.AUTOFILL_HINT_PHONE, true)
|
|| it.contains("email", true)
|
||||||
|| it.equals("usernameOrEmail", true)-> {
|
|| it.contains(View.AUTOFILL_HINT_PHONE, true)-> {
|
||||||
result?.usernameId = autofillId
|
result?.usernameId = autofillId
|
||||||
|
result?.usernameValue = node.autofillValue
|
||||||
Log.d(TAG, "Autofill username hint")
|
Log.d(TAG, "Autofill username hint")
|
||||||
}
|
}
|
||||||
it.equals(View.AUTOFILL_HINT_PASSWORD, true)
|
it.contains(View.AUTOFILL_HINT_PASSWORD, true) -> {
|
||||||
|| it.contains("password", true) -> {
|
|
||||||
result?.passwordId = autofillId
|
result?.passwordId = autofillId
|
||||||
|
result?.passwordValue = node.autofillValue
|
||||||
Log.d(TAG, "Autofill password hint")
|
Log.d(TAG, "Autofill password hint")
|
||||||
|
// Username not needed in this case
|
||||||
|
usernameNeeded = false
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// Ignore autocomplete="off"
|
// Ignore autocomplete="off"
|
||||||
@@ -136,14 +167,17 @@ internal class StructureParser(private val structure: AssistStructure) {
|
|||||||
when (pairAttribute.second.toLowerCase(Locale.ENGLISH)) {
|
when (pairAttribute.second.toLowerCase(Locale.ENGLISH)) {
|
||||||
"tel", "email" -> {
|
"tel", "email" -> {
|
||||||
result?.usernameId = autofillId
|
result?.usernameId = autofillId
|
||||||
|
result?.usernameValue = node.autofillValue
|
||||||
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||||
}
|
}
|
||||||
"text" -> {
|
"text" -> {
|
||||||
usernameCandidate = autofillId
|
usernameCandidate = autofillId
|
||||||
|
usernameValueCandidate = node.autofillValue
|
||||||
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||||
}
|
}
|
||||||
"password" -> {
|
"password" -> {
|
||||||
result?.passwordId = autofillId
|
result?.passwordId = autofillId
|
||||||
|
result?.passwordValue = node.autofillValue
|
||||||
Log.d(TAG, "Autofill password web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
Log.d(TAG, "Autofill password web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -156,42 +190,83 @@ internal class StructureParser(private val structure: AssistStructure) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun inputIsVariationType(inputType: Int, vararg type: Int): Boolean {
|
||||||
|
type.forEach {
|
||||||
|
if (inputType and InputType.TYPE_MASK_VARIATION == it)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showHexInputType(inputType: Int): String {
|
||||||
|
return "0x${"%08x".format(inputType)}"
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseNodeByAndroidInput(node: AssistStructure.ViewNode): Boolean {
|
private fun parseNodeByAndroidInput(node: AssistStructure.ViewNode): Boolean {
|
||||||
val autofillId = node.autofillId
|
val autofillId = node.autofillId
|
||||||
val inputType = node.inputType
|
val inputType = node.inputType
|
||||||
if (inputType and InputType.TYPE_CLASS_TEXT != 0) {
|
when (inputType and InputType.TYPE_MASK_CLASS) {
|
||||||
when {
|
InputType.TYPE_CLASS_TEXT -> {
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS != 0 -> {
|
when {
|
||||||
result?.usernameId = autofillId
|
inputIsVariationType(inputType,
|
||||||
Log.d(TAG, "Autofill username android type: $inputType")
|
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS) -> {
|
||||||
|
result?.usernameId = autofillId
|
||||||
|
result?.usernameValue = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill username android text type: ${showHexInputType(inputType)}")
|
||||||
|
}
|
||||||
|
inputIsVariationType(inputType,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_NORMAL,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> {
|
||||||
|
usernameCandidate = autofillId
|
||||||
|
usernameValueCandidate = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
|
||||||
|
}
|
||||||
|
inputIsVariationType(inputType,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_PASSWORD,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) -> {
|
||||||
|
result?.passwordId = autofillId
|
||||||
|
result?.passwordValue = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill password android text type: ${showHexInputType(inputType)}")
|
||||||
|
usernameNeeded = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
inputIsVariationType(inputType,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_FILTER,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_PHONETIC,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_URI) -> {
|
||||||
|
// Type not used
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.d(TAG, "Autofill unknown android text type: ${showHexInputType(inputType)}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_NORMAL != 0 ||
|
}
|
||||||
inputType and InputType.TYPE_NUMBER_VARIATION_NORMAL != 0 ||
|
InputType.TYPE_CLASS_NUMBER -> {
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_PERSON_NAME != 0 -> {
|
when {
|
||||||
usernameCandidate = autofillId
|
inputIsVariationType(inputType,
|
||||||
Log.d(TAG, "Autofill username candidate android type: $inputType")
|
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
|
||||||
}
|
usernameCandidate = autofillId
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_PASSWORD != 0 ||
|
usernameValueCandidate = node.autofillValue
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD != 0 ||
|
Log.d(TAG, "Autofill usernale candidate android number type: ${showHexInputType(inputType)}")
|
||||||
inputType and InputType.TYPE_NUMBER_VARIATION_PASSWORD != 0 -> {
|
}
|
||||||
result?.passwordId = autofillId
|
inputIsVariationType(inputType,
|
||||||
Log.d(TAG, "Autofill password android type: $inputType")
|
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {
|
||||||
return true
|
result?.passwordId = autofillId
|
||||||
}
|
result?.passwordValue = node.autofillValue
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT != 0 ||
|
Log.d(TAG, "Autofill password android number type: ${showHexInputType(inputType)}")
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_FILTER != 0 ||
|
usernameNeeded = false
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE != 0 ||
|
return true
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_PHONETIC != 0 ||
|
}
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS != 0 ||
|
else -> {
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_URI != 0 ||
|
Log.d(TAG, "Autofill unknown android number type: ${showHexInputType(inputType)}")
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT != 0 ||
|
}
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS != 0 ||
|
|
||||||
inputType and InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD != 0 -> {
|
|
||||||
// Type not used
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Log.d(TAG, "Autofill unknown android type: $inputType")
|
|
||||||
usernameCandidate = autofillId
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,7 +276,14 @@ internal class StructureParser(private val structure: AssistStructure) {
|
|||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
internal class Result {
|
internal class Result {
|
||||||
var applicationId: String? = null
|
var applicationId: String? = null
|
||||||
var domain: String? = null
|
|
||||||
|
var webDomain: String? = null
|
||||||
|
set(value) {
|
||||||
|
if (field == null)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var webScheme: String? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
if (field == null)
|
if (field == null)
|
||||||
field = value
|
field = value
|
||||||
@@ -229,6 +311,21 @@ internal class StructureParser(private val structure: AssistStructure) {
|
|||||||
}
|
}
|
||||||
return all.toTypedArray()
|
return all.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only in registration mode
|
||||||
|
var allowSaveValues = false
|
||||||
|
|
||||||
|
var usernameValue: AutofillValue? = null
|
||||||
|
set(value) {
|
||||||
|
if (allowSaveValues && field == null)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var passwordValue: AutofillValue? = null
|
||||||
|
set(value) {
|
||||||
|
if (allowSaveValues && field == null)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -19,15 +19,11 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.backup
|
package com.kunzisoft.keepass.backup
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.backup.BackupAgentHelper
|
import android.app.backup.BackupAgentHelper
|
||||||
import android.app.backup.SharedPreferencesBackupHelper
|
import android.app.backup.SharedPreferencesBackupHelper
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
class SettingsBackupAgent : BackupAgentHelper() {
|
class SettingsBackupAgent : BackupAgentHelper() {
|
||||||
|
|
||||||
//TODO Backup
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
val defaultPrefs = this.packageName + "_preferences"
|
val defaultPrefs = this.packageName + "_preferences"
|
||||||
val prefHelper = SharedPreferencesBackupHelper(this, defaultPrefs)
|
val prefHelper = SharedPreferencesBackupHelper(this, defaultPrefs)
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ import android.provider.Settings
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
|
import android.view.View
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.biometric.BiometricConstants
|
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.biometric.BiometricPrompt
|
import androidx.biometric.BiometricPrompt
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
@@ -50,9 +50,24 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
: BiometricUnlockDatabaseHelper.BiometricUnlockCallback {
|
: BiometricUnlockDatabaseHelper.BiometricUnlockCallback {
|
||||||
|
|
||||||
private var biometricUnlockDatabaseHelper: BiometricUnlockDatabaseHelper? = null
|
private var biometricUnlockDatabaseHelper: BiometricUnlockDatabaseHelper? = null
|
||||||
private var biometricMode: Mode = Mode.UNAVAILABLE
|
private var biometricMode: Mode = Mode.BIOMETRIC_UNAVAILABLE
|
||||||
|
|
||||||
private var isBiometricPromptAutoOpenEnable = PreferencesUtil.isBiometricPromptAutoOpenEnable(context)
|
// 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 = false
|
||||||
|
get() {
|
||||||
|
return field && biometricPromptAutoOpenPreference
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variable to check if the prompt can be open (if the right activity is currently shown)
|
||||||
|
// checkBiometricAvailability() allows open biometric prompt and onDestroy() removes the authorization
|
||||||
|
private var allowOpenBiometricPrompt = false
|
||||||
|
|
||||||
private var cipherDatabaseAction = CipherDatabaseAction.getInstance(context.applicationContext)
|
private var cipherDatabaseAction = CipherDatabaseAction.getInstance(context.applicationContext)
|
||||||
|
|
||||||
@@ -72,12 +87,15 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
|
|
||||||
// biometric not supported (by API level or hardware) so keep option hidden
|
// biometric not supported (by API level or hardware) so keep option hidden
|
||||||
// or manually disable
|
// or manually disable
|
||||||
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate()
|
val biometricCanAuthenticate = BiometricUnlockDatabaseHelper.canAuthenticate(context)
|
||||||
|
allowOpenBiometricPrompt = true
|
||||||
|
|
||||||
if (!PreferencesUtil.isBiometricUnlockEnable(context)
|
if (!PreferencesUtil.isBiometricUnlockEnable(context)
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
|
||||||
toggleMode(Mode.UNAVAILABLE)
|
toggleMode(Mode.BIOMETRIC_UNAVAILABLE)
|
||||||
|
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED){
|
||||||
|
toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
|
||||||
} else {
|
} else {
|
||||||
// biometric is available but not configured, show icon but in disabled state with some information
|
// biometric is available but not configured, show icon but in disabled state with some information
|
||||||
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
|
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
|
||||||
@@ -101,7 +119,7 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
} else {
|
} else {
|
||||||
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipher ->
|
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipher ->
|
||||||
// biometric available but no stored password found yet for this DB so show info don't listen
|
// biometric available but no stored password found yet for this DB so show info don't listen
|
||||||
toggleMode( if (containsCipher) {
|
toggleMode(if (containsCipher) {
|
||||||
// listen for decryption
|
// listen for decryption
|
||||||
Mode.EXTRACT_CREDENTIAL
|
Mode.EXTRACT_CREDENTIAL
|
||||||
} else {
|
} else {
|
||||||
@@ -143,10 +161,16 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||||
context.runOnUiThread {
|
context.runOnUiThread {
|
||||||
when (biometricMode) {
|
when (biometricMode) {
|
||||||
Mode.UNAVAILABLE -> {}
|
Mode.BIOMETRIC_UNAVAILABLE -> {
|
||||||
Mode.BIOMETRIC_NOT_CONFIGURED -> {}
|
}
|
||||||
Mode.KEY_MANAGER_UNAVAILABLE -> {}
|
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> {
|
||||||
Mode.WAIT_CREDENTIAL -> {}
|
}
|
||||||
|
Mode.BIOMETRIC_NOT_CONFIGURED -> {
|
||||||
|
}
|
||||||
|
Mode.KEY_MANAGER_UNAVAILABLE -> {
|
||||||
|
}
|
||||||
|
Mode.WAIT_CREDENTIAL -> {
|
||||||
|
}
|
||||||
Mode.STORE_CREDENTIAL -> {
|
Mode.STORE_CREDENTIAL -> {
|
||||||
// newly store the entered password in encrypted way
|
// newly store the entered password in encrypted way
|
||||||
biometricUnlockDatabaseHelper?.encryptData(passwordView?.text.toString())
|
biometricUnlockDatabaseHelper?.encryptData(passwordView?.text.toString())
|
||||||
@@ -170,6 +194,15 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
advancedUnlockInfoView?.setIconViewClickListener(false, null)
|
advancedUnlockInfoView?.setIconViewClickListener(false, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun initSecurityUpdateRequired() {
|
||||||
|
showFingerPrintViews(true)
|
||||||
|
setAdvancedUnlockedTitleView(R.string.biometric_security_update_required)
|
||||||
|
|
||||||
|
advancedUnlockInfoView?.setIconViewClickListener(false) {
|
||||||
|
context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun initNotConfigured() {
|
private fun initNotConfigured() {
|
||||||
showFingerPrintViews(true)
|
showFingerPrintViews(true)
|
||||||
setAdvancedUnlockedTitleView(R.string.configure_biometric)
|
setAdvancedUnlockedTitleView(R.string.configure_biometric)
|
||||||
@@ -183,7 +216,6 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
private fun initKeyManagerNotAvailable() {
|
private fun initKeyManagerNotAvailable() {
|
||||||
showFingerPrintViews(true)
|
showFingerPrintViews(true)
|
||||||
setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
|
setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
|
||||||
setAdvancedUnlockedMessageView("")
|
|
||||||
|
|
||||||
advancedUnlockInfoView?.setIconViewClickListener(false) {
|
advancedUnlockInfoView?.setIconViewClickListener(false) {
|
||||||
context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
|
context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
|
||||||
@@ -196,17 +228,26 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
setAdvancedUnlockedMessageView("")
|
setAdvancedUnlockedMessageView("")
|
||||||
|
|
||||||
advancedUnlockInfoView?.setIconViewClickListener(false) {
|
advancedUnlockInfoView?.setIconViewClickListener(false) {
|
||||||
biometricAuthenticationCallback.onAuthenticationError(
|
biometricAuthenticationCallback.onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
|
||||||
BiometricConstants.ERROR_UNABLE_TO_PROCESS
|
context.getString(R.string.credential_before_click_biometric_button))
|
||||||
, context.getString(R.string.credential_before_click_biometric_button))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openBiometricPrompt(biometricPrompt: BiometricPrompt?,
|
private fun openBiometricPrompt(biometricPrompt: BiometricPrompt?,
|
||||||
cryptoObject: BiometricPrompt.CryptoObject,
|
cryptoObject: BiometricPrompt.CryptoObject?,
|
||||||
promptInfo: BiometricPrompt.PromptInfo) {
|
promptInfo: BiometricPrompt.PromptInfo) {
|
||||||
context.runOnUiThread {
|
context.runOnUiThread {
|
||||||
biometricPrompt?.authenticate(promptInfo, cryptoObject)
|
if (allowOpenBiometricPrompt) {
|
||||||
|
if (biometricPrompt != null) {
|
||||||
|
if (cryptoObject != null) {
|
||||||
|
biometricPrompt.authenticate(promptInfo, cryptoObject)
|
||||||
|
} else {
|
||||||
|
setAdvancedUnlockedTitleView(R.string.crypto_object_not_initialized)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setAdvancedUnlockedTitleView(R.string.biometric_prompt_not_initialized)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,14 +257,10 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
setAdvancedUnlockedMessageView("")
|
setAdvancedUnlockedMessageView("")
|
||||||
|
|
||||||
biometricUnlockDatabaseHelper?.initEncryptData { biometricPrompt, cryptoObject, promptInfo ->
|
biometricUnlockDatabaseHelper?.initEncryptData { biometricPrompt, cryptoObject, promptInfo ->
|
||||||
|
// Set listener to open the biometric dialog and save credential
|
||||||
cryptoObject?.let { crypto ->
|
advancedUnlockInfoView?.setIconViewClickListener { _ ->
|
||||||
// Set listener to open the biometric dialog and save credential
|
openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
|
||||||
advancedUnlockInfoView?.setIconViewClickListener { _ ->
|
|
||||||
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,19 +275,16 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
it?.specParameters?.let { specs ->
|
it?.specParameters?.let { specs ->
|
||||||
biometricUnlockDatabaseHelper?.initDecryptData(specs) { biometricPrompt, cryptoObject, promptInfo ->
|
biometricUnlockDatabaseHelper?.initDecryptData(specs) { biometricPrompt, cryptoObject, promptInfo ->
|
||||||
|
|
||||||
cryptoObject?.let { crypto ->
|
// Set listener to open the biometric dialog and check credential
|
||||||
// Set listener to open the biometric dialog and check credential
|
advancedUnlockInfoView?.setIconViewClickListener { _ ->
|
||||||
advancedUnlockInfoView?.setIconViewClickListener { _ ->
|
openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
|
||||||
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto open the biometric prompt
|
|
||||||
if (isBiometricPromptAutoOpenEnable) {
|
|
||||||
isBiometricPromptAutoOpenEnable = false
|
|
||||||
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto open the biometric prompt
|
||||||
|
if (isBiometricPromptAutoOpenEnable) {
|
||||||
|
isBiometricPromptAutoOpenEnable = false
|
||||||
|
openBiometricPrompt(biometricPrompt, cryptoObject, promptInfo)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,48 +293,59 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun initBiometricMode() {
|
fun initBiometricMode() {
|
||||||
|
mAllowAdvancedUnlockMenu = false
|
||||||
when (biometricMode) {
|
when (biometricMode) {
|
||||||
Mode.UNAVAILABLE -> initNotAvailable()
|
Mode.BIOMETRIC_UNAVAILABLE -> initNotAvailable()
|
||||||
|
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> initSecurityUpdateRequired()
|
||||||
Mode.BIOMETRIC_NOT_CONFIGURED -> initNotConfigured()
|
Mode.BIOMETRIC_NOT_CONFIGURED -> initNotConfigured()
|
||||||
Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable()
|
Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable()
|
||||||
Mode.WAIT_CREDENTIAL -> initWaitData()
|
Mode.WAIT_CREDENTIAL -> initWaitData()
|
||||||
Mode.STORE_CREDENTIAL -> initEncryptData()
|
Mode.STORE_CREDENTIAL -> initEncryptData()
|
||||||
Mode.EXTRACT_CREDENTIAL -> initDecryptData()
|
Mode.EXTRACT_CREDENTIAL -> initDecryptData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invalidateBiometricMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun invalidateBiometricMenu() {
|
||||||
// Show fingerprint key deletion
|
// Show fingerprint key deletion
|
||||||
context.invalidateOptionsMenu()
|
if (!mAddBiometricMenuInProgress) {
|
||||||
}
|
mAddBiometricMenuInProgress = true
|
||||||
|
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipher ->
|
||||||
fun destroy() {
|
mAllowAdvancedUnlockMenu = containsCipher
|
||||||
// Restore the checked listener
|
&& (biometricMode != Mode.BIOMETRIC_UNAVAILABLE
|
||||||
checkboxPasswordView?.setOnCheckedChangeListener(onCheckedPasswordChangeListener)
|
&& biometricMode != Mode.KEY_MANAGER_UNAVAILABLE)
|
||||||
}
|
mAddBiometricMenuInProgress = false
|
||||||
|
context.invalidateOptionsMenu()
|
||||||
// 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) {
|
|
||||||
menuInflater.inflate(R.menu.advanced_unlock, menu)
|
|
||||||
addBiometricMenuInProgress = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun destroy() {
|
||||||
|
// Close the biometric prompt
|
||||||
|
allowOpenBiometricPrompt = false
|
||||||
|
biometricUnlockDatabaseHelper?.closeBiometricPrompt()
|
||||||
|
// Restore the checked listener
|
||||||
|
checkboxPasswordView?.setOnCheckedChangeListener(onCheckedPasswordChangeListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun inflateOptionsMenu(menuInflater: MenuInflater, menu: Menu) {
|
||||||
|
if (mAllowAdvancedUnlockMenu)
|
||||||
|
menuInflater.inflate(R.menu.advanced_unlock, menu)
|
||||||
|
}
|
||||||
|
|
||||||
fun deleteEntryKey() {
|
fun deleteEntryKey() {
|
||||||
|
allowOpenBiometricPrompt = false
|
||||||
|
advancedUnlockInfoView?.setIconViewClickListener(false, null)
|
||||||
|
biometricUnlockDatabaseHelper?.closeBiometricPrompt()
|
||||||
biometricUnlockDatabaseHelper?.deleteEntryKey()
|
biometricUnlockDatabaseHelper?.deleteEntryKey()
|
||||||
cipherDatabaseAction.deleteByDatabaseUri(databaseFileUri)
|
cipherDatabaseAction.deleteByDatabaseUri(databaseFileUri) {
|
||||||
biometricMode = Mode.BIOMETRIC_NOT_CONFIGURED
|
checkBiometricAvailability()
|
||||||
checkBiometricAvailability()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {
|
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {
|
||||||
loadDatabaseAfterRegisterCredentials.invoke(encryptedValue, ivSpec)
|
loadDatabaseAfterRegisterCredentials.invoke(encryptedValue, ivSpec)
|
||||||
// TODO setAdvancedUnlockedMessageView(R.string.encrypted_value_stored)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleDecryptedResult(decryptedValue: String) {
|
override fun handleDecryptedResult(decryptedValue: String) {
|
||||||
@@ -319,7 +364,9 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun showFingerPrintViews(show: Boolean) {
|
private fun showFingerPrintViews(show: Boolean) {
|
||||||
context.runOnUiThread { advancedUnlockInfoView?.hide = !show }
|
context.runOnUiThread {
|
||||||
|
advancedUnlockInfoView?.visibility = if (show) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setAdvancedUnlockedTitleView(textId: Int) {
|
private fun setAdvancedUnlockedTitleView(textId: Int) {
|
||||||
@@ -341,7 +388,13 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum class Mode {
|
enum class Mode {
|
||||||
UNAVAILABLE, BIOMETRIC_NOT_CONFIGURED, KEY_MANAGER_UNAVAILABLE, WAIT_CREDENTIAL, STORE_CREDENTIAL, EXTRACT_CREDENTIAL
|
BIOMETRIC_UNAVAILABLE,
|
||||||
|
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
|
||||||
|
BIOMETRIC_NOT_CONFIGURED,
|
||||||
|
KEY_MANAGER_UNAVAILABLE,
|
||||||
|
WAIT_CREDENTIAL,
|
||||||
|
STORE_CREDENTIAL,
|
||||||
|
EXTRACT_CREDENTIAL
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import android.util.Base64
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
|
||||||
|
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
|
||||||
import androidx.biometric.BiometricPrompt
|
import androidx.biometric.BiometricPrompt
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
@@ -60,26 +62,26 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
|||||||
setTitle(context.getString(R.string.biometric_prompt_store_credential_title))
|
setTitle(context.getString(R.string.biometric_prompt_store_credential_title))
|
||||||
setDescription(context.getString(R.string.biometric_prompt_store_credential_message))
|
setDescription(context.getString(R.string.biometric_prompt_store_credential_message))
|
||||||
setConfirmationRequired(true)
|
setConfirmationRequired(true)
|
||||||
// TODO device credential
|
// TODO device credential #102 #152
|
||||||
/*
|
/*
|
||||||
if (keyguardManager?.isDeviceSecure == true)
|
if (keyguardManager?.isDeviceSecure == true)
|
||||||
setDeviceCredentialAllowed(true)
|
setDeviceCredentialAllowed(true)
|
||||||
else
|
else
|
||||||
*/
|
*/
|
||||||
setNegativeButtonText(context.getString(android.R.string.cancel))
|
setNegativeButtonText(context.getString(android.R.string.cancel))
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
private val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply {
|
private val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply {
|
||||||
setTitle(context.getString(R.string.biometric_prompt_extract_credential_title))
|
setTitle(context.getString(R.string.biometric_prompt_extract_credential_title))
|
||||||
//setDescription(context.getString(R.string.biometric_prompt_extract_credential_message))
|
//setDescription(context.getString(R.string.biometric_prompt_extract_credential_message))
|
||||||
setConfirmationRequired(false)
|
setConfirmationRequired(false)
|
||||||
// TODO device credential
|
// TODO device credential #102 #152
|
||||||
/*
|
/*
|
||||||
if (keyguardManager?.isDeviceSecure == true)
|
if (keyguardManager?.isDeviceSecure == true)
|
||||||
setDeviceCredentialAllowed(true)
|
setDeviceCredentialAllowed(true)
|
||||||
else
|
else
|
||||||
*/
|
*/
|
||||||
setNegativeButtonText(context.getString(android.R.string.cancel))
|
setNegativeButtonText(context.getString(android.R.string.cancel))
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
val isKeyManagerInitialized: Boolean
|
val isKeyManagerInitialized: Boolean
|
||||||
@@ -91,12 +93,8 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (BiometricManager.from(context).canAuthenticate() != BiometricManager.BIOMETRIC_SUCCESS) {
|
if (allowInitKeyStore(context)) {
|
||||||
// really not much to do when no fingerprint support found
|
|
||||||
isKeyManagerInit = false
|
|
||||||
} else {
|
|
||||||
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager?
|
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager?
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.keyStore = KeyStore.getInstance(BIOMETRIC_KEYSTORE)
|
this.keyStore = KeyStore.getInstance(BIOMETRIC_KEYSTORE)
|
||||||
this.keyGenerator = KeyGenerator.getInstance(BIOMETRIC_KEY_ALGORITHM, BIOMETRIC_KEYSTORE)
|
this.keyGenerator = KeyGenerator.getInstance(BIOMETRIC_KEY_ALGORITHM, BIOMETRIC_KEYSTORE)
|
||||||
@@ -113,6 +111,9 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
|||||||
isKeyManagerInit = false
|
isKeyManagerInit = false
|
||||||
biometricUnlockCallback?.onBiometricException(e)
|
biometricUnlockCallback?.onBiometricException(e)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// really not much to do when no fingerprint support found
|
||||||
|
isKeyManagerInit = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +159,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
|||||||
fun initEncryptData(actionIfCypherInit
|
fun initEncryptData(actionIfCypherInit
|
||||||
: (biometricPrompt: BiometricPrompt?,
|
: (biometricPrompt: BiometricPrompt?,
|
||||||
cryptoObject: BiometricPrompt.CryptoObject?,
|
cryptoObject: BiometricPrompt.CryptoObject?,
|
||||||
promptInfo: BiometricPrompt.PromptInfo)->Unit) {
|
promptInfo: BiometricPrompt.PromptInfo) -> Unit) {
|
||||||
if (!isKeyManagerInitialized) {
|
if (!isKeyManagerInitialized) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -203,9 +204,9 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun initDecryptData(ivSpecValue: String, actionIfCypherInit
|
fun initDecryptData(ivSpecValue: String, actionIfCypherInit
|
||||||
: (biometricPrompt: BiometricPrompt?,
|
: (biometricPrompt: BiometricPrompt?,
|
||||||
cryptoObject: BiometricPrompt.CryptoObject?,
|
cryptoObject: BiometricPrompt.CryptoObject?,
|
||||||
promptInfo: BiometricPrompt.PromptInfo)->Unit) {
|
promptInfo: BiometricPrompt.PromptInfo) -> Unit) {
|
||||||
if (!isKeyManagerInitialized) {
|
if (!isKeyManagerInitialized) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -271,6 +272,10 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun closeBiometricPrompt() {
|
||||||
|
biometricPrompt?.cancelAuthentication()
|
||||||
|
}
|
||||||
|
|
||||||
interface BiometricUnlockErrorCallback {
|
interface BiometricUnlockErrorCallback {
|
||||||
fun onInvalidKeyException(e: Exception)
|
fun onInvalidKeyException(e: Exception)
|
||||||
fun onBiometricException(e: Exception)
|
fun onBiometricException(e: Exception)
|
||||||
@@ -291,6 +296,37 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
|||||||
private const val BIOMETRIC_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
|
private const val BIOMETRIC_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
|
||||||
private const val BIOMETRIC_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
private const val BIOMETRIC_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
||||||
|
|
||||||
|
fun canAuthenticate(context: Context): Int {
|
||||||
|
return try {
|
||||||
|
BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
|
||||||
|
try {
|
||||||
|
BiometricManager.from(context).canAuthenticate(BIOMETRIC_WEAK)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
|
||||||
|
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun allowInitKeyStore(context: Context): Boolean {
|
||||||
|
val biometricCanAuthenticate = canAuthenticate(context)
|
||||||
|
return ( biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unlockSupported(context: Context): Boolean {
|
||||||
|
val biometricCanAuthenticate = canAuthenticate(context)
|
||||||
|
return ( biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove entry key in keystore
|
* Remove entry key in keystore
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import com.kunzisoft.keepass.crypto.engine.AesEngine
|
|||||||
import com.kunzisoft.keepass.crypto.engine.ChaCha20Engine
|
import com.kunzisoft.keepass.crypto.engine.ChaCha20Engine
|
||||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
||||||
import com.kunzisoft.keepass.crypto.engine.TwofishEngine
|
import com.kunzisoft.keepass.crypto.engine.TwofishEngine
|
||||||
import org.spongycastle.jce.provider.BouncyCastleProvider
|
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
import java.security.Security
|
import java.security.Security
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -37,20 +37,10 @@ object CipherFactory {
|
|||||||
private var blacklisted: Boolean = false
|
private var blacklisted: Boolean = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||||
Security.addProvider(BouncyCastleProvider())
|
Security.addProvider(BouncyCastleProvider())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class)
|
|
||||||
@JvmOverloads
|
|
||||||
fun getInstance(transformation: String, androidOverride: Boolean = false): Cipher {
|
|
||||||
// Return the native AES if it is possible
|
|
||||||
return if (!deviceBlacklisted() && !androidOverride && hasNativeImplementation(transformation) && NativeLib.loaded()) {
|
|
||||||
Cipher.getInstance(transformation, AESProvider())
|
|
||||||
} else {
|
|
||||||
Cipher.getInstance(transformation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deviceBlacklisted(): Boolean {
|
fun deviceBlacklisted(): Boolean {
|
||||||
if (!blacklistInit) {
|
if (!blacklistInit) {
|
||||||
blacklistInit = true
|
blacklistInit = true
|
||||||
@@ -64,6 +54,16 @@ object CipherFactory {
|
|||||||
return transformation == "AES/CBC/PKCS5Padding"
|
return transformation == "AES/CBC/PKCS5Padding"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class)
|
||||||
|
fun getInstance(transformation: String, androidOverride: Boolean = false): Cipher {
|
||||||
|
// Return the native AES if it is possible
|
||||||
|
return if (!deviceBlacklisted() && !androidOverride && hasNativeImplementation(transformation) && NativeLib.loaded()) {
|
||||||
|
Cipher.getInstance(transformation, AESProvider())
|
||||||
|
} else {
|
||||||
|
Cipher.getInstance(transformation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate appropriate cipher based on KeePass 2.x UUID's
|
* Generate appropriate cipher based on KeePass 2.x UUID's
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -19,16 +19,18 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto
|
package com.kunzisoft.keepass.crypto
|
||||||
|
|
||||||
enum class CrsAlgorithm constructor(val id: Int) {
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
|
|
||||||
Null(0),
|
enum class CrsAlgorithm constructor(val id: UnsignedInt) {
|
||||||
ArcFourVariant(1),
|
|
||||||
Salsa20(2),
|
Null(UnsignedInt(0)),
|
||||||
ChaCha20(3);
|
ArcFourVariant(UnsignedInt(1)),
|
||||||
|
Salsa20(UnsignedInt(2)),
|
||||||
|
ChaCha20(UnsignedInt(3));
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun fromId(num: Int): CrsAlgorithm? {
|
fun fromId(num: UnsignedInt): CrsAlgorithm? {
|
||||||
for (e in values()) {
|
for (e in values()) {
|
||||||
if (e.id == num) {
|
if (e.id == num) {
|
||||||
return e
|
return e
|
||||||
|
|||||||
@@ -74,12 +74,10 @@ object CryptoUtil {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmOverloads
|
|
||||||
fun hashSha256(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
|
fun hashSha256(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
|
||||||
return hashGen("SHA-256", data, offset, count)
|
return hashGen("SHA-256", data, offset, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmOverloads
|
|
||||||
fun hashSha512(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
|
fun hashSha512(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
|
||||||
return hashGen("SHA-512", data, offset, count)
|
return hashGen("SHA-512", data, offset, count)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,11 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto
|
package com.kunzisoft.keepass.crypto
|
||||||
|
|
||||||
import org.spongycastle.crypto.StreamCipher
|
import org.bouncycastle.crypto.StreamCipher
|
||||||
import org.spongycastle.crypto.engines.ChaCha7539Engine
|
import org.bouncycastle.crypto.engines.ChaCha7539Engine
|
||||||
import org.spongycastle.crypto.engines.Salsa20Engine
|
import org.bouncycastle.crypto.engines.Salsa20Engine
|
||||||
import org.spongycastle.crypto.params.KeyParameter
|
import org.bouncycastle.crypto.params.KeyParameter
|
||||||
import org.spongycastle.crypto.params.ParametersWithIV
|
import org.bouncycastle.crypto.params.ParametersWithIV
|
||||||
|
|
||||||
object StreamCipherFactory {
|
object StreamCipherFactory {
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.crypto.engine
|
|||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
||||||
import org.spongycastle.jce.provider.BouncyCastleProvider
|
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||||
import java.security.InvalidAlgorithmParameterException
|
import java.security.InvalidAlgorithmParameterException
|
||||||
import java.security.InvalidKeyException
|
import java.security.InvalidKeyException
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
|
|||||||
@@ -17,18 +17,20 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto.finalkey;
|
package com.kunzisoft.keepass.crypto.finalkey
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory;
|
import com.kunzisoft.keepass.crypto.CipherFactory.deviceBlacklisted
|
||||||
|
|
||||||
public class FinalKeyFactory {
|
object AESKeyTransformerFactory : KeyTransformer() {
|
||||||
public static FinalKey createFinalKey() {
|
override fun transformMasterKey(seed: ByteArray?, key: ByteArray?, rounds: Long?): ByteArray? {
|
||||||
// Prefer the native final key implementation
|
// Prefer the native final key implementation
|
||||||
if ( !CipherFactory.INSTANCE.deviceBlacklisted() && NativeFinalKey.available() ) {
|
val keyTransformer = if (!deviceBlacklisted()
|
||||||
return new NativeFinalKey();
|
&& NativeAESKeyTransformer.available()) {
|
||||||
|
NativeAESKeyTransformer()
|
||||||
} else {
|
} else {
|
||||||
// Fall back on the android crypto implementation
|
// Fall back on the android crypto implementation
|
||||||
return new AndroidFinalKey();
|
AndroidAESKeyTransformer()
|
||||||
}
|
}
|
||||||
|
return keyTransformer.transformMasterKey(seed, key, rounds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* 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.crypto.finalkey
|
||||||
|
|
||||||
|
import java.io.IOException
|
||||||
|
import java.security.InvalidKeyException
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.ShortBufferException
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
class AndroidAESKeyTransformer : KeyTransformer() {
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun transformMasterKey(seed: ByteArray?, key: ByteArray?, rounds: Long?): ByteArray? {
|
||||||
|
val cipher: Cipher = try {
|
||||||
|
Cipher.getInstance("AES/ECB/NoPadding")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IOException("Unable to get the cipher", e)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(seed, "AES"))
|
||||||
|
} catch (e: InvalidKeyException) {
|
||||||
|
throw IOException("Unable to init the cipher", e)
|
||||||
|
}
|
||||||
|
if (key == null) {
|
||||||
|
throw IOException("Invalid key")
|
||||||
|
}
|
||||||
|
if (rounds == null) {
|
||||||
|
throw IOException("Invalid rounds")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt key rounds times
|
||||||
|
val keyLength = key.size
|
||||||
|
val newKey = ByteArray(keyLength)
|
||||||
|
System.arraycopy(key, 0, newKey, 0, keyLength)
|
||||||
|
val destKey = ByteArray(keyLength)
|
||||||
|
for (i in 0 until rounds) {
|
||||||
|
try {
|
||||||
|
cipher.update(newKey, 0, newKey.size, destKey, 0)
|
||||||
|
System.arraycopy(destKey, 0, newKey, 0, newKey.size)
|
||||||
|
} catch (e: ShortBufferException) {
|
||||||
|
throw IOException("Short buffer", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash the key
|
||||||
|
val messageDigest: MessageDigest = try {
|
||||||
|
MessageDigest.getInstance("SHA-256")
|
||||||
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
|
throw IOException("SHA-256 not implemented here: " + e.message)
|
||||||
|
}
|
||||||
|
messageDigest.update(newKey)
|
||||||
|
return messageDigest.digest()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Brian Pellin, 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.crypto.finalkey;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
|
||||||
import javax.crypto.NoSuchPaddingException;
|
|
||||||
import javax.crypto.ShortBufferException;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
|
|
||||||
public class AndroidFinalKey extends FinalKey {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public byte[] transformMasterKey(byte[] pKeySeed, byte[] pKey, long rounds) throws IOException {
|
|
||||||
Cipher cipher;
|
|
||||||
try {
|
|
||||||
cipher = Cipher.getInstance("AES/ECB/NoPadding");
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
throw new IOException("NoSuchAlgorithm: " + e.getMessage());
|
|
||||||
} catch (NoSuchPaddingException e) {
|
|
||||||
throw new IOException("NoSuchPadding: " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(pKeySeed, "AES"));
|
|
||||||
} catch (InvalidKeyException e) {
|
|
||||||
throw new IOException("InvalidPasswordException: " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encrypt key rounds times
|
|
||||||
byte[] newKey = new byte[pKey.length];
|
|
||||||
System.arraycopy(pKey, 0, newKey, 0, pKey.length);
|
|
||||||
byte[] destKey = new byte[pKey.length];
|
|
||||||
for (int i = 0; i < rounds; i++) {
|
|
||||||
try {
|
|
||||||
cipher.update(newKey, 0, newKey.length, destKey, 0);
|
|
||||||
System.arraycopy(destKey, 0, newKey, 0, newKey.length);
|
|
||||||
|
|
||||||
} catch (ShortBufferException e) {
|
|
||||||
throw new IOException("Short buffer: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash the key
|
|
||||||
MessageDigest md = null;
|
|
||||||
try {
|
|
||||||
md = MessageDigest.getInstance("SHA-256");
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
assert true;
|
|
||||||
throw new IOException("SHA-256 not implemented here: " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
md.update(newKey);
|
|
||||||
return md.digest();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePassDX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
@@ -17,10 +17,11 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto.finalkey;
|
package com.kunzisoft.keepass.crypto.finalkey
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException
|
||||||
|
|
||||||
public abstract class FinalKey {
|
abstract class KeyTransformer {
|
||||||
public abstract byte[] transformMasterKey(byte[] seed, byte[] key, long rounds) throws IOException;
|
@Throws(IOException::class)
|
||||||
}
|
abstract fun transformMasterKey(seed: ByteArray?, key: ByteArray?, rounds: Long?): ByteArray?
|
||||||
|
}
|
||||||
@@ -21,17 +21,20 @@ package com.kunzisoft.keepass.crypto.finalkey;
|
|||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.NativeLib;
|
import com.kunzisoft.keepass.crypto.NativeLib;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
|
||||||
public class NativeFinalKey extends FinalKey {
|
public class NativeAESKeyTransformer extends KeyTransformer {
|
||||||
|
|
||||||
public static boolean available() {
|
public static boolean available() {
|
||||||
return NativeLib.INSTANCE.init();
|
return NativeLib.INSTANCE.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public byte[] transformMasterKey(byte[] seed, byte[] key, long rounds) throws IOException {
|
public byte[] transformMasterKey(@Nullable byte[] seed, @Nullable byte[] key, @Nullable Long rounds) throws IOException {
|
||||||
NativeLib.INSTANCE.init();
|
NativeLib.INSTANCE.init();
|
||||||
|
|
||||||
return nTransformMasterKey(seed, key, rounds);
|
return nTransformMasterKey(seed, key, rounds);
|
||||||
@@ -22,72 +22,69 @@ package com.kunzisoft.keepass.crypto.keyDerivation
|
|||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.crypto.CryptoUtil
|
import com.kunzisoft.keepass.crypto.CryptoUtil
|
||||||
import com.kunzisoft.keepass.crypto.finalkey.FinalKeyFactory
|
import com.kunzisoft.keepass.crypto.finalkey.AESKeyTransformerFactory
|
||||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class AesKdf internal constructor() : KdfEngine() {
|
class AesKdf : KdfEngine() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
uuid = CIPHER_UUID
|
||||||
|
}
|
||||||
|
|
||||||
override val defaultParameters: KdfParameters
|
override val defaultParameters: KdfParameters
|
||||||
get() {
|
get() {
|
||||||
return KdfParameters(uuid!!).apply {
|
return KdfParameters(uuid!!).apply {
|
||||||
setParamUUID()
|
setParamUUID()
|
||||||
setUInt32(PARAM_ROUNDS, DEFAULT_ROUNDS.toLong())
|
setUInt64(PARAM_ROUNDS, defaultKeyRounds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override val defaultKeyRounds: Long
|
override val defaultKeyRounds: Long = 6000L
|
||||||
get() = DEFAULT_ROUNDS.toLong()
|
|
||||||
|
|
||||||
init {
|
|
||||||
uuid = CIPHER_UUID
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getName(resources: Resources): String {
|
override fun getName(resources: Resources): String {
|
||||||
return resources.getString(R.string.kdf_AES)
|
return resources.getString(R.string.kdf_AES)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray {
|
override fun transform(masterKey: ByteArray, kdfParameters: KdfParameters): ByteArray {
|
||||||
var currentMasterKey = masterKey
|
|
||||||
val rounds = p.getUInt64(PARAM_ROUNDS)
|
|
||||||
var seed = p.getByteArray(PARAM_SEED)
|
|
||||||
|
|
||||||
|
var seed = kdfParameters.getByteArray(PARAM_SEED)
|
||||||
|
if (seed != null && seed.size != 32) {
|
||||||
|
seed = CryptoUtil.hashSha256(seed)
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentMasterKey = masterKey
|
||||||
if (currentMasterKey.size != 32) {
|
if (currentMasterKey.size != 32) {
|
||||||
currentMasterKey = CryptoUtil.hashSha256(currentMasterKey)
|
currentMasterKey = CryptoUtil.hashSha256(currentMasterKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (seed.size != 32) {
|
val rounds = kdfParameters.getUInt64(PARAM_ROUNDS)
|
||||||
seed = CryptoUtil.hashSha256(seed)
|
|
||||||
}
|
|
||||||
|
|
||||||
val key = FinalKeyFactory.createFinalKey()
|
return AESKeyTransformerFactory.transformMasterKey(seed, currentMasterKey, rounds) ?: ByteArray(0)
|
||||||
return key.transformMasterKey(seed, currentMasterKey, rounds)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun randomize(p: KdfParameters) {
|
override fun randomize(kdfParameters: KdfParameters) {
|
||||||
val random = SecureRandom()
|
val random = SecureRandom()
|
||||||
|
|
||||||
val seed = ByteArray(32)
|
val seed = ByteArray(32)
|
||||||
random.nextBytes(seed)
|
random.nextBytes(seed)
|
||||||
|
|
||||||
p.setByteArray(PARAM_SEED, seed)
|
kdfParameters.setByteArray(PARAM_SEED, seed)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getKeyRounds(p: KdfParameters): Long {
|
override fun getKeyRounds(kdfParameters: KdfParameters): Long {
|
||||||
return p.getUInt64(PARAM_ROUNDS)
|
return kdfParameters.getUInt64(PARAM_ROUNDS) ?: defaultKeyRounds
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setKeyRounds(p: KdfParameters, keyRounds: Long) {
|
override fun setKeyRounds(kdfParameters: KdfParameters, keyRounds: Long) {
|
||||||
p.setUInt64(PARAM_ROUNDS, keyRounds)
|
kdfParameters.setUInt64(PARAM_ROUNDS, keyRounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val DEFAULT_ROUNDS = 6000
|
|
||||||
|
|
||||||
val CIPHER_UUID: UUID = bytes16ToUuid(
|
val CIPHER_UUID: UUID = bytes16ToUuid(
|
||||||
byteArrayOf(0xC9.toByte(),
|
byteArrayOf(0xC9.toByte(),
|
||||||
0xD9.toByte(),
|
0xD9.toByte(),
|
||||||
@@ -106,7 +103,7 @@ class AesKdf internal constructor() : KdfEngine() {
|
|||||||
0x4F.toByte(),
|
0x4F.toByte(),
|
||||||
0xEA.toByte()))
|
0xEA.toByte()))
|
||||||
|
|
||||||
const val PARAM_ROUNDS = "R"
|
const val PARAM_ROUNDS = "R" // UInt64
|
||||||
const val PARAM_SEED = "S"
|
const val PARAM_SEED = "S" // Byte array
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePassDX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.crypto.keyDerivation
|
|||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
||||||
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -53,35 +54,49 @@ class Argon2Kdf internal constructor() : KdfEngine() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray {
|
override fun transform(masterKey: ByteArray, kdfParameters: KdfParameters): ByteArray {
|
||||||
|
|
||||||
val salt = p.getByteArray(PARAM_SALT)
|
val salt = kdfParameters.getByteArray(PARAM_SALT)
|
||||||
val parallelism = p.getUInt32(PARAM_PARALLELISM).toInt()
|
val parallelism = kdfParameters.getUInt32(PARAM_PARALLELISM)?.let {
|
||||||
val memory = p.getUInt64(PARAM_MEMORY)
|
UnsignedInt(it)
|
||||||
val iterations = p.getUInt64(PARAM_ITERATIONS)
|
}
|
||||||
val version = p.getUInt32(PARAM_VERSION)
|
val memory = kdfParameters.getUInt64(PARAM_MEMORY)?.div(MEMORY_BLOCK_SIZE)?.let {
|
||||||
val secretKey = p.getByteArray(PARAM_SECRET_KEY)
|
UnsignedInt.fromKotlinLong(it)
|
||||||
val assocData = p.getByteArray(PARAM_ASSOC_DATA)
|
}
|
||||||
|
val iterations = kdfParameters.getUInt64(PARAM_ITERATIONS)?.let {
|
||||||
|
UnsignedInt.fromKotlinLong(it)
|
||||||
|
}
|
||||||
|
val version = kdfParameters.getUInt32(PARAM_VERSION)?.let {
|
||||||
|
UnsignedInt(it)
|
||||||
|
}
|
||||||
|
val secretKey = kdfParameters.getByteArray(PARAM_SECRET_KEY)
|
||||||
|
val assocData = kdfParameters.getByteArray(PARAM_ASSOC_DATA)
|
||||||
|
|
||||||
return Argon2Native.transformKey(masterKey, salt, parallelism, memory, iterations,
|
return Argon2Native.transformKey(masterKey,
|
||||||
secretKey, assocData, version)
|
salt,
|
||||||
|
parallelism,
|
||||||
|
memory,
|
||||||
|
iterations,
|
||||||
|
secretKey,
|
||||||
|
assocData,
|
||||||
|
version)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun randomize(p: KdfParameters) {
|
override fun randomize(kdfParameters: KdfParameters) {
|
||||||
val random = SecureRandom()
|
val random = SecureRandom()
|
||||||
|
|
||||||
val salt = ByteArray(32)
|
val salt = ByteArray(32)
|
||||||
random.nextBytes(salt)
|
random.nextBytes(salt)
|
||||||
|
|
||||||
p.setByteArray(PARAM_SALT, salt)
|
kdfParameters.setByteArray(PARAM_SALT, salt)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getKeyRounds(p: KdfParameters): Long {
|
override fun getKeyRounds(kdfParameters: KdfParameters): Long {
|
||||||
return p.getUInt64(PARAM_ITERATIONS)
|
return kdfParameters.getUInt64(PARAM_ITERATIONS) ?: defaultKeyRounds
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setKeyRounds(p: KdfParameters, keyRounds: Long) {
|
override fun setKeyRounds(kdfParameters: KdfParameters, keyRounds: Long) {
|
||||||
p.setUInt64(PARAM_ITERATIONS, keyRounds)
|
kdfParameters.setUInt64(PARAM_ITERATIONS, keyRounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val minKeyRounds: Long
|
override val minKeyRounds: Long
|
||||||
@@ -90,12 +105,12 @@ class Argon2Kdf internal constructor() : KdfEngine() {
|
|||||||
override val maxKeyRounds: Long
|
override val maxKeyRounds: Long
|
||||||
get() = MAX_ITERATIONS
|
get() = MAX_ITERATIONS
|
||||||
|
|
||||||
override fun getMemoryUsage(p: KdfParameters): Long {
|
override fun getMemoryUsage(kdfParameters: KdfParameters): Long {
|
||||||
return p.getUInt64(PARAM_MEMORY)
|
return kdfParameters.getUInt64(PARAM_MEMORY) ?: defaultMemoryUsage
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setMemoryUsage(p: KdfParameters, memory: Long) {
|
override fun setMemoryUsage(kdfParameters: KdfParameters, memory: Long) {
|
||||||
p.setUInt64(PARAM_MEMORY, memory)
|
kdfParameters.setUInt64(PARAM_MEMORY, memory)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val defaultMemoryUsage: Long
|
override val defaultMemoryUsage: Long
|
||||||
@@ -107,21 +122,23 @@ class Argon2Kdf internal constructor() : KdfEngine() {
|
|||||||
override val maxMemoryUsage: Long
|
override val maxMemoryUsage: Long
|
||||||
get() = MAX_MEMORY
|
get() = MAX_MEMORY
|
||||||
|
|
||||||
override fun getParallelism(p: KdfParameters): Int {
|
override fun getParallelism(kdfParameters: KdfParameters): Long {
|
||||||
return p.getUInt32(PARAM_PARALLELISM).toInt() // TODO Verify
|
return kdfParameters.getUInt32(PARAM_PARALLELISM)?.let {
|
||||||
|
UnsignedInt(it).toKotlinLong()
|
||||||
|
} ?: defaultParallelism
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setParallelism(p: KdfParameters, parallelism: Int) {
|
override fun setParallelism(kdfParameters: KdfParameters, parallelism: Long) {
|
||||||
p.setUInt32(PARAM_PARALLELISM, parallelism.toLong())
|
kdfParameters.setUInt32(PARAM_PARALLELISM, UnsignedInt.fromKotlinLong(parallelism))
|
||||||
}
|
}
|
||||||
|
|
||||||
override val defaultParallelism: Int
|
override val defaultParallelism: Long
|
||||||
get() = DEFAULT_PARALLELISM.toInt()
|
get() = DEFAULT_PARALLELISM.toKotlinLong()
|
||||||
|
|
||||||
override val minParallelism: Int
|
override val minParallelism: Long
|
||||||
get() = MIN_PARALLELISM
|
get() = MIN_PARALLELISM
|
||||||
|
|
||||||
override val maxParallelism: Int
|
override val maxParallelism: Long
|
||||||
get() = MAX_PARALLELISM
|
get() = MAX_PARALLELISM
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -152,23 +169,24 @@ class Argon2Kdf internal constructor() : KdfEngine() {
|
|||||||
private const val PARAM_SECRET_KEY = "K" // byte[]
|
private const val PARAM_SECRET_KEY = "K" // byte[]
|
||||||
private const val PARAM_ASSOC_DATA = "A" // byte[]
|
private const val PARAM_ASSOC_DATA = "A" // byte[]
|
||||||
|
|
||||||
private const val MIN_VERSION: Long = 0x10
|
private val MIN_VERSION = UnsignedInt(0x10)
|
||||||
private const val MAX_VERSION: Long = 0x13
|
private val MAX_VERSION = UnsignedInt(0x13)
|
||||||
|
|
||||||
private const val MIN_SALT = 8
|
private const val MIN_SALT = 8
|
||||||
private const val MAX_SALT = Integer.MAX_VALUE
|
private val MAX_SALT = UnsignedInt.MAX_VALUE.toKotlinLong()
|
||||||
|
|
||||||
private const val MIN_ITERATIONS: Long = 1
|
private const val MIN_ITERATIONS: Long = 1L
|
||||||
private const val MAX_ITERATIONS = 4294967295L
|
private const val MAX_ITERATIONS = 4294967295L
|
||||||
|
|
||||||
private const val MIN_MEMORY = (1024 * 8).toLong()
|
private const val MIN_MEMORY = (1024 * 8).toLong()
|
||||||
private const val MAX_MEMORY = Integer.MAX_VALUE.toLong()
|
private val MAX_MEMORY = UnsignedInt.MAX_VALUE.toKotlinLong()
|
||||||
|
private const val MEMORY_BLOCK_SIZE: Long = 1024L
|
||||||
|
|
||||||
private const val MIN_PARALLELISM = 1
|
private const val MIN_PARALLELISM: Long = 1L
|
||||||
private const val MAX_PARALLELISM = (1 shl 24) - 1
|
private const val MAX_PARALLELISM: Long = ((1 shl 24) - 1).toLong()
|
||||||
|
|
||||||
private const val DEFAULT_ITERATIONS: Long = 2
|
private const val DEFAULT_ITERATIONS: Long = 2L
|
||||||
private const val DEFAULT_MEMORY = (1024 * 1024).toLong()
|
private const val DEFAULT_MEMORY = (1024 * 1024).toLong()
|
||||||
private const val DEFAULT_PARALLELISM: Long = 2
|
private val DEFAULT_PARALLELISM = UnsignedInt(2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,20 +20,29 @@
|
|||||||
package com.kunzisoft.keepass.crypto.keyDerivation;
|
package com.kunzisoft.keepass.crypto.keyDerivation;
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.NativeLib;
|
import com.kunzisoft.keepass.crypto.NativeLib;
|
||||||
|
import com.kunzisoft.keepass.utils.UnsignedInt;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class Argon2Native {
|
public class Argon2Native {
|
||||||
|
|
||||||
public static byte[] transformKey(byte[] password, byte[] salt, int parallelism,
|
public static byte[] transformKey(byte[] password, byte[] salt, UnsignedInt parallelism,
|
||||||
long memory, long iterations, byte[] secretKey,
|
UnsignedInt memory, UnsignedInt iterations, byte[] secretKey,
|
||||||
byte[] associatedData, long version) throws IOException {
|
byte[] associatedData, UnsignedInt version) throws IOException {
|
||||||
NativeLib.INSTANCE.init();
|
NativeLib.INSTANCE.init();
|
||||||
|
|
||||||
return nTransformMasterKey(password, salt, parallelism, memory, iterations, secretKey, associatedData, version);
|
return nTransformMasterKey(
|
||||||
|
password,
|
||||||
|
salt,
|
||||||
|
parallelism.toKotlinInt(),
|
||||||
|
memory.toKotlinInt(),
|
||||||
|
iterations.toKotlinInt(),
|
||||||
|
secretKey,
|
||||||
|
associatedData,
|
||||||
|
version.toKotlinInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static native byte[] nTransformMasterKey(byte[] password, byte[] salt, int parallelism,
|
private static native byte[] nTransformMasterKey(byte[] password, byte[] salt, int parallelism,
|
||||||
long memory, long iterations, byte[] secretKey,
|
int memory, int iterations, byte[] secretKey,
|
||||||
byte[] associatedData, long version) throws IOException;
|
byte[] associatedData, int version) throws IOException;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
package com.kunzisoft.keepass.crypto.keyDerivation
|
package com.kunzisoft.keepass.crypto.keyDerivation
|
||||||
|
|
||||||
import com.kunzisoft.keepass.utils.ObjectNameResource
|
import com.kunzisoft.keepass.utils.ObjectNameResource
|
||||||
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
@@ -33,17 +34,17 @@ abstract class KdfEngine : ObjectNameResource, Serializable {
|
|||||||
abstract val defaultParameters: KdfParameters
|
abstract val defaultParameters: KdfParameters
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
abstract fun transform(masterKey: ByteArray, p: KdfParameters): ByteArray
|
abstract fun transform(masterKey: ByteArray, kdfParameters: KdfParameters): ByteArray
|
||||||
|
|
||||||
abstract fun randomize(p: KdfParameters)
|
abstract fun randomize(kdfParameters: KdfParameters)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ITERATIONS
|
* ITERATIONS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
abstract fun getKeyRounds(p: KdfParameters): Long
|
abstract fun getKeyRounds(kdfParameters: KdfParameters): Long
|
||||||
|
|
||||||
abstract fun setKeyRounds(p: KdfParameters, keyRounds: Long)
|
abstract fun setKeyRounds(kdfParameters: KdfParameters, keyRounds: Long)
|
||||||
|
|
||||||
abstract val defaultKeyRounds: Long
|
abstract val defaultKeyRounds: Long
|
||||||
|
|
||||||
@@ -51,51 +52,51 @@ abstract class KdfEngine : ObjectNameResource, Serializable {
|
|||||||
get() = 1
|
get() = 1
|
||||||
|
|
||||||
open val maxKeyRounds: Long
|
open val maxKeyRounds: Long
|
||||||
get() = Int.MAX_VALUE.toLong()
|
get() = UnsignedInt.MAX_VALUE.toKotlinLong()
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* MEMORY
|
* MEMORY
|
||||||
*/
|
*/
|
||||||
|
|
||||||
open fun getMemoryUsage(p: KdfParameters): Long {
|
open fun getMemoryUsage(kdfParameters: KdfParameters): Long {
|
||||||
return UNKNOWN_VALUE.toLong()
|
return UNKNOWN_VALUE
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun setMemoryUsage(p: KdfParameters, memory: Long) {
|
open fun setMemoryUsage(kdfParameters: KdfParameters, memory: Long) {
|
||||||
// Do nothing by default
|
// Do nothing by default
|
||||||
}
|
}
|
||||||
|
|
||||||
open val defaultMemoryUsage: Long
|
open val defaultMemoryUsage: Long
|
||||||
get() = UNKNOWN_VALUE.toLong()
|
get() = UNKNOWN_VALUE
|
||||||
|
|
||||||
open val minMemoryUsage: Long
|
open val minMemoryUsage: Long
|
||||||
get() = 1
|
get() = 1
|
||||||
|
|
||||||
open val maxMemoryUsage: Long
|
open val maxMemoryUsage: Long
|
||||||
get() = Int.MAX_VALUE.toLong()
|
get() = UnsignedInt.MAX_VALUE.toKotlinLong()
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* PARALLELISM
|
* PARALLELISM
|
||||||
*/
|
*/
|
||||||
|
|
||||||
open fun getParallelism(p: KdfParameters): Int {
|
open fun getParallelism(kdfParameters: KdfParameters): Long {
|
||||||
return UNKNOWN_VALUE
|
return UNKNOWN_VALUE
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun setParallelism(p: KdfParameters, parallelism: Int) {
|
open fun setParallelism(kdfParameters: KdfParameters, parallelism: Long) {
|
||||||
// Do nothing by default
|
// Do nothing by default
|
||||||
}
|
}
|
||||||
|
|
||||||
open val defaultParallelism: Int
|
open val defaultParallelism: Long
|
||||||
get() = UNKNOWN_VALUE
|
get() = UNKNOWN_VALUE
|
||||||
|
|
||||||
open val minParallelism: Int
|
open val minParallelism: Long
|
||||||
get() = 1
|
get() = 1L
|
||||||
|
|
||||||
open val maxParallelism: Int
|
open val maxParallelism: Long
|
||||||
get() = Int.MAX_VALUE
|
get() = UnsignedInt.MAX_VALUE.toKotlinLong()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val UNKNOWN_VALUE = -1
|
const val UNKNOWN_VALUE: Long = -1L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,17 +19,23 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto.keyDerivation
|
package com.kunzisoft.keepass.crypto.keyDerivation
|
||||||
|
|
||||||
import com.kunzisoft.keepass.stream.LittleEndianDataInputStream
|
|
||||||
import com.kunzisoft.keepass.stream.LittleEndianDataOutputStream
|
|
||||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
||||||
import com.kunzisoft.keepass.stream.uuidTo16Bytes
|
import com.kunzisoft.keepass.stream.uuidTo16Bytes
|
||||||
import com.kunzisoft.keepass.utils.VariantDictionary
|
import com.kunzisoft.keepass.utils.VariantDictionary
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class KdfParameters internal constructor(val uuid: UUID) : VariantDictionary() {
|
class KdfParameters: VariantDictionary {
|
||||||
|
|
||||||
|
val uuid: UUID
|
||||||
|
|
||||||
|
constructor(uuid: UUID): super() {
|
||||||
|
this.uuid = uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(uuid: UUID, d: VariantDictionary): super(d) {
|
||||||
|
this.uuid = uuid
|
||||||
|
}
|
||||||
|
|
||||||
fun setParamUUID() {
|
fun setParamUUID() {
|
||||||
setByteArray(PARAM_UUID, uuidTo16Bytes(uuid))
|
setByteArray(PARAM_UUID, uuidTo16Bytes(uuid))
|
||||||
@@ -41,26 +47,17 @@ class KdfParameters internal constructor(val uuid: UUID) : VariantDictionary() {
|
|||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun deserialize(data: ByteArray): KdfParameters? {
|
fun deserialize(data: ByteArray): KdfParameters? {
|
||||||
val bis = ByteArrayInputStream(data)
|
val dictionary = VariantDictionary.deserialize(data)
|
||||||
val lis = LittleEndianDataInputStream(bis)
|
|
||||||
|
|
||||||
val d = deserialize(lis) ?: return null
|
val uuidBytes = dictionary.getByteArray(PARAM_UUID) ?: return null
|
||||||
|
val uuid = bytes16ToUuid(uuidBytes)
|
||||||
|
|
||||||
val uuid = bytes16ToUuid(d.getByteArray(PARAM_UUID))
|
return KdfParameters(uuid, dictionary)
|
||||||
|
|
||||||
val kdfP = KdfParameters(uuid)
|
|
||||||
kdfP.copyTo(d)
|
|
||||||
return kdfP
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun serialize(kdf: KdfParameters): ByteArray {
|
fun serialize(kdfParameters: KdfParameters): ByteArray {
|
||||||
val bos = ByteArrayOutputStream()
|
return VariantDictionary.serialize(kdfParameters)
|
||||||
val los = LittleEndianDataOutputStream(bos)
|
|
||||||
|
|
||||||
serialize(kdf, los)
|
|
||||||
|
|
||||||
return bos.toByteArray()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ open class AssignPasswordInDatabaseRunnable (
|
|||||||
: SaveDatabaseRunnable(context, database, true) {
|
: SaveDatabaseRunnable(context, database, true) {
|
||||||
|
|
||||||
private var mMasterPassword: String? = null
|
private var mMasterPassword: String? = null
|
||||||
protected var mKeyFile: Uri? = null
|
protected var mKeyFileUri: Uri? = null
|
||||||
|
|
||||||
private var mBackupKey: ByteArray? = null
|
private var mBackupKey: ByteArray? = null
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ open class AssignPasswordInDatabaseRunnable (
|
|||||||
if (withMasterPassword)
|
if (withMasterPassword)
|
||||||
this.mMasterPassword = masterPassword
|
this.mMasterPassword = masterPassword
|
||||||
if (withKeyFile)
|
if (withKeyFile)
|
||||||
this.mKeyFile = keyFile
|
this.mKeyFileUri = keyFile
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
@@ -55,7 +55,7 @@ open class AssignPasswordInDatabaseRunnable (
|
|||||||
mBackupKey = ByteArray(database.masterKey.size)
|
mBackupKey = ByteArray(database.masterKey.size)
|
||||||
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
|
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
|
||||||
|
|
||||||
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mKeyFile)
|
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mKeyFileUri)
|
||||||
database.retrieveMasterKey(mMasterPassword, uriInputStream)
|
database.retrieveMasterKey(mMasterPassword, uriInputStream)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
erase(mBackupKey)
|
erase(mBackupKey)
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import android.util.Log
|
|||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.utils.closeDatabase
|
||||||
|
|
||||||
class CreateDatabaseRunnable(context: Context,
|
class CreateDatabaseRunnable(context: Context,
|
||||||
private val mDatabase: Database,
|
private val mDatabase: Database,
|
||||||
@@ -34,7 +36,8 @@ class CreateDatabaseRunnable(context: Context,
|
|||||||
withMasterPassword: Boolean,
|
withMasterPassword: Boolean,
|
||||||
masterPassword: String?,
|
masterPassword: String?,
|
||||||
withKeyFile: Boolean,
|
withKeyFile: Boolean,
|
||||||
keyFile: Uri?)
|
keyFile: Uri?,
|
||||||
|
private val createDatabaseResult: ((Result) -> Unit)?)
|
||||||
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, withMasterPassword, masterPassword, withKeyFile, keyFile) {
|
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, withMasterPassword, masterPassword, withKeyFile, keyFile) {
|
||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
@@ -42,29 +45,36 @@ class CreateDatabaseRunnable(context: Context,
|
|||||||
// Create new database record
|
// Create new database record
|
||||||
mDatabase.apply {
|
mDatabase.apply {
|
||||||
createData(mDatabaseUri, databaseName, rootName)
|
createData(mDatabaseUri, databaseName, rootName)
|
||||||
// Set Database state
|
|
||||||
loaded = true
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
mDatabase.closeAndClear()
|
mDatabase.closeAndClear(UriUtil.getBinaryDir(context))
|
||||||
setError(e)
|
setError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onStartRun()
|
super.onStartRun()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFinishRun() {
|
override fun onActionRun() {
|
||||||
super.onFinishRun()
|
super.onActionRun()
|
||||||
|
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
// Add database to recent files
|
// Add database to recent files
|
||||||
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
||||||
FileDatabaseHistoryAction.getInstance(context.applicationContext)
|
FileDatabaseHistoryAction.getInstance(context.applicationContext)
|
||||||
.addOrUpdateDatabaseUri(mDatabaseUri,
|
.addOrUpdateDatabaseUri(mDatabaseUri,
|
||||||
if (PreferencesUtil.rememberKeyFileLocations(context)) mKeyFile else null)
|
if (PreferencesUtil.rememberKeyFileLocations(context)) mKeyFileUri else null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register the current time to init the lock timer
|
||||||
|
PreferencesUtil.saveCurrentTime(context)
|
||||||
} else {
|
} else {
|
||||||
Log.e("CreateDatabaseRunnable", "Unable to create the database")
|
Log.e("CreateDatabaseRunnable", "Unable to create the database")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onFinishRun() {
|
||||||
|
super.onFinishRun()
|
||||||
|
|
||||||
|
createDatabaseResult?.invoke(result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,10 +27,11 @@ import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
|||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.utils.closeDatabase
|
||||||
|
|
||||||
class LoadDatabaseRunnable(private val context: Context,
|
class LoadDatabaseRunnable(private val context: Context,
|
||||||
private val mDatabase: Database,
|
private val mDatabase: Database,
|
||||||
@@ -39,17 +40,14 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
private val mKey: Uri?,
|
private val mKey: Uri?,
|
||||||
private val mReadonly: Boolean,
|
private val mReadonly: Boolean,
|
||||||
private val mCipherEntity: CipherDatabaseEntity?,
|
private val mCipherEntity: CipherDatabaseEntity?,
|
||||||
private val mOmitBackup: Boolean,
|
|
||||||
private val mFixDuplicateUUID: Boolean,
|
private val mFixDuplicateUUID: Boolean,
|
||||||
private val progressTaskUpdater: ProgressTaskUpdater?,
|
private val progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
private val mDuplicateUuidAction: ((Result) -> Unit)?)
|
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||||
: ActionRunnable() {
|
: ActionRunnable() {
|
||||||
|
|
||||||
private val cacheDirectory = context.applicationContext.filesDir
|
|
||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
// Clear before we load
|
// Clear before we load
|
||||||
mDatabase.closeAndClear(cacheDirectory)
|
mDatabase.closeAndClear(UriUtil.getBinaryDir(context))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionRun() {
|
override fun onActionRun() {
|
||||||
@@ -57,21 +55,17 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
mDatabase.loadData(mUri, mPass, mKey,
|
mDatabase.loadData(mUri, mPass, mKey,
|
||||||
mReadonly,
|
mReadonly,
|
||||||
context.contentResolver,
|
context.contentResolver,
|
||||||
cacheDirectory,
|
UriUtil.getBinaryDir(context),
|
||||||
mOmitBackup,
|
|
||||||
mFixDuplicateUUID,
|
mFixDuplicateUUID,
|
||||||
progressTaskUpdater)
|
progressTaskUpdater)
|
||||||
}
|
}
|
||||||
catch (e: DuplicateUuidDatabaseException) {
|
catch (e: DuplicateUuidDatabaseException) {
|
||||||
mDuplicateUuidAction?.invoke(result)
|
|
||||||
setError(e)
|
setError(e)
|
||||||
}
|
}
|
||||||
catch (e: LoadDatabaseException) {
|
catch (e: LoadDatabaseException) {
|
||||||
setError(e)
|
setError(e)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFinishRun() {
|
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
// Save keyFile in app database
|
// Save keyFile in app database
|
||||||
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
||||||
@@ -88,11 +82,12 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
|
|
||||||
// Register the current time to init the lock timer
|
// Register the current time to init the lock timer
|
||||||
PreferencesUtil.saveCurrentTime(context)
|
PreferencesUtil.saveCurrentTime(context)
|
||||||
|
|
||||||
// Start the opening notification
|
|
||||||
DatabaseOpenNotificationService.start(context)
|
|
||||||
} else {
|
} else {
|
||||||
mDatabase.closeAndClear(cacheDirectory)
|
mDatabase.closeAndClear(UriUtil.getBinaryDir(context))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onFinishRun() {
|
||||||
|
mLoadDatabaseResult?.invoke(result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,20 +23,18 @@ import android.content.*
|
|||||||
import android.content.Context.BIND_ABOVE_CLIENT
|
import android.content.Context.BIND_ABOVE_CLIENT
|
||||||
import android.content.Context.BIND_NOT_FOREGROUND
|
import android.content.Context.BIND_NOT_FOREGROUND
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||||
import com.kunzisoft.keepass.database.element.*
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
||||||
@@ -47,6 +45,7 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
|
|||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK
|
||||||
@@ -54,6 +53,8 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
|
|||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DESCRIPTION_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DESCRIPTION_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENCRYPTION_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENCRYPTION_TASK
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ITERATIONS_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ITERATIONS_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK
|
||||||
@@ -61,72 +62,61 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
|
|||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_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.getBundleFromListNodes
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.retrieveProgressDialog
|
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
|
||||||
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
|
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
|
||||||
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
class ProgressDialogThread(private val activity: FragmentActivity) {
|
class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
||||||
|
|
||||||
var onActionFinish: ((actionTask: String,
|
var onActionFinish: ((actionTask: String,
|
||||||
result: ActionRunnable.Result) -> Unit)? = null
|
result: ActionRunnable.Result) -> Unit)? = null
|
||||||
|
|
||||||
private var intentDatabaseTask = Intent(activity, DatabaseTaskNotificationService::class.java)
|
private var intentDatabaseTask = Intent(activity.applicationContext, DatabaseTaskNotificationService::class.java)
|
||||||
|
|
||||||
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
|
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
|
||||||
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
|
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
|
||||||
|
|
||||||
private var serviceConnection: ServiceConnection? = null
|
private var serviceConnection: ServiceConnection? = null
|
||||||
|
|
||||||
|
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
|
||||||
|
|
||||||
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
|
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
|
||||||
override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
override fun onStartAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||||
TimeoutHelper.temporarilyDisableTimeout()
|
startDialog(titleId, messageId, warningId)
|
||||||
// Stop the opening notification
|
|
||||||
DatabaseOpenNotificationService.stop(activity)
|
|
||||||
startOrUpdateDialog(titleId, messageId, warningId)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUpdateAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
override fun onUpdateAction(titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||||
TimeoutHelper.temporarilyDisableTimeout()
|
updateDialog(titleId, messageId, warningId)
|
||||||
// Stop the opening notification
|
|
||||||
DatabaseOpenNotificationService.stop(activity)
|
|
||||||
startOrUpdateDialog(titleId, messageId, warningId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStopAction(actionTask: String, result: ActionRunnable.Result) {
|
override fun onStopAction(actionTask: String, result: ActionRunnable.Result) {
|
||||||
onActionFinish?.invoke(actionTask, result)
|
onActionFinish?.invoke(actionTask, result)
|
||||||
// Remove the progress task
|
// Remove the progress task
|
||||||
ProgressTaskDialogFragment.stop(activity)
|
stopDialog()
|
||||||
TimeoutHelper.releaseTemporarilyDisableTimeout()
|
|
||||||
|
|
||||||
val inTime = if (activity is LockingActivity) {
|
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeout(activity)
|
|
||||||
} else {
|
|
||||||
TimeoutHelper.checkTime(activity)
|
|
||||||
}
|
|
||||||
// Start the opening notification if in time
|
|
||||||
// (databaseOpenService is open manually in Action Open Task)
|
|
||||||
if (actionTask != ACTION_DATABASE_LOAD_TASK && inTime) {
|
|
||||||
DatabaseOpenNotificationService.start(activity)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startOrUpdateDialog(titleId: Int?, messageId: Int?, warningId: Int?) {
|
private fun startDialog(titleId: Int? = null,
|
||||||
var progressTaskDialogFragment = retrieveProgressDialog(activity)
|
messageId: Int? = null,
|
||||||
|
warningId: Int? = null) {
|
||||||
if (progressTaskDialogFragment == null) {
|
if (progressTaskDialogFragment == null) {
|
||||||
progressTaskDialogFragment = ProgressTaskDialogFragment.build()
|
progressTaskDialogFragment = activity.supportFragmentManager
|
||||||
ProgressTaskDialogFragment.start(activity, progressTaskDialogFragment)
|
.findFragmentByTag(PROGRESS_TASK_DIALOG_TAG) as ProgressTaskDialogFragment?
|
||||||
}
|
}
|
||||||
progressTaskDialogFragment.apply {
|
if (progressTaskDialogFragment == null) {
|
||||||
|
progressTaskDialogFragment = ProgressTaskDialogFragment()
|
||||||
|
progressTaskDialogFragment?.show(activity.supportFragmentManager, PROGRESS_TASK_DIALOG_TAG)
|
||||||
|
}
|
||||||
|
updateDialog(titleId, messageId, warningId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateDialog(titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||||
|
progressTaskDialogFragment?.apply {
|
||||||
titleId?.let {
|
titleId?.let {
|
||||||
updateTitle(it)
|
updateTitle(it)
|
||||||
}
|
}
|
||||||
@@ -139,7 +129,11 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
private fun stopDialog() {
|
||||||
|
progressTaskDialogFragment?.dismissAllowingStateLoss()
|
||||||
|
progressTaskDialogFragment = null
|
||||||
|
}
|
||||||
|
|
||||||
private fun initServiceConnection() {
|
private fun initServiceConnection() {
|
||||||
if (serviceConnection == null) {
|
if (serviceConnection == null) {
|
||||||
serviceConnection = object : ServiceConnection {
|
serviceConnection = object : ServiceConnection {
|
||||||
@@ -158,7 +152,6 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun bindService() {
|
private fun bindService() {
|
||||||
initServiceConnection()
|
initServiceConnection()
|
||||||
serviceConnection?.let {
|
serviceConnection?.let {
|
||||||
@@ -169,7 +162,6 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
/**
|
/**
|
||||||
* Unbind the service and assign null to the service connection to check if already unbind or not
|
* Unbind the service and assign null to the service connection to check if already unbind or not
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
|
||||||
private fun unBindService() {
|
private fun unBindService() {
|
||||||
serviceConnection?.let {
|
serviceConnection?.let {
|
||||||
activity.unbindService(it)
|
activity.unbindService(it)
|
||||||
@@ -177,22 +169,25 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
serviceConnection = null
|
serviceConnection = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
fun isBinded(): Boolean {
|
||||||
|
return mBinder != null
|
||||||
|
}
|
||||||
|
|
||||||
fun registerProgressTask() {
|
fun registerProgressTask() {
|
||||||
ProgressTaskDialogFragment.stop(activity)
|
stopDialog()
|
||||||
|
|
||||||
// Register a database task receiver to stop loading dialog when service finish the task
|
// Register a database task receiver to stop loading dialog when service finish the task
|
||||||
databaseTaskBroadcastReceiver = object : BroadcastReceiver() {
|
databaseTaskBroadcastReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
activity.runOnUiThread {
|
when (intent?.action) {
|
||||||
when (intent?.action) {
|
DATABASE_START_TASK_ACTION -> {
|
||||||
DATABASE_START_TASK_ACTION -> {
|
// Bind to the service when is starting
|
||||||
// Bind to the service when is starting
|
bindService()
|
||||||
bindService()
|
}
|
||||||
}
|
DATABASE_STOP_TASK_ACTION -> {
|
||||||
DATABASE_STOP_TASK_ACTION -> {
|
// Remove the progress task
|
||||||
unBindService()
|
stopDialog()
|
||||||
}
|
unBindService()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,9 +203,8 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
bindService()
|
bindService()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun unregisterProgressTask() {
|
fun unregisterProgressTask() {
|
||||||
ProgressTaskDialogFragment.stop(activity)
|
stopDialog()
|
||||||
|
|
||||||
mBinder?.removeActionTaskListener(actionTaskListener)
|
mBinder?.removeActionTaskListener(actionTaskListener)
|
||||||
mBinder = null
|
mBinder = null
|
||||||
@@ -224,19 +218,12 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun start(bundle: Bundle? = null, actionTask: String) {
|
private fun start(bundle: Bundle? = null, actionTask: String) {
|
||||||
activity.stopService(intentDatabaseTask)
|
activity.stopService(intentDatabaseTask)
|
||||||
if (bundle != null)
|
if (bundle != null)
|
||||||
intentDatabaseTask.putExtras(bundle)
|
intentDatabaseTask.putExtras(bundle)
|
||||||
activity.runOnUiThread {
|
intentDatabaseTask.action = actionTask
|
||||||
intentDatabaseTask.action = actionTask
|
activity.startService(intentDatabaseTask)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
activity.startForegroundService(intentDatabaseTask)
|
|
||||||
} else {
|
|
||||||
activity.startService(intentDatabaseTask)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -255,7 +242,7 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
||||||
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
||||||
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
|
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
|
||||||
putParcelable(DatabaseTaskNotificationService.KEY_FILE_KEY, keyFile)
|
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_CREATE_TASK)
|
, ACTION_DATABASE_CREATE_TASK)
|
||||||
}
|
}
|
||||||
@@ -269,7 +256,7 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||||
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
||||||
putParcelable(DatabaseTaskNotificationService.KEY_FILE_KEY, keyFile)
|
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
|
||||||
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
|
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
|
||||||
putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity)
|
putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity)
|
||||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||||
@@ -288,7 +275,7 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
||||||
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
||||||
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
|
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
|
||||||
putParcelable(DatabaseTaskNotificationService.KEY_FILE_KEY, keyFile)
|
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_ASSIGN_PASSWORD_TASK)
|
, ACTION_DATABASE_ASSIGN_PASSWORD_TASK)
|
||||||
}
|
}
|
||||||
@@ -480,6 +467,13 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
|
, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startDatabaseRemoveUnlinkedData(save: Boolean) {
|
||||||
|
start(Bundle().apply {
|
||||||
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
|
}
|
||||||
|
, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
|
||||||
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
|
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
|
||||||
newMaxHistoryItems: Int,
|
newMaxHistoryItems: Int,
|
||||||
save: Boolean) {
|
save: Boolean) {
|
||||||
@@ -552,12 +546,12 @@ class ProgressDialogThread(private val activity: FragmentActivity) {
|
|||||||
, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
|
, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveParallelism(oldParallelism: Int,
|
fun startDatabaseSaveParallelism(oldParallelism: Long,
|
||||||
newParallelism: Int,
|
newParallelism: Long,
|
||||||
save: Boolean) {
|
save: Boolean) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putInt(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldParallelism)
|
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldParallelism)
|
||||||
putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newParallelism)
|
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newParallelism)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_UPDATE_PARALLELISM_TASK)
|
, ACTION_DATABASE_UPDATE_PARALLELISM_TASK)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePassDX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
@@ -17,6 +17,24 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.search.iterator
|
package com.kunzisoft.keepass.database.action
|
||||||
|
|
||||||
abstract class EntrySearchStringIterator : Iterator<String>
|
import android.content.Context
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
|
||||||
|
class RemoveUnlinkedDataDatabaseRunnable (
|
||||||
|
context: Context,
|
||||||
|
database: Database,
|
||||||
|
saveDatabase: Boolean)
|
||||||
|
: SaveDatabaseRunnable(context, database, saveDatabase) {
|
||||||
|
|
||||||
|
override fun onActionRun() {
|
||||||
|
try {
|
||||||
|
database.removeUnlinkedAttachments()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
setError(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onActionRun()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ class DeleteEntryHistoryDatabaseRunnable (
|
|||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
try {
|
try {
|
||||||
mainEntry.removeEntryFromHistory(entryHistoryPosition)
|
database.removeEntryHistory(mainEntry, entryHistoryPosition)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
setError(e)
|
setError(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ class DeleteNodesRunnable(context: Context,
|
|||||||
} else {
|
} else {
|
||||||
database.deleteEntry(currentNode)
|
database.deleteEntry(currentNode)
|
||||||
}
|
}
|
||||||
|
// Remove the oldest attachments
|
||||||
|
currentNode.getAttachments(database.binaryPool).forEach {
|
||||||
|
database.removeAttachmentIfNotUsed(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
package com.kunzisoft.keepass.database.action.node
|
package com.kunzisoft.keepass.database.action.node
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
@@ -40,16 +41,34 @@ class UpdateEntryRunnable constructor(
|
|||||||
// WARNING : Re attribute parent removed in entry edit activity to save memory
|
// WARNING : Re attribute parent removed in entry edit activity to save memory
|
||||||
mNewEntry.addParentFrom(mOldEntry)
|
mNewEntry.addParentFrom(mOldEntry)
|
||||||
|
|
||||||
|
// Build oldest attachments
|
||||||
|
val oldEntryAttachments = mOldEntry.getAttachments(database.binaryPool, true)
|
||||||
|
val newEntryAttachments = mNewEntry.getAttachments(database.binaryPool, true)
|
||||||
|
val attachmentsToRemove = ArrayList<Attachment>(oldEntryAttachments)
|
||||||
|
// Not use equals because only check name
|
||||||
|
newEntryAttachments.forEach { newAttachment ->
|
||||||
|
oldEntryAttachments.forEach { oldAttachment ->
|
||||||
|
if (oldAttachment.name == newAttachment.name
|
||||||
|
&& oldAttachment.binaryAttachment == newAttachment.binaryAttachment)
|
||||||
|
attachmentsToRemove.remove(oldAttachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update entry with new values
|
// Update entry with new values
|
||||||
mOldEntry.updateWith(mNewEntry)
|
mOldEntry.updateWith(mNewEntry)
|
||||||
mNewEntry.touch(modified = true, touchParents = true)
|
mNewEntry.touch(modified = true, touchParents = true)
|
||||||
|
|
||||||
// Create an entry history (an entry history don't have history)
|
// Create an entry history (an entry history don't have history)
|
||||||
mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false))
|
mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false))
|
||||||
database.removeOldestEntryHistory(mOldEntry)
|
database.removeOldestEntryHistory(mOldEntry, database.binaryPool)
|
||||||
|
|
||||||
// Only change data in index
|
// Only change data in index
|
||||||
database.updateEntry(mOldEntry)
|
database.updateEntry(mOldEntry)
|
||||||
|
|
||||||
|
// Remove oldest attachments
|
||||||
|
attachmentsToRemove.forEach {
|
||||||
|
database.removeAttachmentIfNotUsed(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun nodeFinish(): ActionNodesValues {
|
override fun nodeFinish(): ActionNodesValues {
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.database.element
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
||||||
|
|
||||||
|
data class Attachment(var name: String,
|
||||||
|
var binaryAttachment: BinaryAttachment) : Parcelable {
|
||||||
|
|
||||||
|
constructor(parcel: Parcel) : this(
|
||||||
|
parcel.readString() ?: "",
|
||||||
|
parcel.readParcelable(BinaryAttachment::class.java.classLoader) ?: BinaryAttachment()
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeString(name)
|
||||||
|
parcel.writeParcelable(binaryAttachment, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "$name at $binaryAttachment"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is Attachment) return false
|
||||||
|
|
||||||
|
if (name != other.name) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return name.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object CREATOR : Parcelable.Creator<Attachment> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): Attachment {
|
||||||
|
return Attachment(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<Attachment?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,9 +25,7 @@ import android.net.Uri
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
||||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.*
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
|
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||||
@@ -44,15 +42,15 @@ import com.kunzisoft.keepass.database.file.input.DatabaseInputKDBX
|
|||||||
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB
|
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB
|
||||||
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX
|
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.stream.readBytes4ToUInt
|
||||||
import com.kunzisoft.keepass.stream.readBytes4ToInt
|
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
import com.kunzisoft.keepass.utils.SingletonHolder
|
import com.kunzisoft.keepass.utils.SingletonHolder
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import org.apache.commons.io.FileUtils
|
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
|
||||||
class Database {
|
class Database {
|
||||||
@@ -71,6 +69,13 @@ class Database {
|
|||||||
val drawFactory = IconDrawableFactory()
|
val drawFactory = IconDrawableFactory()
|
||||||
|
|
||||||
var loaded = false
|
var loaded = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
loadTimestamp = if (field) System.currentTimeMillis() else null
|
||||||
|
}
|
||||||
|
|
||||||
|
var loadTimestamp: Long? = null
|
||||||
|
private set
|
||||||
|
|
||||||
val iconFactory: IconImageFactory
|
val iconFactory: IconImageFactory
|
||||||
get() {
|
get() {
|
||||||
@@ -151,6 +156,17 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun compressionForNewEntry(): Boolean {
|
||||||
|
if (mDatabaseKDB != null)
|
||||||
|
return false
|
||||||
|
// Default compression not necessary if stored in header
|
||||||
|
mDatabaseKDBX?.let {
|
||||||
|
return it.compressionAlgorithm == CompressionAlgorithm.GZip
|
||||||
|
&& it.kdbxVersion.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
fun updateDataBinaryCompression(oldCompression: CompressionAlgorithm,
|
fun updateDataBinaryCompression(oldCompression: CompressionAlgorithm,
|
||||||
newCompression: CompressionAlgorithm) {
|
newCompression: CompressionAlgorithm) {
|
||||||
mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression)
|
mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression)
|
||||||
@@ -205,7 +221,6 @@ class Database {
|
|||||||
|
|
||||||
var numberKeyEncryptionRounds: Long
|
var numberKeyEncryptionRounds: Long
|
||||||
get() = mDatabaseKDB?.numberKeyEncryptionRounds ?: mDatabaseKDBX?.numberKeyEncryptionRounds ?: 0
|
get() = mDatabaseKDB?.numberKeyEncryptionRounds ?: mDatabaseKDBX?.numberKeyEncryptionRounds ?: 0
|
||||||
@Throws(NumberFormatException::class)
|
|
||||||
set(numberRounds) {
|
set(numberRounds) {
|
||||||
mDatabaseKDB?.numberKeyEncryptionRounds = numberRounds
|
mDatabaseKDB?.numberKeyEncryptionRounds = numberRounds
|
||||||
mDatabaseKDBX?.numberKeyEncryptionRounds = numberRounds
|
mDatabaseKDBX?.numberKeyEncryptionRounds = numberRounds
|
||||||
@@ -213,13 +228,13 @@ class Database {
|
|||||||
|
|
||||||
var memoryUsage: Long
|
var memoryUsage: Long
|
||||||
get() {
|
get() {
|
||||||
return mDatabaseKDBX?.memoryUsage ?: return KdfEngine.UNKNOWN_VALUE.toLong()
|
return mDatabaseKDBX?.memoryUsage ?: return KdfEngine.UNKNOWN_VALUE
|
||||||
}
|
}
|
||||||
set(memory) {
|
set(memory) {
|
||||||
mDatabaseKDBX?.memoryUsage = memory
|
mDatabaseKDBX?.memoryUsage = memory
|
||||||
}
|
}
|
||||||
|
|
||||||
var parallelism: Int
|
var parallelism: Long
|
||||||
get() = mDatabaseKDBX?.parallelism ?: KdfEngine.UNKNOWN_VALUE
|
get() = mDatabaseKDBX?.parallelism ?: KdfEngine.UNKNOWN_VALUE
|
||||||
set(parallelism) {
|
set(parallelism) {
|
||||||
mDatabaseKDBX?.parallelism = parallelism
|
mDatabaseKDBX?.parallelism = parallelism
|
||||||
@@ -263,14 +278,14 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if RecycleBin is available or not for this version of database
|
* Determine if a configurable RecycleBin is available or not for this version of database
|
||||||
* @return true if RecycleBin available
|
* @return true if a configurable RecycleBin available
|
||||||
*/
|
*/
|
||||||
val allowRecycleBin: Boolean
|
val allowConfigurableRecycleBin: Boolean
|
||||||
get() = mDatabaseKDBX != null
|
get() = mDatabaseKDBX != null
|
||||||
|
|
||||||
var isRecycleBinEnabled: Boolean
|
var isRecycleBinEnabled: Boolean
|
||||||
// TODO #394 isRecycleBinEnabled mDatabaseKDB
|
// Backup is always enabled in KDB database
|
||||||
get() = mDatabaseKDB != null || mDatabaseKDBX?.isRecycleBinEnabled ?: false
|
get() = mDatabaseKDB != null || mDatabaseKDBX?.isRecycleBinEnabled ?: false
|
||||||
set(value) {
|
set(value) {
|
||||||
mDatabaseKDBX?.isRecycleBinEnabled = value
|
mDatabaseKDBX?.isRecycleBinEnabled = value
|
||||||
@@ -288,12 +303,12 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun ensureRecycleBinExists(resources: Resources) {
|
fun ensureRecycleBinExists(resources: Resources) {
|
||||||
mDatabaseKDB?.ensureRecycleBinExists()
|
mDatabaseKDB?.ensureBackupExists()
|
||||||
mDatabaseKDBX?.ensureRecycleBinExists(resources)
|
mDatabaseKDBX?.ensureRecycleBinExists(resources)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeRecycleBin() {
|
fun removeRecycleBin() {
|
||||||
// TODO #394 delete backup mDatabaseKDB?.removeRecycleBin()
|
// Don't allow remove backup in KDB
|
||||||
mDatabaseKDBX?.removeRecycleBin()
|
mDatabaseKDBX?.removeRecycleBin()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,6 +325,8 @@ class Database {
|
|||||||
fun createData(databaseUri: Uri, databaseName: String, rootName: String) {
|
fun createData(databaseUri: Uri, databaseName: String, rootName: String) {
|
||||||
setDatabaseKDBX(DatabaseKDBX(databaseName, rootName))
|
setDatabaseKDBX(DatabaseKDBX(databaseName, rootName))
|
||||||
this.fileUri = databaseUri
|
this.fileUri = databaseUri
|
||||||
|
// Set Database state
|
||||||
|
this.loaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
@@ -317,7 +334,6 @@ class Database {
|
|||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
contentResolver: ContentResolver,
|
contentResolver: ContentResolver,
|
||||||
cacheDirectory: File,
|
cacheDirectory: File,
|
||||||
omitBackup: Boolean,
|
|
||||||
fixDuplicateUUID: Boolean,
|
fixDuplicateUUID: Boolean,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||||
|
|
||||||
@@ -338,7 +354,10 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load Data, pass Uris as InputStreams
|
// Load Data, pass Uris as InputStreams
|
||||||
databaseInputStream = BufferedInputStream(UriUtil.getUriInputStream(contentResolver, uri))
|
val databaseStream = UriUtil.getUriInputStream(contentResolver, uri)
|
||||||
|
?: throw IOException("Database input stream cannot be retrieve")
|
||||||
|
|
||||||
|
databaseInputStream = BufferedInputStream(databaseStream)
|
||||||
if (!databaseInputStream.markSupported()) {
|
if (!databaseInputStream.markSupported()) {
|
||||||
throw IOException("Input stream does not support mark.")
|
throw IOException("Input stream does not support mark.")
|
||||||
}
|
}
|
||||||
@@ -347,8 +366,8 @@ class Database {
|
|||||||
databaseInputStream.mark(10)
|
databaseInputStream.mark(10)
|
||||||
|
|
||||||
// Get the file directory to save the attachments
|
// Get the file directory to save the attachments
|
||||||
val sig1 = databaseInputStream.readBytes4ToInt()
|
val sig1 = databaseInputStream.readBytes4ToUInt()
|
||||||
val sig2 = databaseInputStream.readBytes4ToInt()
|
val sig2 = databaseInputStream.readBytes4ToUInt()
|
||||||
|
|
||||||
// Return to the start
|
// Return to the start
|
||||||
databaseInputStream.reset()
|
databaseInputStream.reset()
|
||||||
@@ -376,14 +395,12 @@ class Database {
|
|||||||
else -> throw SignatureDatabaseException()
|
else -> throw SignatureDatabaseException()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mSearchHelper = SearchHelper(omitBackup)
|
this.mSearchHelper = SearchHelper()
|
||||||
loaded = true
|
loaded = true
|
||||||
|
|
||||||
} catch (e: LoadDatabaseException) {
|
} catch (e: LoadDatabaseException) {
|
||||||
Log.e("KPD", "Database::loadData", e)
|
|
||||||
throw e
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("KPD", "Database::loadData", e)
|
|
||||||
throw FileNotFoundDatabaseException()
|
throw FileNotFoundDatabaseException()
|
||||||
} finally {
|
} finally {
|
||||||
keyFileInputStream?.close()
|
keyFileInputStream?.close()
|
||||||
@@ -391,23 +408,66 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isGroupSearchable(group: Group, isOmitBackup: Boolean): Boolean {
|
fun isGroupSearchable(group: Group, omitBackup: Boolean): Boolean {
|
||||||
return mDatabaseKDB?.isGroupSearchable(group.groupKDB, isOmitBackup) ?:
|
return mDatabaseKDB?.isGroupSearchable(group.groupKDB, omitBackup) ?:
|
||||||
mDatabaseKDBX?.isGroupSearchable(group.groupKDBX, isOmitBackup) ?:
|
mDatabaseKDBX?.isGroupSearchable(group.groupKDBX, omitBackup) ?:
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createVirtualGroupFromSearch(searchQuery: String, max: Int = Integer.MAX_VALUE): Group? {
|
fun createVirtualGroupFromSearch(searchQuery: String,
|
||||||
return mSearchHelper?.createVirtualGroupWithSearchResult(this, searchQuery, max)
|
omitBackup: Boolean,
|
||||||
|
max: Int = Integer.MAX_VALUE): Group? {
|
||||||
|
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
||||||
|
searchQuery, SearchParameters(), omitBackup, max)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createVirtualGroupFromSearch(searchInfo: SearchInfo, max: Int = Integer.MAX_VALUE): Group? {
|
fun createVirtualGroupFromSearchInfo(searchInfoString: String,
|
||||||
val query = (if (searchInfo.webDomain != null)
|
omitBackup: Boolean,
|
||||||
searchInfo.webDomain
|
max: Int = Integer.MAX_VALUE): Group? {
|
||||||
else
|
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
||||||
searchInfo.applicationId)
|
searchInfoString, SearchParameters().apply {
|
||||||
?: return null
|
searchInTitles = false
|
||||||
return mSearchHelper?.createVirtualGroupWithSearchResult(this, query, max)
|
searchInUserNames = false
|
||||||
|
searchInPasswords = false
|
||||||
|
searchInUrls = true
|
||||||
|
searchInNotes = true
|
||||||
|
searchInOther = true
|
||||||
|
searchInUUIDs = false
|
||||||
|
searchInTags = false
|
||||||
|
ignoreCase = true
|
||||||
|
}, omitBackup, max)
|
||||||
|
}
|
||||||
|
|
||||||
|
val binaryPool: BinaryPool
|
||||||
|
get() {
|
||||||
|
return mDatabaseKDBX?.binaryPool ?: BinaryPool()
|
||||||
|
}
|
||||||
|
|
||||||
|
val allowMultipleAttachments: Boolean
|
||||||
|
get() {
|
||||||
|
if (mDatabaseKDB != null)
|
||||||
|
return false
|
||||||
|
if (mDatabaseKDBX != null)
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildNewBinary(cacheDirectory: File,
|
||||||
|
compressed: Boolean = false,
|
||||||
|
protected: Boolean = false): BinaryAttachment? {
|
||||||
|
return mDatabaseKDB?.buildNewBinary(cacheDirectory)
|
||||||
|
?: mDatabaseKDBX?.buildNewBinary(cacheDirectory, compressed, protected)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAttachmentIfNotUsed(attachment: Attachment) {
|
||||||
|
// No need in KDB database because unique attachment by entry
|
||||||
|
// Don't clear to fix upload multiple times
|
||||||
|
mDatabaseKDBX?.removeUnlinkedAttachment(attachment.binaryAttachment, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeUnlinkedAttachments() {
|
||||||
|
// No check in database KDB because unique attachment by entry
|
||||||
|
mDatabaseKDBX?.removeUnlinkedAttachments(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(DatabaseOutputException::class)
|
@Throws(DatabaseOutputException::class)
|
||||||
@@ -455,7 +515,7 @@ class Database {
|
|||||||
} else {
|
} else {
|
||||||
var outputStream: OutputStream? = null
|
var outputStream: OutputStream? = null
|
||||||
try {
|
try {
|
||||||
outputStream = contentResolver.openOutputStream(uri)
|
outputStream = contentResolver.openOutputStream(uri, "rwt")
|
||||||
outputStream?.let { definedOutputStream ->
|
outputStream?.let { definedOutputStream ->
|
||||||
val databaseOutput = mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
|
val databaseOutput = mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
|
||||||
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, definedOutputStream) }
|
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, definedOutputStream) }
|
||||||
@@ -477,7 +537,9 @@ class Database {
|
|||||||
mDatabaseKDBX?.clearCache()
|
mDatabaseKDBX?.clearCache()
|
||||||
// In all cases, delete all the files in the temp dir
|
// In all cases, delete all the files in the temp dir
|
||||||
try {
|
try {
|
||||||
FileUtils.cleanDirectory(filesDirectory)
|
filesDirectory?.let { directory ->
|
||||||
|
cleanDirectory(directory)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to clear the directory cache.", e)
|
Log.e(TAG, "Unable to clear the directory cache.", e)
|
||||||
}
|
}
|
||||||
@@ -488,6 +550,17 @@ class Database {
|
|||||||
this.loaded = false
|
this.loaded = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun cleanDirectory(directory: File) {
|
||||||
|
directory.listFiles()?.let { files ->
|
||||||
|
for (file in files) {
|
||||||
|
if (file.isDirectory) {
|
||||||
|
cleanDirectory(file)
|
||||||
|
}
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
||||||
return mDatabaseKDB?.validatePasswordEncoding(password, containsKeyFile)
|
return mDatabaseKDB?.validatePasswordEncoding(password, containsKeyFile)
|
||||||
?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile)
|
?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile)
|
||||||
@@ -687,7 +760,7 @@ class Database {
|
|||||||
fun canRecycle(entry: Entry): Boolean {
|
fun canRecycle(entry: Entry): Boolean {
|
||||||
var canRecycle: Boolean? = null
|
var canRecycle: Boolean? = null
|
||||||
entry.entryKDB?.let {
|
entry.entryKDB?.let {
|
||||||
canRecycle = mDatabaseKDB?.canRecycle()
|
canRecycle = mDatabaseKDB?.canRecycle(it)
|
||||||
}
|
}
|
||||||
entry.entryKDBX?.let {
|
entry.entryKDBX?.let {
|
||||||
canRecycle = mDatabaseKDBX?.canRecycle(it)
|
canRecycle = mDatabaseKDBX?.canRecycle(it)
|
||||||
@@ -698,7 +771,7 @@ class Database {
|
|||||||
fun canRecycle(group: Group): Boolean {
|
fun canRecycle(group: Group): Boolean {
|
||||||
var canRecycle: Boolean? = null
|
var canRecycle: Boolean? = null
|
||||||
group.groupKDB?.let {
|
group.groupKDB?.let {
|
||||||
canRecycle = mDatabaseKDB?.canRecycle()
|
canRecycle = mDatabaseKDB?.canRecycle(it)
|
||||||
}
|
}
|
||||||
group.groupKDBX?.let {
|
group.groupKDBX?.let {
|
||||||
canRecycle = mDatabaseKDBX?.canRecycle(it)
|
canRecycle = mDatabaseKDBX?.canRecycle(it)
|
||||||
@@ -750,18 +823,25 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startManageEntry(entry: Entry) {
|
fun startManageEntry(entry: Entry?) {
|
||||||
mDatabaseKDBX?.let {
|
mDatabaseKDBX?.let {
|
||||||
entry.startToManageFieldReferences(it)
|
entry?.startToManageFieldReferences(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopManageEntry(entry: Entry) {
|
fun stopManageEntry(entry: Entry?) {
|
||||||
mDatabaseKDBX?.let {
|
mDatabaseKDBX?.let {
|
||||||
entry.stopToManageFieldReferences()
|
entry?.stopToManageFieldReferences()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if database allows custom field
|
||||||
|
*/
|
||||||
|
fun allowEntryCustomFields(): Boolean {
|
||||||
|
return mDatabaseKDBX != null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove oldest history for each entry if more than max items or max memory
|
* Remove oldest history for each entry if more than max items or max memory
|
||||||
*/
|
*/
|
||||||
@@ -769,7 +849,7 @@ class Database {
|
|||||||
rootGroup?.doForEachChildAndForIt(
|
rootGroup?.doForEachChildAndForIt(
|
||||||
object : NodeHandler<Entry>() {
|
object : NodeHandler<Entry>() {
|
||||||
override fun operate(node: Entry): Boolean {
|
override fun operate(node: Entry): Boolean {
|
||||||
removeOldestEntryHistory(node)
|
removeOldestEntryHistory(node, binaryPool)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -777,34 +857,19 @@ class Database {
|
|||||||
override fun operate(node: Group): Boolean {
|
override fun operate(node: Group): Boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
fun removeEachEntryHistory() {
|
|
||||||
rootGroup?.doForEachChildAndForIt(
|
|
||||||
object : NodeHandler<Entry>() {
|
|
||||||
override fun operate(node: Entry): Boolean {
|
|
||||||
node.removeAllHistory()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
object : NodeHandler<Group>() {
|
|
||||||
override fun operate(node: Group): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove oldest history if more than max items or max memory
|
* Remove oldest history if more than max items or max memory
|
||||||
*/
|
*/
|
||||||
fun removeOldestEntryHistory(entry: Entry) {
|
fun removeOldestEntryHistory(entry: Entry, binaryPool: BinaryPool) {
|
||||||
mDatabaseKDBX?.let {
|
mDatabaseKDBX?.let {
|
||||||
|
|
||||||
val maxItems = historyMaxItems
|
val maxItems = historyMaxItems
|
||||||
if (maxItems >= 0) {
|
if (maxItems >= 0) {
|
||||||
while (entry.getHistory().size > maxItems) {
|
while (entry.getHistory().size > maxItems) {
|
||||||
entry.removeOldestEntryFromHistory()
|
removeOldestEntryHistory(entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -813,11 +878,10 @@ class Database {
|
|||||||
while (true) {
|
while (true) {
|
||||||
var historySize: Long = 0
|
var historySize: Long = 0
|
||||||
for (entryHistory in entry.getHistory()) {
|
for (entryHistory in entry.getHistory()) {
|
||||||
historySize += entryHistory.getSize()
|
historySize += entryHistory.getSize(binaryPool)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (historySize > maxSize) {
|
if (historySize > maxSize) {
|
||||||
entry.removeOldestEntryFromHistory()
|
removeOldestEntryHistory(entry)
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -826,6 +890,22 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun removeOldestEntryHistory(entry: Entry) {
|
||||||
|
entry.removeOldestEntryFromHistory()?.let {
|
||||||
|
it.getAttachments(binaryPool, false).forEach { attachmentToRemove ->
|
||||||
|
removeAttachmentIfNotUsed(attachmentToRemove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeEntryHistory(entry: Entry, entryHistoryPosition: Int) {
|
||||||
|
entry.removeEntryFromHistory(entryHistoryPosition)?.let {
|
||||||
|
it.getAttachments(binaryPool, false).forEach { attachmentToRemove ->
|
||||||
|
removeAttachmentIfNotUsed(attachmentToRemove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object : SingletonHolder<Database>(::Database) {
|
companion object : SingletonHolder<Database>(::Database) {
|
||||||
|
|
||||||
private val TAG = Database::class.java.name
|
private val TAG = Database::class.java.name
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import android.content.res.Resources
|
|||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.core.os.ConfigurationCompat
|
import androidx.core.os.ConfigurationCompat
|
||||||
|
import org.joda.time.Duration
|
||||||
|
import org.joda.time.Instant
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -95,6 +97,7 @@ class DateInstant : Parcelable {
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
val NEVER_EXPIRE = neverExpire
|
val NEVER_EXPIRE = neverExpire
|
||||||
|
val IN_ONE_MONTH = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate())
|
||||||
private val dateFormat = SimpleDateFormat.getDateTimeInstance()
|
private val dateFormat = SimpleDateFormat.getDateTimeInstance()
|
||||||
|
|
||||||
private val neverExpire: DateInstant
|
private val neverExpire: DateInstant
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ class DeletedObject {
|
|||||||
|
|
||||||
constructor()
|
constructor()
|
||||||
|
|
||||||
@JvmOverloads
|
|
||||||
constructor(uuid: UUID, deletionTime: Date = Date()) {
|
constructor(uuid: UUID, deletionTime: Date = Date()) {
|
||||||
this.uuid = uuid
|
this.uuid = uuid
|
||||||
this.mDeletionTime = deletionTime
|
this.mDeletionTime = deletionTime
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user