Compare commits
3594 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31444a823e | ||
|
|
068933f0fb | ||
|
|
f496711280 | ||
|
|
657d2420d6 | ||
|
|
2467721265 | ||
|
|
d37fbb9992 | ||
|
|
19b8b54dae | ||
|
|
84328caf3c | ||
|
|
a0bdfc973a | ||
|
|
1f91854490 | ||
|
|
1380325b66 | ||
|
|
d244eef62e | ||
|
|
8fb8d9ed37 | ||
|
|
8ce63cb5c5 | ||
|
|
9ecf2ae942 | ||
|
|
544f7003f6 | ||
|
|
6d633c9986 | ||
|
|
1e77a42c93 | ||
|
|
d1f2641e40 | ||
|
|
4b8ae154cc | ||
|
|
0c1aacdf83 | ||
|
|
5f34df3549 | ||
|
|
f2e6aa1abb | ||
|
|
866731df81 | ||
|
|
5d931e09d5 | ||
|
|
fe17c21c01 | ||
|
|
085941019e | ||
|
|
24b3758545 | ||
|
|
9083f99325 | ||
|
|
2189be9267 | ||
|
|
43218eede1 | ||
|
|
d1a176d27d | ||
|
|
cf51af91bf | ||
|
|
02ff1188b2 | ||
|
|
0fac9b6864 | ||
|
|
b550830c30 | ||
|
|
6f485dd298 | ||
|
|
b0dfde62c7 | ||
|
|
686dae0af6 | ||
|
|
ee3eabe8c8 | ||
|
|
521c8aa6a9 | ||
|
|
66207d599f | ||
|
|
e0029e0c3f | ||
|
|
3683b64721 | ||
|
|
8d4a0971f9 | ||
|
|
e4c3baa344 | ||
|
|
1e60d7e637 | ||
|
|
262b0227c1 | ||
|
|
226e461324 | ||
|
|
151eb26d56 | ||
|
|
335e767426 | ||
|
|
d212fa180b | ||
|
|
bc4ea2ec2a | ||
|
|
5d8c80fc1e | ||
|
|
a02714ff6e | ||
|
|
4bd952e223 | ||
|
|
91bbc6d84e | ||
|
|
6dbd16c5f6 | ||
|
|
76e040c585 | ||
|
|
8de6382a64 | ||
|
|
53532ead9f | ||
|
|
f4e6baeac2 | ||
|
|
5c46fdf41a | ||
|
|
22073e4bbd | ||
|
|
41e7376b7b | ||
|
|
3fc26c8c4e | ||
|
|
14f070a942 | ||
|
|
c078bd05e2 | ||
|
|
8ce9757b7c | ||
|
|
e028738dc2 | ||
|
|
9f4a302b72 | ||
|
|
2ef17e0c7a | ||
|
|
b86a8c8633 | ||
|
|
5a3be0853e | ||
|
|
99568db10c | ||
|
|
bf892f5b6a | ||
|
|
8e2c7ba1f0 | ||
|
|
fd3bb4b243 | ||
|
|
7f4a1d6896 | ||
|
|
d62734e8ac | ||
|
|
fbebc12a38 | ||
|
|
3c65be2a72 | ||
|
|
a29a9f28ef | ||
|
|
eb14dadb3c | ||
|
|
8d926a306b | ||
|
|
5699359099 | ||
|
|
e3176033dc | ||
|
|
9df6215c02 | ||
|
|
93a0e4c0a6 | ||
|
|
f55a824cdc | ||
|
|
766026d3be | ||
|
|
c64fc56496 | ||
|
|
6e2fb21431 | ||
|
|
2bb70abc39 | ||
|
|
a6cb1dbe5c | ||
|
|
5222a72cc6 | ||
|
|
5d3aa44545 | ||
|
|
61cfda93a5 | ||
|
|
b490295b90 | ||
|
|
61035ca47b | ||
|
|
1dc08bbfef | ||
|
|
9ea7c86da7 | ||
|
|
4fa3fb86cb | ||
|
|
df089f4415 | ||
|
|
6be12eb440 | ||
|
|
84efd1c497 | ||
|
|
4817654d58 | ||
|
|
70d45e0bba | ||
|
|
a4c7e3860b | ||
|
|
2a890091d7 | ||
|
|
cc593e6e1f | ||
|
|
552684fd90 | ||
|
|
a260e1d4e3 | ||
|
|
07bbf232b6 | ||
|
|
64b4a2f317 | ||
|
|
f0455fbca6 | ||
|
|
2fa973f34a | ||
|
|
470497b8ef | ||
|
|
215cc641c3 | ||
|
|
a98fa0d7bb | ||
|
|
c70841d3e5 | ||
|
|
3fdcd3c43d | ||
|
|
b32bb609de | ||
|
|
4db03fe034 | ||
|
|
68430ef62c | ||
|
|
d46f48f317 | ||
|
|
e68e4dac46 | ||
|
|
3a79f94698 | ||
|
|
b913913bf1 | ||
|
|
16f92fc895 | ||
|
|
189e98b3fe | ||
|
|
64fea078e8 | ||
|
|
4ad5d35be7 | ||
|
|
82015953b9 | ||
|
|
630318dbef | ||
|
|
d289e8acaa | ||
|
|
4632a202a9 | ||
|
|
4a9282ee53 | ||
|
|
45ec38ecab | ||
|
|
b82fc4659a | ||
|
|
aade780f92 | ||
|
|
6001f68cce | ||
|
|
d0a072554e | ||
|
|
53ccac0e9c | ||
|
|
14e2a3f79e | ||
|
|
9afe5aaaf4 | ||
|
|
b992c9eb65 | ||
|
|
fa2a79bc6c | ||
|
|
b1d4310b93 | ||
|
|
1811b02fc8 | ||
|
|
5bbf436e4e | ||
|
|
d47a12c3d9 | ||
|
|
dd7f6b389c | ||
|
|
e829e3eedf | ||
|
|
178de4dd99 | ||
|
|
a4cfc8ff49 | ||
|
|
b607cce691 | ||
|
|
5986f39041 | ||
|
|
8ab39aaea6 | ||
|
|
4114d5aeb1 | ||
|
|
2a1a5d9bd4 | ||
|
|
72b347bc85 | ||
|
|
44628f6fc4 | ||
|
|
9191fd4a21 | ||
|
|
02c4eba676 | ||
|
|
a45b5605a1 | ||
|
|
8570f47372 | ||
|
|
71fd17c0ac | ||
|
|
eb4e45cb69 | ||
|
|
3891bb1c8f | ||
|
|
a3c5237c2b | ||
|
|
e05b904e78 | ||
|
|
0423dd7295 | ||
|
|
8fa8043ff4 | ||
|
|
244174e494 | ||
|
|
db8920ae69 | ||
|
|
670e949e9f | ||
|
|
c4daafea6d | ||
|
|
c8f07ffa7c | ||
|
|
7f721427b6 | ||
|
|
ad670151c8 | ||
|
|
800335e416 | ||
|
|
2da58f5899 | ||
|
|
0850b19aec | ||
|
|
b6d32999b9 | ||
|
|
4f96a300c2 | ||
|
|
c630b9d0c0 | ||
|
|
7324f630ba | ||
|
|
519da9c4ef | ||
|
|
6fc26ef6e6 | ||
|
|
dfb7e6f3bd | ||
|
|
5a883b83a5 | ||
|
|
dfef51fd27 | ||
|
|
8bb7c7c8af | ||
|
|
98370a03b5 | ||
|
|
82875ff832 | ||
|
|
9b4b5914d6 | ||
|
|
7b9576841d | ||
|
|
59794557b3 | ||
|
|
c3e4504a1a | ||
|
|
65d7fdbf7e | ||
|
|
d5310e5c58 | ||
|
|
f02b7c16d2 | ||
|
|
7cd456ecbc | ||
|
|
1aeff9684e | ||
|
|
4b0ad75e7b | ||
|
|
055b3c6002 | ||
|
|
9ad4858224 | ||
|
|
33422f90ce | ||
|
|
b9f4f9380b | ||
|
|
56a1601d79 | ||
|
|
a7d04e573a | ||
|
|
0c4d827469 | ||
|
|
5656f6afb5 | ||
|
|
4bd12b8a95 | ||
|
|
939b319563 | ||
|
|
05865fe8c3 | ||
|
|
09cf8aabf6 | ||
|
|
111a77d984 | ||
|
|
c2cc0a77ab | ||
|
|
5b052b02a2 | ||
|
|
3b3908dd7c | ||
|
|
1b96747187 | ||
|
|
13f2003fed | ||
|
|
4d6dbc9e30 | ||
|
|
a235b21fbf | ||
|
|
158ed26f6f | ||
|
|
81930bf3a1 | ||
|
|
e1ecebb4a0 | ||
|
|
52fd576f55 | ||
|
|
6742a8893c | ||
|
|
7db0c4f7d3 | ||
|
|
372b2aebe0 | ||
|
|
b713237ec3 | ||
|
|
ee1a0a53a6 | ||
|
|
45ece887a2 | ||
|
|
0f59aaf4f7 | ||
|
|
0d2c814b3d | ||
|
|
64bdcc2f32 | ||
|
|
dfb418c12a | ||
|
|
c8868f31e6 | ||
|
|
48cc8b3f75 | ||
|
|
62777373a4 | ||
|
|
ce987237ae | ||
|
|
77b99f9ef4 | ||
|
|
049ac4cb21 | ||
|
|
41e3c60632 | ||
|
|
5b2c846f7d | ||
|
|
8a0192bcac | ||
|
|
3d2fce78c6 | ||
|
|
120bfd47e2 | ||
|
|
408ac08059 | ||
|
|
9ae2377bd9 | ||
|
|
f126a9ab09 | ||
|
|
c2d4bf67d0 | ||
|
|
d0367ea7f8 | ||
|
|
73c6c97637 | ||
|
|
3f63fa9c30 | ||
|
|
2884d12b4b | ||
|
|
24dd966eab | ||
|
|
47b2464297 | ||
|
|
08bb11a4d7 | ||
|
|
e0c2c14b98 | ||
|
|
481b818619 | ||
|
|
138efebd8c | ||
|
|
3251f02ab2 | ||
|
|
8f74cdfac0 | ||
|
|
75f795b6ab | ||
|
|
dd901f3b16 | ||
|
|
cf2d641303 | ||
|
|
489c820ab9 | ||
|
|
dd21382b29 | ||
|
|
05b2ccf0ed | ||
|
|
428efd2bb6 | ||
|
|
512b4cd16e | ||
|
|
4d648b77b9 | ||
|
|
02f6caf4dd | ||
|
|
2b23649674 | ||
|
|
1693be7776 | ||
|
|
c97be80a3a | ||
|
|
29d4d5232d | ||
|
|
90ee9d8c40 | ||
|
|
5c0a82ba38 | ||
|
|
13aee1072f | ||
|
|
b158cf1323 | ||
|
|
22be5ecbca | ||
|
|
4b16d9394a | ||
|
|
c0ad54219f | ||
|
|
4af6f776b0 | ||
|
|
76373658d8 | ||
|
|
f9f6e69a95 | ||
|
|
61354df795 | ||
|
|
3a5c42b138 | ||
|
|
55e7544739 | ||
|
|
ad0dcdd54a | ||
|
|
caa208c847 | ||
|
|
067d00376d | ||
|
|
07f0521ef9 | ||
|
|
3be3593b1b | ||
|
|
71db66d174 | ||
|
|
cd58a8e1b5 | ||
|
|
256a579b1c | ||
|
|
97a427ea24 | ||
|
|
b848587901 | ||
|
|
fd90af3757 | ||
|
|
cb1c9e1103 | ||
|
|
c14092d52b | ||
|
|
27c19bd708 | ||
|
|
13d5273314 | ||
|
|
a0f93b5441 | ||
|
|
4c4c92527e | ||
|
|
ff9999f39c | ||
|
|
45d06f269c | ||
|
|
acf92c0f8e | ||
|
|
4b7de3b85f | ||
|
|
0502bfb986 | ||
|
|
eacecd9b0d | ||
|
|
38245b4807 | ||
|
|
95228e67c2 | ||
|
|
33cb4a55f5 | ||
|
|
7beb13d859 | ||
|
|
f0e0d26923 | ||
|
|
5326322356 | ||
|
|
ebe1c0bcdd | ||
|
|
42100d4cb9 | ||
|
|
369f3d240e | ||
|
|
d40e83fb52 | ||
|
|
6776b0867f | ||
|
|
5b1e945883 | ||
|
|
2f2f5e6f43 | ||
|
|
dfc7654842 | ||
|
|
183015707f | ||
|
|
2bc4908452 | ||
|
|
213e5c40d7 | ||
|
|
e7cce21c51 | ||
|
|
128ce5657e | ||
|
|
861911ad63 | ||
|
|
9093c65235 | ||
|
|
e6ab8f82ff | ||
|
|
77ff1850f3 | ||
|
|
e985bd2a20 | ||
|
|
a1e91bb26f | ||
|
|
a6cd02d146 | ||
|
|
eb51f6712b | ||
|
|
0a19ecb715 | ||
|
|
d7e7020244 | ||
|
|
ea3349eea4 | ||
|
|
9cec64ded4 | ||
|
|
f9dc456032 | ||
|
|
6416aad823 | ||
|
|
10b5c9c261 | ||
|
|
bc0e364164 | ||
|
|
83ab5223a8 | ||
|
|
94c6710f22 | ||
|
|
a1e5266161 | ||
|
|
adbdb9a642 | ||
|
|
527f734fcf | ||
|
|
f1a29af0c6 | ||
|
|
b69a277769 | ||
|
|
d049ed39e8 | ||
|
|
ecfe767068 | ||
|
|
8d827eb562 | ||
|
|
9048f618c5 | ||
|
|
66377ded62 | ||
|
|
04c4c82953 | ||
|
|
ccd8467cba | ||
|
|
4eee9e95c4 | ||
|
|
c64b4b8d62 | ||
|
|
72ef0f2f3f | ||
|
|
9b5a95a10f | ||
|
|
1a3baa6523 | ||
|
|
a745ed1c28 | ||
|
|
39ff112b42 | ||
|
|
b3aab27c9b | ||
|
|
9b3c751a49 | ||
|
|
d29f4e0097 | ||
|
|
0645fbe938 | ||
|
|
ee6c8fc041 | ||
|
|
d93e3a1c2d | ||
|
|
d4e0c008b8 | ||
|
|
0ca392f312 | ||
|
|
c6c14c2354 | ||
|
|
652616226b | ||
|
|
d2101fd3e5 | ||
|
|
3b967dd4e3 | ||
|
|
e5d58721dd | ||
|
|
1495c442ac | ||
|
|
2206184bcb | ||
|
|
9e0b6fa800 | ||
|
|
b11533f9fe | ||
|
|
50343193e1 | ||
|
|
7d7e3f4ad6 | ||
|
|
e22d9f6bdf | ||
|
|
affcc28f13 | ||
|
|
7a151bc2fe | ||
|
|
5349c4783e | ||
|
|
96a72d9842 | ||
|
|
04eae1ae3d | ||
|
|
67afa55f1d | ||
|
|
efa8fd9f17 | ||
|
|
64128991a6 | ||
|
|
3fa38c29f6 | ||
|
|
86a335768c | ||
|
|
2d29092c52 | ||
|
|
2d7e76a279 | ||
|
|
1514dbb1de | ||
|
|
1707d3c3ba | ||
|
|
3fee162c4d | ||
|
|
78fd9d616b | ||
|
|
46828c3317 | ||
|
|
5126bd4fb6 | ||
|
|
6440e5e054 | ||
|
|
15b84739e7 | ||
|
|
4bfe296e1a | ||
|
|
3fcc0db4f8 | ||
|
|
a8238565f3 | ||
|
|
53114462b3 | ||
|
|
094da79cea | ||
|
|
f093206a1c | ||
|
|
5b4940d017 | ||
|
|
aee36eeec6 | ||
|
|
cc783f8be1 | ||
|
|
a20f491cf1 | ||
|
|
f5ff9bf263 | ||
|
|
dcba54b499 | ||
|
|
d1e103c1d7 | ||
|
|
542daba206 | ||
|
|
94b61b1bbd | ||
|
|
718e590bfd | ||
|
|
9ce5e184c0 | ||
|
|
dbc477765f | ||
|
|
92fcadf3f3 | ||
|
|
c437fd96a8 | ||
|
|
10d33ecb82 | ||
|
|
d506c0cb27 | ||
|
|
acac7f7540 | ||
|
|
c853bd282a | ||
|
|
ecc4550261 | ||
|
|
c26ece7166 | ||
|
|
88b50c7902 | ||
|
|
7ba2cdd6ff | ||
|
|
589d9a2f1d | ||
|
|
a92411b95b | ||
|
|
472051bd24 | ||
|
|
e5109a1f43 | ||
|
|
f7ae9e3574 | ||
|
|
170ec3c636 | ||
|
|
1ab3fa8b3b | ||
|
|
8b046512e3 | ||
|
|
228a10c8e0 | ||
|
|
9c53bea190 | ||
|
|
11cf991498 | ||
|
|
a88c3721b2 | ||
|
|
0b4b6d4d91 | ||
|
|
941f9bcd48 | ||
|
|
09988a858d | ||
|
|
f1bf9fb25c | ||
|
|
1751fa49c0 | ||
|
|
6b4fc9a4fa | ||
|
|
7c8d85e428 | ||
|
|
e335140f23 | ||
|
|
d85f398b5f | ||
|
|
a16082a59d | ||
|
|
456269a343 | ||
|
|
eb8e1e20eb | ||
|
|
ed3c84fec0 | ||
|
|
be40416a2d | ||
|
|
5b5476a513 | ||
|
|
dc64dd6400 | ||
|
|
eca02d3bde | ||
|
|
176b6c2936 | ||
|
|
5b22350bdf | ||
|
|
6e1e011234 | ||
|
|
ac65ef6a5c | ||
|
|
fc198dde74 | ||
|
|
15ac51b2fc | ||
|
|
34214432e1 | ||
|
|
361ca92493 | ||
|
|
e367051b80 | ||
|
|
a2a4a50c5e | ||
|
|
afc74b2f2a | ||
|
|
fc756d1eaf | ||
|
|
eb8a4b1e49 | ||
|
|
8d258b3538 | ||
|
|
a59cfa3477 | ||
|
|
f1e513006e | ||
|
|
9df5c8f439 | ||
|
|
3ae099accf | ||
|
|
bb3e9396f2 | ||
|
|
1628749bde | ||
|
|
f2006b5e42 | ||
|
|
80d387d9e7 | ||
|
|
4452b4d599 | ||
|
|
dfeaeb9888 | ||
|
|
7e45a20ee7 | ||
|
|
f3fe92e4de | ||
|
|
b606909c65 | ||
|
|
2882bb30d7 | ||
|
|
5b62227e3f | ||
|
|
8b6af6fd8a | ||
|
|
99e9a92953 | ||
|
|
9f626309c3 | ||
|
|
3fe7cf2bfd | ||
|
|
9b5c274b49 | ||
|
|
46b350e7ac | ||
|
|
22a4aeb108 | ||
|
|
332e116ba7 | ||
|
|
8b594a1a1f | ||
|
|
ab23ec6d4d | ||
|
|
0ef574d675 | ||
|
|
6d15a2462d | ||
|
|
24fcdeb7aa | ||
|
|
13905db732 | ||
|
|
6e1c8e5bec | ||
|
|
9aa1d11b94 | ||
|
|
6c9f359fae | ||
|
|
531ebcae85 | ||
|
|
fe9601b510 | ||
|
|
bdf7cc6ea0 | ||
|
|
1cfe02af6f | ||
|
|
647e3f9383 | ||
|
|
597f52799d | ||
|
|
a59e052ed8 | ||
|
|
11da0a4500 | ||
|
|
fd736bd1c2 | ||
|
|
ca6a4bfeef | ||
|
|
02e9debc42 | ||
|
|
9bc9bd8b95 | ||
|
|
d810f79b7a | ||
|
|
f3468951f1 | ||
|
|
7ec5badabb | ||
|
|
1ff2f501ca | ||
|
|
cfcb49e233 | ||
|
|
467df2020e | ||
|
|
a961b41de0 | ||
|
|
40e8d5225e | ||
|
|
bc755ae1df | ||
|
|
b1cb0c3786 | ||
|
|
090d0fa2db | ||
|
|
27918a12b0 | ||
|
|
ba1498b0b2 | ||
|
|
cbde96dd82 | ||
|
|
344118a755 | ||
|
|
259c8a4bd9 | ||
|
|
fe92e41e91 | ||
|
|
e58c2f2a99 | ||
|
|
f4d5bd1bea | ||
|
|
20b352cabe | ||
|
|
20e35f1a69 | ||
|
|
d963f56d0f | ||
|
|
aecfbc7728 | ||
|
|
5734df89f0 | ||
|
|
bdf9b864d4 | ||
|
|
1c0f1a036b | ||
|
|
327c9de464 | ||
|
|
8b2f994769 | ||
|
|
a5e53d872b | ||
|
|
1868d90693 | ||
|
|
da0c19e068 | ||
|
|
d1103d8db4 | ||
|
|
b2e92646a1 | ||
|
|
19bc2444bc | ||
|
|
831b649cbb | ||
|
|
ded3c204b9 | ||
|
|
23eec5f066 | ||
|
|
6c167090e1 | ||
|
|
7d9eca0d46 | ||
|
|
c551aff474 | ||
|
|
e627745358 | ||
|
|
5a30d9d2b5 | ||
|
|
0a46817bbc | ||
|
|
a4134fa8c8 | ||
|
|
683535a5a6 | ||
|
|
edb53112c2 | ||
|
|
83a77af520 | ||
|
|
df3ae17c7b | ||
|
|
4a1624a443 | ||
|
|
a8de9f9f9f | ||
|
|
3aa5b40acd | ||
|
|
8400f3e874 | ||
|
|
b40bca1913 | ||
|
|
7100257f31 | ||
|
|
17df1a4d8a | ||
|
|
d7a5209c68 | ||
|
|
076220eacd | ||
|
|
99a50f271a | ||
|
|
63d265da06 | ||
|
|
30e3624eb1 | ||
|
|
88f3713e28 | ||
|
|
90f0c22545 | ||
|
|
8deed8468d | ||
|
|
923ad26b1b | ||
|
|
3bc858e4c2 | ||
|
|
f5a7fa41a7 | ||
|
|
bf71d5508b | ||
|
|
b44c9cfc51 | ||
|
|
5b4338abae | ||
|
|
aa5adc28cb | ||
|
|
2dad013cc0 | ||
|
|
7ade66f3ac | ||
|
|
ed75a64b46 | ||
|
|
e156b80d91 | ||
|
|
e8f79ae467 | ||
|
|
90e4862280 | ||
|
|
438080d3d6 | ||
|
|
3c17605764 | ||
|
|
3f68bc0eda | ||
|
|
ecbee73eae | ||
|
|
3e4452da00 | ||
|
|
549c690b56 | ||
|
|
aabe06f29b | ||
|
|
82693c5cd3 | ||
|
|
37a4f26d2f | ||
|
|
ca94063c7b | ||
|
|
eadc4bf6c2 | ||
|
|
b1c307c86b | ||
|
|
1874a0056d | ||
|
|
48331f9552 | ||
|
|
f907aa578a | ||
|
|
41e2620cc1 | ||
|
|
e7a82b167a | ||
|
|
088c556b00 | ||
|
|
c80343b6d4 | ||
|
|
4e52a8cf60 | ||
|
|
1ed1d4233f | ||
|
|
6e4626bc02 | ||
|
|
2608ae247f | ||
|
|
785586bfe9 | ||
|
|
bdcbb177ae | ||
|
|
15ac365d79 | ||
|
|
debbcb753b | ||
|
|
69d73aeaa4 | ||
|
|
dffe53370f | ||
|
|
4334e6dcdf | ||
|
|
c2c6c093d5 | ||
|
|
77e539eec2 | ||
|
|
a57970210e | ||
|
|
1b31a46fb7 | ||
|
|
87f19c74fc | ||
|
|
bd157a9724 | ||
|
|
5a327eb0db | ||
|
|
4b9c0b0109 | ||
|
|
df6b75cdbb | ||
|
|
0b4f8c122b | ||
|
|
2a87eaf3e5 | ||
|
|
c52266f5cf | ||
|
|
3b21f8add2 | ||
|
|
6574bd10a0 | ||
|
|
23f3335988 | ||
|
|
a5d7f33c82 | ||
|
|
3782c4dac0 | ||
|
|
1fc02fd2fe | ||
|
|
cc347c1dbe | ||
|
|
79ff20eb18 | ||
|
|
e6e8a447da | ||
|
|
233f0c5bdb | ||
|
|
9ed4271a14 | ||
|
|
470c0b6b43 | ||
|
|
afa8ae42b9 | ||
|
|
63d426503f | ||
|
|
ffb7f80b26 | ||
|
|
63f8826fd8 | ||
|
|
ef836e8b84 | ||
|
|
abc1c43a51 | ||
|
|
6b54dd9e0d | ||
|
|
1f54e7752d | ||
|
|
6ac941f276 | ||
|
|
a4fe92562f | ||
|
|
b9bd1d9d4b | ||
|
|
3b6c28488a | ||
|
|
875eb3500d | ||
|
|
3a88a2451c | ||
|
|
6800b73a4f | ||
|
|
983404e6d8 | ||
|
|
b95c0a18a7 | ||
|
|
36b317cad8 | ||
|
|
35d74888fb | ||
|
|
6c308483f7 | ||
|
|
9d25fb74ec | ||
|
|
d217b52744 | ||
|
|
319da4b174 | ||
|
|
9bee467942 | ||
|
|
44ac70fc97 | ||
|
|
d897611d62 | ||
|
|
c2ae251e73 | ||
|
|
35ad285864 | ||
|
|
97bdae21eb | ||
|
|
d6dc6e43c7 | ||
|
|
01e6e530d5 | ||
|
|
9ec0178beb | ||
|
|
d66f2f6d24 | ||
|
|
79cd4004cc | ||
|
|
991243e2df | ||
|
|
b91cf11d86 | ||
|
|
d182ec09fa | ||
|
|
8641822358 | ||
|
|
9665cbb428 | ||
|
|
a280dfaf3b | ||
|
|
3e56521ea8 | ||
|
|
b205230ea9 | ||
|
|
51645ab126 | ||
|
|
5d04897e75 | ||
|
|
1ac0ea5cc6 | ||
|
|
a07e8b51e5 | ||
|
|
a81f0238f4 | ||
|
|
2b81eb8ec7 | ||
|
|
e5eb642781 | ||
|
|
a4cbe25733 | ||
|
|
2042c85b22 | ||
|
|
3149f8745c | ||
|
|
15b9f1616f | ||
|
|
94c02b7288 | ||
|
|
7e70b59a59 | ||
|
|
2c7f5e41ed | ||
|
|
108b8df280 | ||
|
|
553098f9be | ||
|
|
131eb78407 | ||
|
|
f956a279a5 | ||
|
|
7150686b92 | ||
|
|
4b1fb2c173 | ||
|
|
94464bf608 | ||
|
|
2faa88784a | ||
|
|
e6607b53d8 | ||
|
|
3f6a6c864a | ||
|
|
30a578257d | ||
|
|
8411134adf | ||
|
|
f86a5d1a19 | ||
|
|
be72492537 | ||
|
|
76f9e8ec6e | ||
|
|
8fb1c44e58 | ||
|
|
f607b35cf3 | ||
|
|
0e56bec35a | ||
|
|
c890d10114 | ||
|
|
dee2fe5ce7 | ||
|
|
4a4d767bce | ||
|
|
d57e0cf601 | ||
|
|
7fa141dd1b | ||
|
|
c261a0cbca | ||
|
|
66661cbd49 | ||
|
|
100c126c3d | ||
|
|
d466e3077d | ||
|
|
24587dc34e | ||
|
|
32cc57dd03 | ||
|
|
a55488846b | ||
|
|
dcf61fd4e2 | ||
|
|
5bf998468a | ||
|
|
01c9625c59 | ||
|
|
772c378922 | ||
|
|
ee50a91379 | ||
|
|
9cfda3bad8 | ||
|
|
aa19b08bd9 | ||
|
|
87f69bb7e2 | ||
|
|
41c0aeedbe | ||
|
|
3cbe53d76f | ||
|
|
aed60d6c1e | ||
|
|
be7d35490d | ||
|
|
d0ea997c63 | ||
|
|
1fecffeba2 | ||
|
|
76319a56a2 | ||
|
|
1d71de7031 | ||
|
|
e9a1cfea11 | ||
|
|
9115856d19 | ||
|
|
8c45266c18 | ||
|
|
6b4130df89 | ||
|
|
e3e10e7dfa | ||
|
|
cbf900004d | ||
|
|
51b36dc460 | ||
|
|
9f8016afe2 | ||
|
|
d5a36db50a | ||
|
|
ecd458d8d0 | ||
|
|
9088297c41 | ||
|
|
5282deb088 | ||
|
|
75d661f12b | ||
|
|
83bc769d9e | ||
|
|
8324acadc8 | ||
|
|
6e42db41be | ||
|
|
3917bfc9e6 | ||
|
|
d11febb1ce | ||
|
|
360eb6f9cc | ||
|
|
e4ac5d01d0 | ||
|
|
6a51fc0668 | ||
|
|
ba03f07fbe | ||
|
|
7bf7d63f64 | ||
|
|
d3efaabc24 | ||
|
|
b4283ed98b | ||
|
|
de407e4cf9 | ||
|
|
60ed3a9836 | ||
|
|
7948358d85 | ||
|
|
96b82bb9b2 | ||
|
|
699ccf13f0 | ||
|
|
ae88aa4e42 | ||
|
|
bcce13b12f | ||
|
|
4fd4f660a7 | ||
|
|
518e59b33c | ||
|
|
165cdcc00d | ||
|
|
48c2115fdf | ||
|
|
d7d68ccdeb | ||
|
|
5cf6362db4 | ||
|
|
4efcc48160 | ||
|
|
383274ce0f | ||
|
|
c9dec3a2f7 | ||
|
|
2d4bf2903b | ||
|
|
2b88cfbda0 | ||
|
|
eb6ab7a156 | ||
|
|
9bed7bf213 | ||
|
|
99c2796014 | ||
|
|
9ee9bf12ae | ||
|
|
688cbe50f2 | ||
|
|
e0577d1628 | ||
|
|
de70925f8a | ||
|
|
0f8dd17fde | ||
|
|
4bc8a08606 | ||
|
|
cf34433186 | ||
|
|
21113d6fc8 | ||
|
|
d7decba3f5 | ||
|
|
671364f395 | ||
|
|
d0072237e7 | ||
|
|
d11c001e0d | ||
|
|
a197453ce0 | ||
|
|
d1586a8d80 | ||
|
|
6e959e415f | ||
|
|
8a8e14701b | ||
|
|
60bb144020 | ||
|
|
b561db3a67 | ||
|
|
b0e722acce | ||
|
|
279bd16b74 | ||
|
|
2e0081b66c | ||
|
|
f79d32b22b | ||
|
|
3579606d1e | ||
|
|
b93c6266a6 | ||
|
|
ea9c530667 | ||
|
|
6ea95c050a | ||
|
|
5a99a28195 | ||
|
|
0cd5351c07 | ||
|
|
148bed801b | ||
|
|
87a1a3ba1b | ||
|
|
20d2c10bba | ||
|
|
3eed5e395e | ||
|
|
870fbaa05c | ||
|
|
55868f68da | ||
|
|
e6f0fbeab5 | ||
|
|
8a554349b5 | ||
|
|
ab87d4e564 | ||
|
|
8400a83b70 | ||
|
|
5d2caa37a9 | ||
|
|
a6b7cfc2d3 | ||
|
|
74107b90bb | ||
|
|
9f4a915d43 | ||
|
|
3f1f22e1c3 | ||
|
|
1d98eb2b95 | ||
|
|
f1b56fed16 | ||
|
|
23e75c56ed | ||
|
|
eafc9cc1fa | ||
|
|
3e3dbb22df | ||
|
|
127b8ba0bd | ||
|
|
8a8e588a01 | ||
|
|
3c1b010821 | ||
|
|
739c938576 | ||
|
|
69fed0c347 | ||
|
|
d040258296 | ||
|
|
abee18839b | ||
|
|
9c0eb4e27e | ||
|
|
10598ef5e5 | ||
|
|
fa09e2d21d | ||
|
|
11ddfe3445 | ||
|
|
9abbc8876b | ||
|
|
c5cf99e13d | ||
|
|
82ac1c8f5e | ||
|
|
837f8146e4 | ||
|
|
9698c3f2ee | ||
|
|
85290b8f9d | ||
|
|
1ccf48d7a5 | ||
|
|
2cbec9f2b6 | ||
|
|
692be415fa | ||
|
|
83d5218f92 | ||
|
|
324b016324 | ||
|
|
005c9673f7 | ||
|
|
b8762dc6e0 | ||
|
|
93abff0768 | ||
|
|
ac1cfb43d8 | ||
|
|
30ec712782 | ||
|
|
bbd93fbc5b | ||
|
|
c46cbaff4b | ||
|
|
1e979b256a | ||
|
|
50429b419f | ||
|
|
2c540be5a3 | ||
|
|
155d3d222f | ||
|
|
3fc3bf1302 | ||
|
|
f0fafbbc6e | ||
|
|
5f7d980d89 | ||
|
|
c3799ed7fe | ||
|
|
f7533af5d8 | ||
|
|
2c9504fb93 | ||
|
|
c22b3f5335 | ||
|
|
2aea7fd46d | ||
|
|
bb9dda0cf1 | ||
|
|
ced6f05e59 | ||
|
|
cc14b5d4d4 | ||
|
|
2922f7110d | ||
|
|
762a877c5e | ||
|
|
aace7a8260 | ||
|
|
ac91abcb45 | ||
|
|
5d6ccd84c9 | ||
|
|
bd2f980f8e | ||
|
|
69af330ba1 | ||
|
|
b7bc3bfa8f | ||
|
|
678663ad66 | ||
|
|
ec3aa14112 | ||
|
|
3cdd98dc1a | ||
|
|
fc3270efd2 | ||
|
|
0dd871d3b5 | ||
|
|
41108ff407 | ||
|
|
99de8cf220 | ||
|
|
a60e887eb3 | ||
|
|
5133cb5c8f | ||
|
|
1607d84c21 | ||
|
|
cc0a990af8 | ||
|
|
fec5bdcfc8 | ||
|
|
648514d150 | ||
|
|
37b9745b6b | ||
|
|
f858b1144c | ||
|
|
c7cd7b83fa | ||
|
|
3cef086b69 | ||
|
|
ca51f591de | ||
|
|
da63404a84 | ||
|
|
de2160f992 | ||
|
|
8b7bb36e66 | ||
|
|
6ade91dafa | ||
|
|
2ef66f3011 | ||
|
|
5b2fd0bfbb | ||
|
|
ab392e2cf3 | ||
|
|
8eb922e93f | ||
|
|
20900dce07 | ||
|
|
c2705ff5d2 | ||
|
|
8af70fa7b5 | ||
|
|
e7eb8099ac | ||
|
|
cedf2eafbb | ||
|
|
e7553c68a0 | ||
|
|
cea7f1c2d1 | ||
|
|
45937f9c9c | ||
|
|
ee2a1ea924 | ||
|
|
1851c205e9 | ||
|
|
c4a3947cb3 | ||
|
|
d3e76bcf21 | ||
|
|
cf4e64d6c5 | ||
|
|
4cac571f1b | ||
|
|
445ed92ff7 | ||
|
|
6686ce15c1 | ||
|
|
59f134e0cd | ||
|
|
aab3f8c56f | ||
|
|
17c26e2a96 | ||
|
|
aec9124ef5 | ||
|
|
2df1e9bc2e | ||
|
|
6ceecbfd0f | ||
|
|
460726cadb | ||
|
|
32baa7b9f1 | ||
|
|
f44e648ccc | ||
|
|
2ad385acb6 | ||
|
|
ebaec2eaf0 | ||
|
|
dac8c2c15d | ||
|
|
60965491b0 | ||
|
|
19a3faa85a | ||
|
|
3779346eed | ||
|
|
185e263ddd | ||
|
|
8c0d984955 | ||
|
|
8d1012eda0 | ||
|
|
03f147e3d0 | ||
|
|
13ef5d640c | ||
|
|
2e8eed5afe | ||
|
|
54653a5bea | ||
|
|
e29082eba3 | ||
|
|
b635e9bb0d | ||
|
|
179bfdc3d2 | ||
|
|
631cb104cd | ||
|
|
1aeaf6855e | ||
|
|
6bac1ace30 | ||
|
|
488bab7c4d | ||
|
|
69fbfd7723 | ||
|
|
7697713c44 | ||
|
|
ee9b072f45 | ||
|
|
741defd31e | ||
|
|
e259b37f74 | ||
|
|
9a87de797c | ||
|
|
5d44ba658e | ||
|
|
00518b8231 | ||
|
|
6a3a99d2ac | ||
|
|
819434968c | ||
|
|
9f55ca2fdb | ||
|
|
7f69563edb | ||
|
|
b5cf0f987e | ||
|
|
28ad0b39c3 | ||
|
|
b4e9040d5c | ||
|
|
18a6ff0aa5 | ||
|
|
66e8c25265 | ||
|
|
aba1f2d35b | ||
|
|
5f29bcea8f | ||
|
|
fac6fd7926 | ||
|
|
5f4ab201af | ||
|
|
9591d36f9d | ||
|
|
a38f34995d | ||
|
|
f72564e48d | ||
|
|
39ae743c64 | ||
|
|
9cdb355878 | ||
|
|
6bdabbc96b | ||
|
|
d9e7d5ff6f | ||
|
|
9f41da7868 | ||
|
|
d4f7258ed1 | ||
|
|
aa34d78052 | ||
|
|
72712e8e0e | ||
|
|
bf25054ef2 | ||
|
|
32efafb404 | ||
|
|
3748ba1afa | ||
|
|
5baf91dc77 | ||
|
|
a152a48402 | ||
|
|
5fb4c4c20c | ||
|
|
80cf4f05f8 | ||
|
|
ce8e532f61 | ||
|
|
7e04fcbb6c | ||
|
|
71c37d0b8b | ||
|
|
518375e29a | ||
|
|
25c845c1c7 | ||
|
|
6a468b339f | ||
|
|
05a52dd482 | ||
|
|
5f1413ea1f | ||
|
|
84e45482a4 | ||
|
|
32bfdca562 | ||
|
|
547971545e | ||
|
|
c12b16faf9 | ||
|
|
858d6c8723 | ||
|
|
0f021fae9f | ||
|
|
5cb42264c5 | ||
|
|
c17af4098f | ||
|
|
e6c094b433 | ||
|
|
b15a86618b | ||
|
|
2e66cee551 | ||
|
|
c6e3d0125a | ||
|
|
f60f7d32dc | ||
|
|
dd7175d0c6 | ||
|
|
ba0e87f32a | ||
|
|
cebba5a09d | ||
|
|
7b0c31c641 | ||
|
|
487be74e83 | ||
|
|
3c7c88e0f5 | ||
|
|
e876e89b41 | ||
|
|
065fd9632c | ||
|
|
f7d8641e31 | ||
|
|
ed6456599b | ||
|
|
649538846a | ||
|
|
7468db5269 | ||
|
|
8291e94de4 | ||
|
|
94d30fc7ec | ||
|
|
36c6f371c5 | ||
|
|
94b3f66e14 | ||
|
|
c9527ddacd | ||
|
|
805c728e92 | ||
|
|
7812a86adb | ||
|
|
d22defcd83 | ||
|
|
d06829ff7b | ||
|
|
9110a1aca6 | ||
|
|
d7c90601d0 | ||
|
|
7b5fc600f5 | ||
|
|
0e699a1918 | ||
|
|
49f9964bc7 | ||
|
|
ee3ec4b14d | ||
|
|
cdcc9f0aff | ||
|
|
d56a01998f | ||
|
|
4e7f7f7fa0 | ||
|
|
201d8f8aee | ||
|
|
3847cb4d2d | ||
|
|
89a338ac33 | ||
|
|
0084d113d3 | ||
|
|
6d49cc3577 | ||
|
|
910c45a6be | ||
|
|
1d16ad764f | ||
|
|
142021c849 | ||
|
|
c87b9768b1 | ||
|
|
fbc3c1a9a4 | ||
|
|
05d1656a9e | ||
|
|
98804db478 | ||
|
|
c90f18c45a | ||
|
|
553783bfce | ||
|
|
fbf67f28f5 | ||
|
|
b8005466cd | ||
|
|
82177a386e | ||
|
|
176b276835 | ||
|
|
a4732194ef | ||
|
|
4a815daf4d | ||
|
|
8aa7804e8d | ||
|
|
3b7086627d | ||
|
|
20fb4036d1 | ||
|
|
3679ad3d70 | ||
|
|
63f23c571f | ||
|
|
7e96303c7d | ||
|
|
4f5879179e | ||
|
|
5a81f54a1b | ||
|
|
d8ccaf3578 | ||
|
|
7d0848f22b | ||
|
|
b02cffab56 | ||
|
|
59b6deb7be | ||
|
|
1b734051d5 | ||
|
|
037fe41961 | ||
|
|
7bf1263c04 | ||
|
|
0e4afd4681 | ||
|
|
21930021ed | ||
|
|
748b5c54c9 | ||
|
|
82d305008b | ||
|
|
1f24955533 | ||
|
|
ccfa28b895 | ||
|
|
2e3562d87e | ||
|
|
f1be109c15 | ||
|
|
b09bd39ad4 | ||
|
|
d812c3d61b | ||
|
|
0d7772a4c6 | ||
|
|
a5abf4d186 | ||
|
|
c938a13482 | ||
|
|
0bfc395986 | ||
|
|
60c536f444 | ||
|
|
8129e6e0c1 | ||
|
|
223a8e9a5e | ||
|
|
0e21c75007 | ||
|
|
6ca031e8fd | ||
|
|
f02e86fb50 | ||
|
|
b17f8244da | ||
|
|
4c02ce138b | ||
|
|
5eba208b00 | ||
|
|
2a4fbbfb35 | ||
|
|
5dd8bdcae6 | ||
|
|
c3e82eea5d | ||
|
|
2cfb96be33 | ||
|
|
715eedfab1 | ||
|
|
7f3a1c9a7d | ||
|
|
8413d2b31a | ||
|
|
04a03da382 | ||
|
|
e3274657ea | ||
|
|
f3b25cb792 | ||
|
|
d181f886fe | ||
|
|
616d073395 | ||
|
|
d36fc19585 | ||
|
|
95d9e07e2f | ||
|
|
25077a7b9a | ||
|
|
6971cd1a6b | ||
|
|
1e43a65743 | ||
|
|
2f2cbf343e | ||
|
|
3426b3cdeb | ||
|
|
e1d4c172f4 | ||
|
|
4c4a67afaf | ||
|
|
d33f210940 | ||
|
|
ead59bd410 | ||
|
|
f9445de71f | ||
|
|
c26995779a | ||
|
|
c6277453a3 | ||
|
|
91ebf2ba6f | ||
|
|
0f87bdd5a3 | ||
|
|
9e97042dd1 | ||
|
|
b7c4a99e71 | ||
|
|
48e315453e | ||
|
|
a8a6d14ca3 | ||
|
|
e895dd3430 | ||
|
|
f59859137a | ||
|
|
dee92e9e40 | ||
|
|
6701f4f95e | ||
|
|
e20f769854 | ||
|
|
4f762a9432 | ||
|
|
3c49eb1635 | ||
|
|
bdc6a282e2 | ||
|
|
8392ab2cc4 | ||
|
|
93c7c09f8c | ||
|
|
120116414f | ||
|
|
11794e5819 | ||
|
|
edce3d7bec | ||
|
|
e133e32e7c | ||
|
|
471859e448 | ||
|
|
d8de66eb14 | ||
|
|
cfdc0237d7 | ||
|
|
05fad24eda | ||
|
|
d4818c5567 | ||
|
|
8e8e6a7b93 | ||
|
|
6547f0ffad | ||
|
|
6f172fffa8 | ||
|
|
ed16e06676 | ||
|
|
1874f06f42 | ||
|
|
e9db24429a | ||
|
|
a59f4d45ca | ||
|
|
c7b3e0926c | ||
|
|
f0f5258bc9 | ||
|
|
12c07cf793 | ||
|
|
d2b8c85015 | ||
|
|
b9652291bd | ||
|
|
b0d1f93bfc | ||
|
|
a6d6c247a8 | ||
|
|
553416c927 | ||
|
|
b83696bc60 | ||
|
|
23ce320d75 | ||
|
|
dd170aafee | ||
|
|
27d5733dbc | ||
|
|
9ba769c53e | ||
|
|
8ff57e3004 | ||
|
|
5c75c6c7d3 | ||
|
|
56efb20ffa | ||
|
|
699578bb59 | ||
|
|
8d7d01bf88 | ||
|
|
0bc37d2fc2 | ||
|
|
aaa1655af1 | ||
|
|
f1bd4e1bba | ||
|
|
f74147070a | ||
|
|
20f4ea93e4 | ||
|
|
15a28e7c83 | ||
|
|
c550e1de54 | ||
|
|
ab26e561fd | ||
|
|
66968a28a3 | ||
|
|
37d1f91224 | ||
|
|
9e69068d42 | ||
|
|
8e2a9fcd01 | ||
|
|
1753887916 | ||
|
|
21bcffcc87 | ||
|
|
1caed49c75 | ||
|
|
619ea35168 | ||
|
|
877f913e8f | ||
|
|
25c47390c0 | ||
|
|
b3c46348a1 | ||
|
|
6f154194f1 | ||
|
|
c3ab08ce17 | ||
|
|
004fffa992 | ||
|
|
d6bd80c9c0 | ||
|
|
318bcdd011 | ||
|
|
3076f2af68 | ||
|
|
3dd9ef5564 | ||
|
|
367e5fa84e | ||
|
|
97cd61fd13 | ||
|
|
869bf7a345 | ||
|
|
9e15ac242d | ||
|
|
84943e58f1 | ||
|
|
2289bf0a27 | ||
|
|
7fda40c983 | ||
|
|
7eeed8f670 | ||
|
|
4d3f4ed5c2 | ||
|
|
145a4f5c20 | ||
|
|
9afe3d26e9 | ||
|
|
b73a7f1ed8 | ||
|
|
91a2bc3862 | ||
|
|
78a8a840b0 | ||
|
|
f4d54b6ca3 | ||
|
|
bc7a1c332c | ||
|
|
0e75cb9095 | ||
|
|
41b6fb6dcd | ||
|
|
2ca3cbc88f | ||
|
|
d05641a3d6 | ||
|
|
28bf84e05c | ||
|
|
ff51b53660 | ||
|
|
8b8e034b18 | ||
|
|
39927b06e3 | ||
|
|
66db2e7d16 | ||
|
|
a927c33ef1 | ||
|
|
17bc18b881 | ||
|
|
aa643c4a82 | ||
|
|
836fbea676 | ||
|
|
045049243c | ||
|
|
b9813a3494 | ||
|
|
9b42a93ce1 | ||
|
|
8502bceef1 | ||
|
|
663387476f | ||
|
|
daafd83df9 | ||
|
|
f780f2725b | ||
|
|
483aca871a | ||
|
|
352e709c3b | ||
|
|
629057b2c1 | ||
|
|
0e5f53596d | ||
|
|
0d91f07646 | ||
|
|
db882a26ab | ||
|
|
35c8ea22b1 | ||
|
|
7f01619358 | ||
|
|
ee109b4ceb | ||
|
|
7a398e5453 | ||
|
|
23a548f9b4 | ||
|
|
d4655d7034 | ||
|
|
9feb96b541 | ||
|
|
e939278193 | ||
|
|
d4ef1a2617 | ||
|
|
5f8746ced3 | ||
|
|
40a063e94f | ||
|
|
8f5439b958 | ||
|
|
e347f57d8b | ||
|
|
7169b15fd8 | ||
|
|
f9def8c96f | ||
|
|
ef43837af1 | ||
|
|
2efb8e8b8c | ||
|
|
e5bb69ea5f | ||
|
|
0979ca607d | ||
|
|
6ae186b2af | ||
|
|
98fb27f77d | ||
|
|
d68510bbaa | ||
|
|
71fdd2d92d | ||
|
|
3656689ff3 | ||
|
|
7d78406db6 | ||
|
|
ac47748e41 | ||
|
|
80f9b46479 | ||
|
|
999f1bf47a | ||
|
|
9e114eb2b8 | ||
|
|
4177d34b00 | ||
|
|
3ec5c04bf6 | ||
|
|
a877c068b6 | ||
|
|
6a3db90c1e | ||
|
|
a079e0d864 | ||
|
|
719776d66e | ||
|
|
c5af1241e9 | ||
|
|
27e4d7b563 | ||
|
|
450ab34721 | ||
|
|
3e2d4eae2c | ||
|
|
d89b6529ef | ||
|
|
5caf11556a | ||
|
|
78cc6f0f40 | ||
|
|
0007cd4668 | ||
|
|
05195e41de | ||
|
|
66f44ef87d | ||
|
|
a0585d9b11 | ||
|
|
5067946b13 | ||
|
|
f52241d5a8 | ||
|
|
04ccb25fa3 | ||
|
|
5a3f4b60b8 | ||
|
|
4408b2e488 | ||
|
|
9a26acee35 | ||
|
|
6ac377348b | ||
|
|
daeb88d4f4 | ||
|
|
47bf199f52 | ||
|
|
91540b022d | ||
|
|
505a51b6b5 | ||
|
|
28400488aa | ||
|
|
45ae600289 | ||
|
|
8be6874651 | ||
|
|
f694a500e0 | ||
|
|
c415fa01fc | ||
|
|
5225a9459c | ||
|
|
2974b150af | ||
|
|
cf353c8067 | ||
|
|
3aacb5d8b3 | ||
|
|
114fbdbe01 | ||
|
|
f1f83cbec4 | ||
|
|
335c28c2c9 | ||
|
|
daf12cbcce | ||
|
|
bf56eca003 | ||
|
|
c3542224ae | ||
|
|
a12b7fd58a | ||
|
|
98d004edbf | ||
|
|
67586a98b3 | ||
|
|
429faf44db | ||
|
|
c6d8911883 | ||
|
|
69ad7979ae | ||
|
|
12e398ce9b | ||
|
|
5c4a202616 | ||
|
|
adc1ec8444 | ||
|
|
0227d2fcb4 | ||
|
|
e87caac723 | ||
|
|
95abe3b5ac | ||
|
|
133a902c54 | ||
|
|
494544a4c2 | ||
|
|
aacb03d9ef | ||
|
|
fbeaa1781f | ||
|
|
ca73aad538 | ||
|
|
94b6118bf7 | ||
|
|
4a4bfefd17 | ||
|
|
7d6d86c6ff | ||
|
|
6796b0cd2a | ||
|
|
29e1f824b0 | ||
|
|
51263a2911 | ||
|
|
dd7f857475 | ||
|
|
bedc327e65 | ||
|
|
f0fdd4a537 | ||
|
|
9b847a0561 | ||
|
|
469e76b80a | ||
|
|
9c6994b476 | ||
|
|
52ba487617 | ||
|
|
21c57c9484 | ||
|
|
9b1ea6a07a | ||
|
|
f7d7bb0ea3 | ||
|
|
dd96b9ef53 | ||
|
|
b6b01893ba | ||
|
|
ad531d793d | ||
|
|
a9dd11e24a | ||
|
|
115983830b | ||
|
|
442cece081 | ||
|
|
14e08457b9 | ||
|
|
fabcc08cd5 | ||
|
|
7750843b04 | ||
|
|
40a893a8c9 | ||
|
|
37ebd30a4d | ||
|
|
c03188e976 | ||
|
|
d7da1ce333 | ||
|
|
dd9ee8c3f8 | ||
|
|
34bbd8f439 | ||
|
|
053f57cff5 | ||
|
|
365d2e2844 | ||
|
|
c0ac01a34a | ||
|
|
22c0bc0adb | ||
|
|
1b88f2ddf0 | ||
|
|
b4f2a1eb89 | ||
|
|
e9fc9cbc2a | ||
|
|
b809180a1b | ||
|
|
ecc75df3a1 | ||
|
|
d1b6863143 | ||
|
|
faf27143aa | ||
|
|
aee58a4475 | ||
|
|
c4cbf07d78 | ||
|
|
c5e07f643f | ||
|
|
818f5820d5 | ||
|
|
77c6e28876 | ||
|
|
fb7f66012d | ||
|
|
0f7f7bbe6c | ||
|
|
8dedb8deb4 | ||
|
|
dbb2c10bba | ||
|
|
67a5eef7d6 | ||
|
|
979f651251 | ||
|
|
3b93cbb009 | ||
|
|
30c63bfc4b | ||
|
|
bed40324a1 | ||
|
|
bc035de377 | ||
|
|
4db3cb6936 | ||
|
|
ed4b91f4bd | ||
|
|
24c7151276 | ||
|
|
804a9c07b8 | ||
|
|
528ea56821 | ||
|
|
7ba9c69ff8 | ||
|
|
0fc34da08a | ||
|
|
3fbf8cdbc8 | ||
|
|
e21f20d818 | ||
|
|
9fd9a60ca3 | ||
|
|
78b683d724 | ||
|
|
ad2f5036e1 | ||
|
|
4afbad8faa | ||
|
|
d284db4d3c | ||
|
|
82450c0ae8 | ||
|
|
8dd6c33901 | ||
|
|
f920d40db5 | ||
|
|
19be6c1acc | ||
|
|
7d9d8ad0e4 | ||
|
|
85f8237d5f | ||
|
|
c542894734 | ||
|
|
d348987077 | ||
|
|
3718610595 | ||
|
|
9c36ec0623 | ||
|
|
c6917b5d74 | ||
|
|
ae8b1c0c29 | ||
|
|
27978c459c | ||
|
|
1dc7f5c666 | ||
|
|
12ac870d3a | ||
|
|
dd1baa0224 | ||
|
|
4eaa179789 | ||
|
|
9008cd4549 | ||
|
|
bb27ef41cc | ||
|
|
2d35ac1df8 | ||
|
|
589ffc0c06 | ||
|
|
1f7f38c7d3 | ||
|
|
83817a2dc0 | ||
|
|
11af9da66f | ||
|
|
af3926acf3 | ||
|
|
ab40c2b3fd | ||
|
|
cc3204453e | ||
|
|
5ef8d3b7b9 | ||
|
|
2a9de97a19 | ||
|
|
9cecfed417 | ||
|
|
319715918a | ||
|
|
a3bf6e8b6d | ||
|
|
c4062658ce | ||
|
|
01a5de413e | ||
|
|
e4c22b1f29 | ||
|
|
fd05670dbc | ||
|
|
1ac094bfae | ||
|
|
b10e60126f | ||
|
|
ef1f27f421 | ||
|
|
0ed208675c | ||
|
|
00f7a0a194 | ||
|
|
935d4f4a64 | ||
|
|
dc4d88260d | ||
|
|
18934601da | ||
|
|
4ea811aeda | ||
|
|
f8fdecdc8f | ||
|
|
5467c61137 | ||
|
|
9c72b4cc56 | ||
|
|
9102217bc3 | ||
|
|
0e8fd7b2c4 | ||
|
|
fdf052cddb | ||
|
|
9a8d50ba6f | ||
|
|
136c97c312 | ||
|
|
bf00b88ef3 | ||
|
|
bafd1ea549 | ||
|
|
982618511b | ||
|
|
a4ad7ca3b1 | ||
|
|
99d71b57a4 | ||
|
|
1b2d8502e0 | ||
|
|
53e4ea9334 | ||
|
|
3ce704155c | ||
|
|
a06ea8fe55 | ||
|
|
31eb0fb48a | ||
|
|
d6a012e85f | ||
|
|
11c1cc7c72 | ||
|
|
6b7acb7bd5 | ||
|
|
bdebf19d7b | ||
|
|
cb1973ffb5 | ||
|
|
c6e2342ab4 | ||
|
|
2447599364 | ||
|
|
6a2cda74f1 | ||
|
|
8385d55d69 | ||
|
|
85e3464a15 | ||
|
|
6680039de7 | ||
|
|
9935826877 | ||
|
|
b977792168 | ||
|
|
2595cf87d8 | ||
|
|
f4342f1448 | ||
|
|
84c26b7c40 | ||
|
|
1cd7940a17 | ||
|
|
9514032f25 | ||
|
|
c7d6da2373 | ||
|
|
41b822fb6c | ||
|
|
69bf098c84 | ||
|
|
b4283ed4dc | ||
|
|
0fa0cac9e6 | ||
|
|
c71ef24052 | ||
|
|
cf0f665b14 | ||
|
|
2034e3ab78 | ||
|
|
89cfeec1b3 | ||
|
|
d8ae212df0 | ||
|
|
39b817bc69 | ||
|
|
09d79d52ae | ||
|
|
5c4b98d0e9 | ||
|
|
e5d6fc0604 | ||
|
|
5e656ebfba | ||
|
|
58fb75e55d | ||
|
|
e01621e658 | ||
|
|
b4aee17f53 | ||
|
|
dc70918648 | ||
|
|
69772edfa3 | ||
|
|
dd224cab05 | ||
|
|
b62873129e | ||
|
|
5052a1f564 | ||
|
|
2c36163e7a | ||
|
|
1bf912d6f0 | ||
|
|
d290259075 | ||
|
|
1689672faf | ||
|
|
8196e05679 | ||
|
|
fe0235da43 | ||
|
|
0895a73546 | ||
|
|
f06821e35b | ||
|
|
9cce5f645f | ||
|
|
c1a46408e9 | ||
|
|
21c3ccd637 | ||
|
|
3b9b034d80 | ||
|
|
348a5c3eb7 | ||
|
|
e3adaba3b3 | ||
|
|
4c5be658c3 | ||
|
|
b6517a449b | ||
|
|
ae0b8db0b0 | ||
|
|
56f0f8a299 | ||
|
|
c9af786b79 | ||
|
|
f34f615b80 | ||
|
|
aef2ef8479 | ||
|
|
7afbc9f5a4 | ||
|
|
df51b62041 | ||
|
|
045abc54fb | ||
|
|
9b2d9683eb | ||
|
|
3b0dd4a36c | ||
|
|
5e15f82313 | ||
|
|
d841c25bd3 | ||
|
|
8d3f1fe179 | ||
|
|
130ec130cc | ||
|
|
5e7a95eac0 | ||
|
|
a8cb49d12d | ||
|
|
c179ac626a | ||
|
|
041583bf96 | ||
|
|
ed710335b3 | ||
|
|
b556581a87 | ||
|
|
77a1b7918c | ||
|
|
45149e1b28 | ||
|
|
932338a25a | ||
|
|
925509e5a0 | ||
|
|
25646fbad7 | ||
|
|
e1733512c4 | ||
|
|
8379ffe1ce | ||
|
|
c77537ecee | ||
|
|
2192d97c69 | ||
|
|
a0dc76bda8 | ||
|
|
7fe177edc6 | ||
|
|
1f5e6f1e17 | ||
|
|
bf0aa295b0 | ||
|
|
649dffc3e0 | ||
|
|
a0f5ed66e2 | ||
|
|
7df3b95c22 | ||
|
|
0756474d40 | ||
|
|
60747db945 | ||
|
|
afcfad162e | ||
|
|
63f15bdc9e | ||
|
|
3b826869e9 | ||
|
|
af0256add0 | ||
|
|
b8d8cba12c | ||
|
|
616e9a0ec2 | ||
|
|
366434cbd7 | ||
|
|
f6d4046af6 | ||
|
|
82932f002e | ||
|
|
7593a05953 | ||
|
|
3026a9e3e4 | ||
|
|
362939eab9 | ||
|
|
61d52731a5 | ||
|
|
6aecc6521c | ||
|
|
ef5829593e | ||
|
|
4a8f67093f | ||
|
|
9cbe0664f6 | ||
|
|
965d6e4e8e | ||
|
|
eefdeb0bb7 | ||
|
|
a904a51293 | ||
|
|
cce377d70d | ||
|
|
5721bca5a3 | ||
|
|
bcd5b024f0 | ||
|
|
571f257c17 | ||
|
|
3451135800 | ||
|
|
f426a78a94 | ||
|
|
3d65236e63 | ||
|
|
7b51b5005a | ||
|
|
3d9cf16960 | ||
|
|
35def53666 | ||
|
|
5c46a89ddc | ||
|
|
4e429025bf | ||
|
|
95fae11eee | ||
|
|
9a22a9fb8b | ||
|
|
f60e2e2ca6 | ||
|
|
deb9101335 | ||
|
|
407f93ac43 | ||
|
|
78c39edceb | ||
|
|
c8445fb711 | ||
|
|
7c0e7347c8 | ||
|
|
12f37d0931 | ||
|
|
9a5086d9ba | ||
|
|
3222c7e677 | ||
|
|
632e0648d4 | ||
|
|
e3198031e3 | ||
|
|
0ced9c8e26 | ||
|
|
65f4a708cd | ||
|
|
36e7b00d9a | ||
|
|
8b2c48f5ca | ||
|
|
9f7a0d4f17 | ||
|
|
fa5ae17621 | ||
|
|
7a2536c559 | ||
|
|
96d2edb641 | ||
|
|
8a2bd23c32 | ||
|
|
d3b935ea7f | ||
|
|
e53bc3b048 | ||
|
|
5f1cfc9dda | ||
|
|
43207b316f | ||
|
|
96ed4c419a | ||
|
|
840a2253e2 | ||
|
|
18db9b0a77 | ||
|
|
6c7a5292a4 | ||
|
|
bef1c74226 | ||
|
|
176ec8bace | ||
|
|
c62064002f | ||
|
|
45b7800a68 | ||
|
|
fa761ac69b | ||
|
|
cc11e98aa6 | ||
|
|
8f1c71137a | ||
|
|
8fdf2dcb7a | ||
|
|
d4cd5b73bd | ||
|
|
77975aed2a | ||
|
|
a7700ce27e | ||
|
|
726ff1a126 | ||
|
|
e24269c452 | ||
|
|
686f4656ec | ||
|
|
55fe10d2dc | ||
|
|
422984ac41 | ||
|
|
706d117d80 | ||
|
|
13f8df4e0d | ||
|
|
263d433193 | ||
|
|
c15c11f3b1 | ||
|
|
524c8ccfc5 | ||
|
|
902392ea30 | ||
|
|
bef179187f | ||
|
|
ea7221c39a | ||
|
|
edaf9f6296 | ||
|
|
0d83725b77 | ||
|
|
6ce31305c6 | ||
|
|
90935c033d | ||
|
|
b4c3f831a7 | ||
|
|
f0e25e8198 | ||
|
|
d800082621 | ||
|
|
653d3da718 | ||
|
|
0f39409386 | ||
|
|
ccae0d1a57 | ||
|
|
257992d314 | ||
|
|
5eb843b63d | ||
|
|
3929b478a7 | ||
|
|
18734ed822 | ||
|
|
876e749b31 | ||
|
|
32b8c505d9 | ||
|
|
37a0dce7c5 | ||
|
|
2332f36b56 | ||
|
|
21cc9cc026 | ||
|
|
db4d76502e | ||
|
|
1578ea7590 | ||
|
|
7405de01fe | ||
|
|
77dc5943e5 | ||
|
|
f8a2748ede | ||
|
|
a99ca00bb3 | ||
|
|
6eb80eea2f | ||
|
|
82828f7f82 | ||
|
|
23c9a5963a | ||
|
|
7595f113ec | ||
|
|
e9e5a4ee0d | ||
|
|
1947fc3e83 | ||
|
|
8844689482 | ||
|
|
0d2ba54c10 | ||
|
|
a4bb5137ea | ||
|
|
b8aea1f97a | ||
|
|
120e1893bd | ||
|
|
836df52a50 | ||
|
|
00498aaeac | ||
|
|
f4f47cff75 | ||
|
|
40dc3d45fc | ||
|
|
b89d2a6da1 | ||
|
|
55a4af9f00 | ||
|
|
719d45e75e | ||
|
|
8703684740 | ||
|
|
e95e7218f6 | ||
|
|
e9d4711978 | ||
|
|
9309506e97 | ||
|
|
00d2a80e95 | ||
|
|
c1e62b7d90 | ||
|
|
84775d36dc | ||
|
|
fc4eb11fd8 | ||
|
|
ce70ce6c76 | ||
|
|
ffd404ec1b | ||
|
|
6f943db012 | ||
|
|
12342ac426 | ||
|
|
489ddc3f56 | ||
|
|
02a266cbea | ||
|
|
bdc9facd41 | ||
|
|
7d679aac0b | ||
|
|
ae1719e795 | ||
|
|
6cea05e9f4 | ||
|
|
cda84d4e64 | ||
|
|
b3f63a85d5 | ||
|
|
196903cb12 | ||
|
|
e6f082adfd | ||
|
|
bb2f641073 | ||
|
|
de6312d317 | ||
|
|
887b0f3119 | ||
|
|
f13c8d7884 | ||
|
|
a10fdb260d | ||
|
|
0f8b1790f3 | ||
|
|
531e345dd9 | ||
|
|
c1942759d4 | ||
|
|
eca9e573de | ||
|
|
61376f8d68 | ||
|
|
292e2c60e2 | ||
|
|
2ffaf81109 | ||
|
|
da37381678 | ||
|
|
3236bf6122 | ||
|
|
a352ae6922 | ||
|
|
f3631a6a09 | ||
|
|
0d376bd5b7 | ||
|
|
fd57bd0378 | ||
|
|
5a11882d1b | ||
|
|
0f041da548 | ||
|
|
00a23119c5 | ||
|
|
cdeb49473b | ||
|
|
b55b3731a5 | ||
|
|
4afbb688ba | ||
|
|
f289a921f1 | ||
|
|
f2165bc4c1 | ||
|
|
73b20bfe4a | ||
|
|
fbf2006e3f | ||
|
|
358b701396 | ||
|
|
d3caae3a2d | ||
|
|
300062d3ac | ||
|
|
bb0aaad383 | ||
|
|
e9aa8609f1 | ||
|
|
37cbb18626 | ||
|
|
3dad6daa2e | ||
|
|
f49485e161 | ||
|
|
f3f268742f | ||
|
|
99e76f2254 | ||
|
|
a7801e376b | ||
|
|
90000aa1fb | ||
|
|
a2a26cd058 | ||
|
|
e3cbefc5b6 | ||
|
|
c6ceb25ee8 | ||
|
|
022777888c | ||
|
|
ad5e644362 | ||
|
|
bf6cb04fe8 | ||
|
|
dbde23fb7b | ||
|
|
c9cb469d65 | ||
|
|
2e37c20e55 | ||
|
|
ade665d228 | ||
|
|
a62f0cfd3b | ||
|
|
8b867f78fe | ||
|
|
d6a43fd8e5 | ||
|
|
9887b8142d | ||
|
|
4d92d6dc2b | ||
|
|
d8cd84ed9e | ||
|
|
6bc740e881 | ||
|
|
db348cc368 | ||
|
|
6ebf59d7ff | ||
|
|
d35e31d128 | ||
|
|
b9b6d3d2cb | ||
|
|
728b111ac9 | ||
|
|
5afbfbfd43 | ||
|
|
9e5ce589ae | ||
|
|
f486150a0f | ||
|
|
075ee815f0 | ||
|
|
d321283b13 | ||
|
|
0ec72bb013 | ||
|
|
cdfbcd873c | ||
|
|
8be382fa7e | ||
|
|
82f5ab1446 | ||
|
|
c97f8c31ce | ||
|
|
92e5b5e9c3 | ||
|
|
a70fca493d | ||
|
|
42c8f0c345 | ||
|
|
d3d5a1745d | ||
|
|
8392d8b684 | ||
|
|
1abd13c6e0 | ||
|
|
e126b66e19 | ||
|
|
47449d93db | ||
|
|
13002f96f1 | ||
|
|
1985a035be | ||
|
|
cdfc7e4158 | ||
|
|
5050052710 | ||
|
|
48e39d2ffa | ||
|
|
a8d053e82a | ||
|
|
2154945c3c | ||
|
|
8d83a0a86a | ||
|
|
c196cdf405 | ||
|
|
b6a5f43176 | ||
|
|
008ded4a5c | ||
|
|
d476574d05 | ||
|
|
371b3813d4 | ||
|
|
e4c3c224d9 | ||
|
|
69bc697568 | ||
|
|
fe08d034bb | ||
|
|
18f4714410 | ||
|
|
1b6c416893 | ||
|
|
6153a28b4b | ||
|
|
9574cf16fb | ||
|
|
d309a67416 | ||
|
|
fb865af088 | ||
|
|
c1e7039357 | ||
|
|
0fd3b37641 | ||
|
|
cea91f7b2f | ||
|
|
3959896832 | ||
|
|
d55dccdeb1 | ||
|
|
c46c286b51 | ||
|
|
36b37b3b8f | ||
|
|
ce9931d8a3 | ||
|
|
66f5ff35d3 | ||
|
|
aa15d261f3 | ||
|
|
00a32463c7 | ||
|
|
dd60ff8b74 | ||
|
|
4588611cbf | ||
|
|
80c0152d46 | ||
|
|
fac727cd3d | ||
|
|
9c30183068 | ||
|
|
9c28d5c5c5 | ||
|
|
3c55e3a3f0 | ||
|
|
ba6e3b801d | ||
|
|
4064ab47ac | ||
|
|
b6005fcb56 | ||
|
|
71de526366 | ||
|
|
a3e5d8448b | ||
|
|
f65b6e5484 | ||
|
|
05d0d9e501 | ||
|
|
d107beadf2 | ||
|
|
9c2fd26579 | ||
|
|
d796ea6324 | ||
|
|
8386c9e729 | ||
|
|
00ca4524b5 | ||
|
|
f4d4853319 | ||
|
|
00678cc9ca | ||
|
|
2845486a3f | ||
|
|
a815e6447d | ||
|
|
5b71a30ee9 | ||
|
|
ac2d94420b | ||
|
|
1460c1364a | ||
|
|
37f38fe988 | ||
|
|
cf025b9135 | ||
|
|
283ff7a280 | ||
|
|
e668f016b4 | ||
|
|
256c2c955a | ||
|
|
d560c3e8de | ||
|
|
4f8e8e6669 | ||
|
|
7cdc2e0915 | ||
|
|
74b236b317 | ||
|
|
e344cafd9b | ||
|
|
4b0d16cad1 | ||
|
|
da4d8629bd | ||
|
|
68ae3b79ab | ||
|
|
78e0336c1b | ||
|
|
89ffeaf03b | ||
|
|
e439f4d643 | ||
|
|
8b779a0fca | ||
|
|
4b71dc8445 | ||
|
|
780875d5f2 | ||
|
|
7356d4b0e2 | ||
|
|
654dea6b7e | ||
|
|
1204374637 | ||
|
|
880fde2148 | ||
|
|
d776d76100 | ||
|
|
39d95105e1 | ||
|
|
2a7af826a8 | ||
|
|
9d2fd53073 | ||
|
|
4c99923467 | ||
|
|
edc5985a7e | ||
|
|
96fc79103b | ||
|
|
76879f3a73 | ||
|
|
38dac3803b | ||
|
|
c0d4ad2042 | ||
|
|
c669d5657a | ||
|
|
98c44fa578 | ||
|
|
328629fe88 | ||
|
|
9754535055 | ||
|
|
26f701d890 | ||
|
|
f3df8024e6 | ||
|
|
bc9fdfc7a4 | ||
|
|
e1e37989e4 | ||
|
|
dc67cb1807 | ||
|
|
2e8a2457bc | ||
|
|
6d4398c6fd | ||
|
|
9f67ad872d | ||
|
|
aba396f274 | ||
|
|
4315f34398 | ||
|
|
47b456e1ee | ||
|
|
529810b2dc | ||
|
|
6f67b8e788 | ||
|
|
a9c9d12444 | ||
|
|
f8428cec61 | ||
|
|
ac46bce807 | ||
|
|
23e899042d | ||
|
|
80c4a3c06d | ||
|
|
d2a31601ba | ||
|
|
7ddb4f3486 | ||
|
|
e56da87e0e | ||
|
|
2e4ebecf67 | ||
|
|
1b4ccaed91 | ||
|
|
1e2d41c7fb | ||
|
|
1b2ead054a | ||
|
|
468c1b95b7 | ||
|
|
60d8eff71f | ||
|
|
8baae8b801 | ||
|
|
839375fcf1 | ||
|
|
9f16f26347 | ||
|
|
69b4cacab4 | ||
|
|
b74b5040b1 | ||
|
|
a28decc854 | ||
|
|
ed82c36628 | ||
|
|
3536629dd9 | ||
|
|
c87696696e | ||
|
|
cb59cef1b8 | ||
|
|
b5ba03df4d | ||
|
|
d9b600466c | ||
|
|
0d37a59a5c | ||
|
|
4edf2f8cd1 | ||
|
|
c60dfdf0d7 | ||
|
|
006afc6841 | ||
|
|
f70879581d | ||
|
|
7a2d2b0376 | ||
|
|
b5368fa239 | ||
|
|
db1d71af9f | ||
|
|
6b03ef35a6 | ||
|
|
2afd02d86f | ||
|
|
b32c00f455 | ||
|
|
fbe9fb41ed | ||
|
|
cc0a7f7d76 | ||
|
|
db33fc60b9 | ||
|
|
6feaee4f86 | ||
|
|
1a27a31a32 | ||
|
|
5b611e71d5 | ||
|
|
6de88bfe11 | ||
|
|
6d7236249f | ||
|
|
69fbaba8a6 | ||
|
|
6d88737505 | ||
|
|
9869cfc736 | ||
|
|
8505326a68 | ||
|
|
3a4af88384 | ||
|
|
5b2e7d0f70 | ||
|
|
ddeea6bee3 | ||
|
|
0cfe3a7634 | ||
|
|
727463e4d1 | ||
|
|
d42abfdc56 | ||
|
|
e01ea1df4c | ||
|
|
078bfac5f5 | ||
|
|
111b07b9e6 | ||
|
|
dfbc89addc | ||
|
|
bf44da9a14 | ||
|
|
d75d13965b | ||
|
|
8aedebdc94 | ||
|
|
9388c4bb0d | ||
|
|
77d4f601af | ||
|
|
7fae590848 | ||
|
|
bc41558a26 | ||
|
|
f6651face4 | ||
|
|
345f00f7f2 | ||
|
|
e876d02118 | ||
|
|
5b7018f71b | ||
|
|
f45b3fc50a | ||
|
|
01196be30d | ||
|
|
0a2999bffb | ||
|
|
8f097096e7 | ||
|
|
cd97fc046a | ||
|
|
eeb10f31a6 | ||
|
|
9df5e116e8 | ||
|
|
1228a03d39 | ||
|
|
a5e1b3096e | ||
|
|
b41ae67128 | ||
|
|
ddfbe20125 | ||
|
|
0bfe9291dd | ||
|
|
622b2e1edc | ||
|
|
4a40719534 | ||
|
|
384993d363 | ||
|
|
01b7d28154 | ||
|
|
d7c4f5577f | ||
|
|
a69d23ca64 | ||
|
|
e2f8b7a6e3 | ||
|
|
171a0b012f | ||
|
|
5c04b15433 | ||
|
|
6397feffff | ||
|
|
e73b9b7f1c | ||
|
|
2d528db054 | ||
|
|
80c4ba6723 | ||
|
|
0d82e40c67 | ||
|
|
b75d6d02fa | ||
|
|
76d4542716 | ||
|
|
87955de849 | ||
|
|
6df60cf5da | ||
|
|
3c23a314f0 | ||
|
|
8fda6b04a4 | ||
|
|
9fa98e6b76 | ||
|
|
deb685f39b | ||
|
|
d7851d3a18 | ||
|
|
44946fc54a | ||
|
|
a033d10adc | ||
|
|
5a3e599fe0 | ||
|
|
a9f645f389 | ||
|
|
d662f0903a | ||
|
|
beaa947eb7 | ||
|
|
48006b64d6 | ||
|
|
8f195ba66f | ||
|
|
123288e745 | ||
|
|
5866e95d49 | ||
|
|
e79f395424 | ||
|
|
999ca87fec | ||
|
|
1217266d88 | ||
|
|
bb262198be | ||
|
|
11aae77caf | ||
|
|
8212cede6e | ||
|
|
a3c51884f4 | ||
|
|
b8890aca7f | ||
|
|
014b0cce14 | ||
|
|
6d860c5cb7 | ||
|
|
d8be832858 | ||
|
|
afcb9fcf41 | ||
|
|
3c7ae0aaf0 | ||
|
|
6b7f93dbfe | ||
|
|
c40b255022 | ||
|
|
1742d265f3 | ||
|
|
3240e0bcae | ||
|
|
ff185f6505 | ||
|
|
346b517c9d | ||
|
|
80f00aba0a | ||
|
|
949905f6e2 | ||
|
|
b9e26fecfd | ||
|
|
232682f4a8 | ||
|
|
de3b690d60 | ||
|
|
de69a78a98 | ||
|
|
1c341c34a3 | ||
|
|
33beb57e9d | ||
|
|
66eeadca0b | ||
|
|
a10d1c98a8 | ||
|
|
59ead4986f | ||
|
|
09f6c18189 | ||
|
|
a5cd6d5ac0 | ||
|
|
0f3ad7c8b1 | ||
|
|
0487dea7fc | ||
|
|
a6803bf0e3 | ||
|
|
8cac1ee284 | ||
|
|
196620e1bd | ||
|
|
43d6c76873 | ||
|
|
b864c39a0d | ||
|
|
818b975111 | ||
|
|
d5fbc8393f | ||
|
|
df9a71a63d | ||
|
|
7b5e9d2344 | ||
|
|
7fc2d95886 | ||
|
|
2ba8702787 | ||
|
|
78d3b369bb | ||
|
|
bb3620680b | ||
|
|
d4a45655ca | ||
|
|
c9c739fd52 | ||
|
|
2b359cc592 | ||
|
|
151b7a323d | ||
|
|
1063dc2b63 | ||
|
|
f9f59a6eb1 | ||
|
|
73156cc337 | ||
|
|
7d53607f49 | ||
|
|
7539945465 | ||
|
|
51df8e7bb1 | ||
|
|
17029ce67c | ||
|
|
8cedc313cf | ||
|
|
5afe3acac1 | ||
|
|
9887b58b71 | ||
|
|
ec8363ba6a | ||
|
|
fcfb71f13b | ||
|
|
3a12e431ff | ||
|
|
bc4ed8e123 | ||
|
|
445e9540a5 | ||
|
|
bbc2a2a9dd | ||
|
|
5117bc78b6 | ||
|
|
10bf149a07 | ||
|
|
0a976bd012 | ||
|
|
df31c43e59 | ||
|
|
c5d30b9b23 | ||
|
|
2e18beff27 | ||
|
|
25e0cec2cc | ||
|
|
16cc4c5c97 | ||
|
|
e5cfb6b7eb | ||
|
|
a882ba07e9 | ||
|
|
801f3f99aa | ||
|
|
2338b9b57d | ||
|
|
8ba396c693 | ||
|
|
1164022765 | ||
|
|
b0c5519da5 | ||
|
|
f5073238d8 | ||
|
|
3ffa89bfaf | ||
|
|
26d8b2fa22 | ||
|
|
6b17502694 | ||
|
|
a69d57a4f4 | ||
|
|
430bc6150f | ||
|
|
b888615e0d | ||
|
|
9fae343668 | ||
|
|
db467889b0 | ||
|
|
153b8d1f37 | ||
|
|
87858762d4 | ||
|
|
50d3282a65 | ||
|
|
aee0500b38 | ||
|
|
f2ef6eb94e | ||
|
|
151a5a7e73 | ||
|
|
6a088c58de | ||
|
|
23e7bf9f89 | ||
|
|
59f24206ad | ||
|
|
e45ef019c0 | ||
|
|
2830d3c8fa | ||
|
|
088816dfab | ||
|
|
453a29b81c | ||
|
|
e5cb160aa4 | ||
|
|
844588a0d4 | ||
|
|
cfcfd47705 | ||
|
|
f416c0ec7d | ||
|
|
78f707c07c | ||
|
|
d28a59a2fe | ||
|
|
edf3525a3f | ||
|
|
2b7fe35305 | ||
|
|
d5819ea4d0 | ||
|
|
1099126def | ||
|
|
4ba3a797e3 | ||
|
|
51b9bb88e5 | ||
|
|
fa376148bd | ||
|
|
452b9677da | ||
|
|
02a779f9a2 | ||
|
|
ea60645247 | ||
|
|
b444a13285 | ||
|
|
0c6b2a13eb | ||
|
|
e10bdc1169 | ||
|
|
7512cffca3 | ||
|
|
203440e9b8 | ||
|
|
145030e854 | ||
|
|
2b81dfb100 | ||
|
|
c96ace5281 | ||
|
|
520c6b60be | ||
|
|
492382d552 | ||
|
|
7ca55dd531 | ||
|
|
ce4ba73fc4 | ||
|
|
5622d92cbb | ||
|
|
8de1c5fd36 | ||
|
|
6c84fea8dc | ||
|
|
0b94070086 | ||
|
|
2f209182f5 | ||
|
|
d7bc572f3e | ||
|
|
4985b49194 | ||
|
|
a792df2021 | ||
|
|
a42ec74723 | ||
|
|
de3dbe3b36 | ||
|
|
874fdb7da0 | ||
|
|
3bf0de3888 | ||
|
|
d4020c5e0f | ||
|
|
6175fc00ad | ||
|
|
76e996f429 | ||
|
|
fd222b73ce | ||
|
|
a8cc0b1edf | ||
|
|
ea2f3545a6 | ||
|
|
0cf136712a | ||
|
|
34a453873a | ||
|
|
0acac3b096 | ||
|
|
37141410e0 | ||
|
|
69b0e276e3 | ||
|
|
5872376f50 | ||
|
|
075f72d9f6 | ||
|
|
4441ec1b14 | ||
|
|
0735cc1a54 | ||
|
|
dea2ad6904 | ||
|
|
0f0b6b4a8a | ||
|
|
174e562dcb | ||
|
|
f080750545 | ||
|
|
621415fe51 | ||
|
|
428c2818a5 | ||
|
|
5aa1c70999 | ||
|
|
3508f47842 | ||
|
|
62d4993e6d | ||
|
|
ede6070e43 | ||
|
|
a61b1d4337 | ||
|
|
631d946dcf | ||
|
|
8945334f37 | ||
|
|
af2df11a56 | ||
|
|
6dc0c42b1e | ||
|
|
0328293746 | ||
|
|
ad406947cf | ||
|
|
6bb4c1171f | ||
|
|
0f8c71a9df | ||
|
|
faa39190fc | ||
|
|
7c0b925c96 | ||
|
|
1b8c453fd0 | ||
|
|
8cc8f595bd | ||
|
|
cb0b6e010d | ||
|
|
23dc7be1ab | ||
|
|
4b14ad07d2 | ||
|
|
9a5a8ae23a | ||
|
|
02b27e235c | ||
|
|
8a4bf7896f | ||
|
|
208ea29643 | ||
|
|
7c52ec731a | ||
|
|
c6ee38e435 | ||
|
|
65253cc5b9 | ||
|
|
d1a1a23cbc | ||
|
|
e8bb3a5ba7 | ||
|
|
22b8f82770 | ||
|
|
5874c5b9cb | ||
|
|
fad09b2cd5 | ||
|
|
cfb08afd7d | ||
|
|
16d939c601 | ||
|
|
af072648c1 | ||
|
|
45d2609494 | ||
|
|
f5ea65f18c | ||
|
|
e9fc6bed23 | ||
|
|
ccc8e4664d | ||
|
|
651ef04137 | ||
|
|
063aba333c | ||
|
|
a3a517ff89 | ||
|
|
40da29b681 | ||
|
|
684d81c895 | ||
|
|
c6ddd3b238 | ||
|
|
3186413bee | ||
|
|
aae1c4cf1c | ||
|
|
e64f264f12 | ||
|
|
08cf747f52 | ||
|
|
383437a3c7 | ||
|
|
c7cba3f50b | ||
|
|
7feb499d50 | ||
|
|
2e6c25b651 | ||
|
|
192903e8d7 | ||
|
|
6f72ade4d4 | ||
|
|
7e3fc0fa59 | ||
|
|
4ea896b57c | ||
|
|
073ccb9b52 | ||
|
|
f32c944d31 | ||
|
|
acd1e3bdfc | ||
|
|
774cbdf0fe | ||
|
|
f5fd527590 | ||
|
|
7ac9a7e94a | ||
|
|
5735f7a945 | ||
|
|
f8f423b5c1 | ||
|
|
81ba7f0721 | ||
|
|
e6be8c23fb | ||
|
|
9cc1764a18 | ||
|
|
6a77adc313 | ||
|
|
e532572d5a | ||
|
|
dcae49c5f8 | ||
|
|
7321c01e8c | ||
|
|
a85b9998c3 | ||
|
|
30d2ce43d1 | ||
|
|
d46edfc9b7 | ||
|
|
79d11138e6 | ||
|
|
f5cd019b6c | ||
|
|
42c4de56fd | ||
|
|
44b3c28a2a | ||
|
|
e5184a1568 | ||
|
|
76d60ded4c | ||
|
|
d2e7e925f7 | ||
|
|
6357a30acb | ||
|
|
4b1fdd0e38 | ||
|
|
227fc060b9 | ||
|
|
32e5aba906 | ||
|
|
55013bb220 | ||
|
|
5544b20d7f | ||
|
|
d6c7f9c68b | ||
|
|
8b5004e500 | ||
|
|
d6cf11b87d | ||
|
|
d4c3a3be6b | ||
|
|
e724b188ef | ||
|
|
ebf92b1103 | ||
|
|
42e2a49af6 | ||
|
|
be7cd3275a | ||
|
|
966df11beb | ||
|
|
fad852f00d | ||
|
|
9c3e6eb823 | ||
|
|
8b88f72efc | ||
|
|
e0aab6cfbf | ||
|
|
29a2e60e05 | ||
|
|
12df74b3a7 | ||
|
|
22d943c9e2 | ||
|
|
5839f51f44 | ||
|
|
4ecb8d4483 | ||
|
|
a08035551a | ||
|
|
c2460d7262 | ||
|
|
4776eac07e | ||
|
|
4952d107dd | ||
|
|
b5d6ee9dee | ||
|
|
e7a30c6024 | ||
|
|
6578e52ec5 | ||
|
|
97508beb5c | ||
|
|
e063b0d6fc | ||
|
|
bb65dc0e81 | ||
|
|
09e00ec119 | ||
|
|
985f8fad3b | ||
|
|
a9accc8c42 | ||
|
|
41316d2bd3 | ||
|
|
2c172eb8d3 | ||
|
|
d49827f9f8 | ||
|
|
b2aafda2b1 | ||
|
|
b4c50e0262 | ||
|
|
fbe51c12c1 | ||
|
|
991959416b | ||
|
|
41f0e61f60 | ||
|
|
eab8cd101f | ||
|
|
97765d798c | ||
|
|
a7f76248ac | ||
|
|
8de670fcf2 | ||
|
|
744823cce4 | ||
|
|
d95a3e00aa | ||
|
|
ccc190f7b0 | ||
|
|
3e6cd98cb9 | ||
|
|
0e3b8fdbb6 | ||
|
|
5aa3f79616 | ||
|
|
42427d0690 | ||
|
|
1a7b32e6d1 | ||
|
|
83555bfdc5 | ||
|
|
03990c1dd9 | ||
|
|
b361be5cb0 | ||
|
|
d02f6d1e67 | ||
|
|
1e56c34e2f | ||
|
|
2a2f8dcecd | ||
|
|
b609ed3ad4 | ||
|
|
80521f8ec2 | ||
|
|
3a5df6a893 | ||
|
|
0e60c4f910 | ||
|
|
f5f2d3c883 | ||
|
|
3c830bfaf2 | ||
|
|
98caf9b5bf | ||
|
|
cfbb8fab1b | ||
|
|
3069e5e566 | ||
|
|
ac050a09e8 | ||
|
|
78406ccdbf | ||
|
|
2efea1bb00 | ||
|
|
0157a160f0 | ||
|
|
eb4084a6a4 | ||
|
|
63f5e5416f | ||
|
|
38e0433e8f | ||
|
|
fc5ffc5f62 | ||
|
|
460e3558a9 | ||
|
|
b06f223124 | ||
|
|
aabfb2adfd | ||
|
|
f9c47c9035 | ||
|
|
e9485ebf56 | ||
|
|
fcd7fd2889 | ||
|
|
279f4a347a | ||
|
|
b29fe23403 | ||
|
|
1972a551e9 | ||
|
|
7e2ffd2ec4 | ||
|
|
06793ae13e | ||
|
|
26e961d356 | ||
|
|
da956d3bd5 | ||
|
|
318a72a123 | ||
|
|
5c4b4864d2 | ||
|
|
4ab31fe21a | ||
|
|
c22a213635 | ||
|
|
aa166a0104 | ||
|
|
2a36626731 | ||
|
|
2f2360fd48 | ||
|
|
e466643229 | ||
|
|
a9044c3dc4 | ||
|
|
2d28cc21a0 | ||
|
|
ebb0e7e118 | ||
|
|
8205454858 | ||
|
|
e909112ab8 | ||
|
|
469923855a | ||
|
|
f2aca08886 | ||
|
|
9900f8fecb | ||
|
|
73224887b9 | ||
|
|
e31574015b | ||
|
|
ef26251469 | ||
|
|
798bce2759 | ||
|
|
88fee5f6de | ||
|
|
937695a1e5 | ||
|
|
a43b580d67 | ||
|
|
b8d0bff22b | ||
|
|
c180d38394 | ||
|
|
10f7d955ff | ||
|
|
143651099a | ||
|
|
63bb12269f | ||
|
|
00ee80184e | ||
|
|
60ba058515 | ||
|
|
1cf8131b6c | ||
|
|
1b38bd59ef | ||
|
|
2e409c3246 | ||
|
|
63fbca8029 | ||
|
|
b37966f79c | ||
|
|
bac5b0de5b | ||
|
|
5dac161553 | ||
|
|
528c167a88 | ||
|
|
cfe01aa996 | ||
|
|
0f53c975cc | ||
|
|
b7b99c77c8 | ||
|
|
1eea5412a5 | ||
|
|
4240465930 | ||
|
|
3dfbf7d2ad | ||
|
|
440490a4bb | ||
|
|
746382811b | ||
|
|
967a54dd50 | ||
|
|
289d9a2531 | ||
|
|
c56b4964fe | ||
|
|
79dbb942f9 | ||
|
|
a5bb5635d3 | ||
|
|
3efe43c0fe | ||
|
|
5fb5299d34 | ||
|
|
b988882251 | ||
|
|
85e82e3fb9 | ||
|
|
35cfe261d2 | ||
|
|
fe4faf9ebc | ||
|
|
751392d656 | ||
|
|
2e5ce5e94f | ||
|
|
535eeb2594 | ||
|
|
8f5e0e93ee | ||
|
|
6f17c5dcac | ||
|
|
4e7c7ba8ce | ||
|
|
6bd5b2345c | ||
|
|
63832ef8fd | ||
|
|
3719bf3593 | ||
|
|
7bca41ca72 | ||
|
|
3f6a9c3af5 | ||
|
|
309380bdd5 | ||
|
|
f135bdb905 | ||
|
|
2b926fd157 | ||
|
|
e911eea69c | ||
|
|
9d182b8299 | ||
|
|
61366e000f | ||
|
|
21cf49f4f8 | ||
|
|
30f0de83d3 | ||
|
|
42278a4b66 | ||
|
|
8752f92cea | ||
|
|
064c468e62 | ||
|
|
ef78fb749c | ||
|
|
a5d1db392b | ||
|
|
6234fc2ca3 | ||
|
|
c9cf90cdc9 | ||
|
|
5b033975b6 | ||
|
|
5663a153f7 | ||
|
|
4b73c45e65 | ||
|
|
ac248d8b73 | ||
|
|
726b0d0fa3 | ||
|
|
2d40164549 | ||
|
|
5203152f78 | ||
|
|
b064bb74cd | ||
|
|
843d8e8e77 | ||
|
|
c5f95b243d | ||
|
|
06f1f4c8ad | ||
|
|
9847f834c2 | ||
|
|
b744d58e6c | ||
|
|
c95358b344 | ||
|
|
2f15d6c9f2 | ||
|
|
d13aa047d5 | ||
|
|
7590d18c67 | ||
|
|
a689116c97 | ||
|
|
3bf7459f05 | ||
|
|
c70faaedd1 | ||
|
|
cb2417fbe4 | ||
|
|
65dd996f2e | ||
|
|
01790a6f31 | ||
|
|
8b84bb893d | ||
|
|
8cb99847c5 | ||
|
|
b3e01277d4 | ||
|
|
b10d407659 | ||
|
|
b7c5a5d238 | ||
|
|
90d1ce63e8 | ||
|
|
9e30b4e5f7 | ||
|
|
e541a8c629 | ||
|
|
0afe25c922 | ||
|
|
bca133430f | ||
|
|
9c925518a7 | ||
|
|
18e79b99e7 | ||
|
|
4e02846df9 | ||
|
|
2268b78bba | ||
|
|
ea8acd0677 | ||
|
|
9e931dd03f | ||
|
|
19e3aabca4 | ||
|
|
ae697d82d5 | ||
|
|
33382273c3 | ||
|
|
7d8466d77a | ||
|
|
5ca08a00d2 | ||
|
|
df3942697e | ||
|
|
5700ca5bcf | ||
|
|
54b2419d64 | ||
|
|
d8b1c94b78 | ||
|
|
2d1ffc23b9 | ||
|
|
e6b33d60c3 | ||
|
|
3fd06890d7 | ||
|
|
4af4ad7663 | ||
|
|
6ca8501e28 | ||
|
|
432b385f60 | ||
|
|
6cebdefa4a | ||
|
|
bc665eb83d | ||
|
|
cb187300fe | ||
|
|
da761614bd | ||
|
|
f34e007ecd | ||
|
|
3b6ad080b4 | ||
|
|
9919e90ba5 | ||
|
|
f4af44925b | ||
|
|
4bb366b568 | ||
|
|
7e7ab4ce19 | ||
|
|
4d833d25ce | ||
|
|
a9c508ecd9 | ||
|
|
ef4dbb8fdb | ||
|
|
9eb66face5 | ||
|
|
3fd13f3e3b | ||
|
|
319c9cad4b | ||
|
|
c12297c98d | ||
|
|
7c38361844 | ||
|
|
559554a975 | ||
|
|
7e2ffa2124 | ||
|
|
66dbac4bb2 | ||
|
|
8b6a843a85 | ||
|
|
976cff2751 | ||
|
|
f7c30fa8eb | ||
|
|
7757c8218b | ||
|
|
2928b7daa3 | ||
|
|
3a55dea276 | ||
|
|
2a25213d66 | ||
|
|
035ffd8135 | ||
|
|
b040487f1f | ||
|
|
6fc821aecf | ||
|
|
cdceb1fb6f | ||
|
|
07d185913d | ||
|
|
f2a245a9c8 | ||
|
|
33338f4759 | ||
|
|
f7a4370b29 | ||
|
|
77b7afedda | ||
|
|
caa13039e5 | ||
|
|
02845d93ed | ||
|
|
9ef4695cc7 | ||
|
|
d619e089c0 | ||
|
|
3c50348a79 | ||
|
|
167ea3b82b | ||
|
|
9eda3e62f7 | ||
|
|
99c4319b51 | ||
|
|
790b25db65 | ||
|
|
97d4972f9a | ||
|
|
8e6853756f | ||
|
|
6d3aae187b | ||
|
|
b8c7acf7ce | ||
|
|
17a356ae76 | ||
|
|
bd847e632d | ||
|
|
2bfb9b048d | ||
|
|
dc40b50b65 | ||
|
|
3e2271e596 | ||
|
|
4b4fd2a11d | ||
|
|
23468290df | ||
|
|
a276f6aa06 | ||
|
|
f2a58361a1 | ||
|
|
271023b528 | ||
|
|
e1771ca249 | ||
|
|
ca4f4bd151 | ||
|
|
d81454d618 | ||
|
|
fb43c1c624 | ||
|
|
9e060f878d | ||
|
|
bd9c21ee8a | ||
|
|
3e6d40e8da | ||
|
|
79683cb3fc | ||
|
|
52a2090a31 | ||
|
|
3dfe4ace7b | ||
|
|
bd0d17b134 | ||
|
|
6b0ccc1780 | ||
|
|
d75ac4b825 | ||
|
|
b60d610d02 | ||
|
|
f7a5c5d0ea | ||
|
|
28f79aec11 | ||
|
|
778d963fbf | ||
|
|
a765bc84e7 | ||
|
|
804ecc1baa | ||
|
|
d331c3dc03 | ||
|
|
7010d2f86a | ||
|
|
b1d6117eb2 | ||
|
|
f3b814388d | ||
|
|
b62996a57c | ||
|
|
a49e056f02 | ||
|
|
a6dece16bf | ||
|
|
8e3ddd64d2 | ||
|
|
45a847fa3e | ||
|
|
6b6f03b143 | ||
|
|
5446efca4a | ||
|
|
8d04a7f90b | ||
|
|
626495c19e | ||
|
|
e5a198f524 | ||
|
|
161524843f | ||
|
|
5550e7dea3 | ||
|
|
64f66c290c | ||
|
|
e8925b3c0b | ||
|
|
cf67ce04a8 | ||
|
|
84ee4ca2c7 | ||
|
|
27eb095720 | ||
|
|
d273f21819 | ||
|
|
455fd0cd6d | ||
|
|
c5a8650c81 | ||
|
|
b5f9bbed5e | ||
|
|
45da17adb8 | ||
|
|
e789741090 | ||
|
|
5c6d93bc57 | ||
|
|
697b672038 | ||
|
|
2d9e9c24a8 | ||
|
|
5fb281c800 | ||
|
|
96896c1c42 | ||
|
|
d7052bd9e6 | ||
|
|
8b23932788 | ||
|
|
50912c6966 | ||
|
|
53b51934b9 | ||
|
|
a8a3685965 | ||
|
|
149b67e28b | ||
|
|
a83032bffa | ||
|
|
a5d3a153bf | ||
|
|
4210c155eb | ||
|
|
4bf110a9b1 | ||
|
|
50f2684500 | ||
|
|
e95424b8f9 | ||
|
|
8462882707 | ||
|
|
a5c8d25f64 | ||
|
|
689ce2f9b3 | ||
|
|
54246533ac | ||
|
|
66e4b0fe47 | ||
|
|
3e8ae3e2e3 | ||
|
|
d856ef3772 | ||
|
|
5727880ac7 | ||
|
|
ec4302a780 | ||
|
|
d4203598a1 | ||
|
|
a278c8c718 | ||
|
|
faf5f4b51a | ||
|
|
b2f503b326 | ||
|
|
beb5484bf6 | ||
|
|
ec63d75349 | ||
|
|
4c0e79b245 | ||
|
|
50a77684c1 | ||
|
|
8bb84b486d | ||
|
|
4b05f2536f | ||
|
|
d6f968fe7e | ||
|
|
ed758edd44 | ||
|
|
94b7fce2e5 | ||
|
|
dbd9c6cbb7 | ||
|
|
0f6376fb80 | ||
|
|
9522328238 | ||
|
|
e6ad716119 | ||
|
|
440006bb08 | ||
|
|
ea289ef7cf | ||
|
|
352b171484 | ||
|
|
969ab56bf8 | ||
|
|
062a9852e5 | ||
|
|
dd77d7a5e6 | ||
|
|
f60e32522a | ||
|
|
070a91f19c | ||
|
|
0790e80670 | ||
|
|
20841e3d7b | ||
|
|
0caae233c3 | ||
|
|
5497d8fafb | ||
|
|
f69b43249c | ||
|
|
b606fd98f6 | ||
|
|
ba3b7b0f1f | ||
|
|
058d82dc36 | ||
|
|
6f0b0ac4fa | ||
|
|
35f87b0f94 | ||
|
|
0ead9ce9b4 | ||
|
|
80479a6a7c | ||
|
|
a7cea8201e | ||
|
|
081a7fa798 | ||
|
|
85782c4f93 | ||
|
|
d7b7df26d7 | ||
|
|
b6b1c8e31d | ||
|
|
17156f7ca2 | ||
|
|
0761d356b8 | ||
|
|
6da747ce6f | ||
|
|
87b1a1f527 | ||
|
|
72a8a55faf | ||
|
|
9a6a709746 | ||
|
|
428b53cc56 | ||
|
|
e688859e32 | ||
|
|
98336da116 | ||
|
|
c037e443b0 | ||
|
|
d339a50e0a | ||
|
|
7d836f2633 | ||
|
|
45d8470b4c | ||
|
|
1ca3bfe472 | ||
|
|
066da83d70 | ||
|
|
44ab881751 | ||
|
|
5ab3cf985a | ||
|
|
f271f2b181 | ||
|
|
91d75be0ea | ||
|
|
774dddca54 | ||
|
|
e18b3436c9 | ||
|
|
fcb1b5ae6b | ||
|
|
de980d030a | ||
|
|
0e859646fe | ||
|
|
059c7b7713 | ||
|
|
5fb7bf71c8 | ||
|
|
8b0133ff7f | ||
|
|
8d834946b8 | ||
|
|
2f646395d4 | ||
|
|
f6e79ba37b | ||
|
|
e633c7a861 | ||
|
|
dc02a8d78c | ||
|
|
baa9b88512 | ||
|
|
c522e87da8 | ||
|
|
ef5ebf2c15 | ||
|
|
4b147e770c | ||
|
|
157a5c0b05 | ||
|
|
f2288b0c64 | ||
|
|
d8506450aa | ||
|
|
f9b085e73f | ||
|
|
388cf6a91b | ||
|
|
9e6e77b363 | ||
|
|
ec33ca8173 | ||
|
|
6be0457947 | ||
|
|
f3b84aa845 | ||
|
|
bd0b5b0954 | ||
|
|
7dc93604ad | ||
|
|
0ab22698a6 | ||
|
|
c885ce7aaf | ||
|
|
92d1a7b901 | ||
|
|
6119054b45 | ||
|
|
e7aed72398 | ||
|
|
cee7fa50f5 | ||
|
|
39a38bb223 | ||
|
|
7159a993db | ||
|
|
23933e80e3 | ||
|
|
abc971b5cc | ||
|
|
7dedcc8a21 | ||
|
|
10d46e5dee | ||
|
|
139f7eb36d | ||
|
|
1ddfa894b6 | ||
|
|
d1695ab8c2 | ||
|
|
f27979e729 | ||
|
|
6e61e8172a | ||
|
|
21890894ae | ||
|
|
1feecd559d | ||
|
|
6ea4afe75b | ||
|
|
fd96f6367d | ||
|
|
8ce183c4c9 | ||
|
|
407a1db101 | ||
|
|
622d096e31 | ||
|
|
bf27fb1f89 | ||
|
|
860b9055c5 | ||
|
|
b3ae3a4148 | ||
|
|
0abd7d5762 | ||
|
|
0aac2bc55b | ||
|
|
fa08dc5cfb | ||
|
|
8d18970b4c | ||
|
|
173f5ce979 | ||
|
|
2e7088310a | ||
|
|
c75d99030c | ||
|
|
e4ba1d9bae | ||
|
|
e2886c342a | ||
|
|
e600d8a56c | ||
|
|
caeb305475 | ||
|
|
3d3a9d9bad | ||
|
|
5499ad5b94 | ||
|
|
0e29cd0cee | ||
|
|
24fb1b1a8f | ||
|
|
03fb4cbf0c | ||
|
|
e909280d5b | ||
|
|
d41ddf60b4 | ||
|
|
1e01a74986 | ||
|
|
96a007aace | ||
|
|
9f23bb6129 | ||
|
|
b7e8559773 | ||
|
|
5b247575c8 | ||
|
|
eb0e5b478f | ||
|
|
08906ae1da | ||
|
|
395a5efecd | ||
|
|
0452dd14f6 | ||
|
|
3906df314d | ||
|
|
ce49aa2ebd | ||
|
|
f2cb062b1e | ||
|
|
f25819a940 | ||
|
|
3075a9f9f4 | ||
|
|
52f1a672c8 | ||
|
|
45785fde1c | ||
|
|
6a7649e1d7 | ||
|
|
8b3831eb2b | ||
|
|
73e7f4669c | ||
|
|
c9f7bbbd25 | ||
|
|
ee67238133 | ||
|
|
b425da8d0f | ||
|
|
754a7f70bc | ||
|
|
a857ffa987 | ||
|
|
391ce2ebba | ||
|
|
086723adf4 | ||
|
|
e993279c35 | ||
|
|
aa64310875 | ||
|
|
795baf2c01 | ||
|
|
68ac453100 | ||
|
|
79d1f512e5 | ||
|
|
e739211314 | ||
|
|
d3f6374bb4 | ||
|
|
5add632cbc | ||
|
|
d210d1bcce | ||
|
|
6d6422cd63 | ||
|
|
66e8b7702b | ||
|
|
b75502ad87 | ||
|
|
3fba96d11f | ||
|
|
58d10672ea | ||
|
|
3571905705 | ||
|
|
acf0e2a1cb | ||
|
|
9e7dcb0d7c | ||
|
|
3c261e3cf7 | ||
|
|
b6f324f399 | ||
|
|
f2459489fa | ||
|
|
f8691cf285 | ||
|
|
2e631d3c42 | ||
|
|
1044dca936 | ||
|
|
56c3f495d5 | ||
|
|
0f3036dd9c | ||
|
|
af445ef157 | ||
|
|
25eb09f11c | ||
|
|
16f255aeca | ||
|
|
d0b340837d | ||
|
|
893828ac44 | ||
|
|
a3ca03636a | ||
|
|
582ffe3f23 | ||
|
|
3caad2cceb | ||
|
|
618dcf014d | ||
|
|
d88e20bb56 | ||
|
|
8a8b2b027e | ||
|
|
41cb223099 | ||
|
|
b93ea5e662 | ||
|
|
31c35939fd | ||
|
|
20a35f4221 | ||
|
|
bc6aeb2e93 | ||
|
|
a561299809 | ||
|
|
76efb938ab | ||
|
|
56abf73eaf | ||
|
|
2bc068d65a | ||
|
|
f191259f37 | ||
|
|
1384c6661d | ||
|
|
c047621548 | ||
|
|
017aaf2e54 | ||
|
|
e4b2b930af | ||
|
|
2646c0f0ee | ||
|
|
4afadb779c | ||
|
|
ad6e4daa22 | ||
|
|
d5cd07fe76 | ||
|
|
364065ed51 | ||
|
|
b4f05d4da7 | ||
|
|
3d12a0e8e9 | ||
|
|
e29f3194f3 | ||
|
|
6bac86638b | ||
|
|
d6ee1cdf6e | ||
|
|
e9d0efaf93 | ||
|
|
85467fa15b | ||
|
|
84bb47aa53 | ||
|
|
75f245c7dc | ||
|
|
6c5be88432 | ||
|
|
590b22de69 | ||
|
|
4770269f6f | ||
|
|
823f591aa8 | ||
|
|
c88c489633 | ||
|
|
8afb58a044 | ||
|
|
428fa8a61b | ||
|
|
3ba5e1ee79 | ||
|
|
d46d0cb384 | ||
|
|
f841884557 | ||
|
|
d381ab5316 | ||
|
|
a3e8e7ae77 | ||
|
|
a405753827 | ||
|
|
8df74d2c4b | ||
|
|
cbe0ffe52a | ||
|
|
a5631a0476 | ||
|
|
e4bd704a53 | ||
|
|
81a53440bc | ||
|
|
3466de1990 | ||
|
|
253b053c2c | ||
|
|
928d012046 | ||
|
|
19367406c6 | ||
|
|
f4e3717dd3 | ||
|
|
c01e1d91c5 | ||
|
|
6983f9f0b6 | ||
|
|
c13cf1a86c | ||
|
|
2a3dafe07f | ||
|
|
0104d02442 | ||
|
|
8cef4fde82 | ||
|
|
3dc02516ea | ||
|
|
3614529fda | ||
|
|
80d4f06e56 | ||
|
|
9ff4f395b5 | ||
|
|
61b8fa116a | ||
|
|
22a3541b7b | ||
|
|
c57515fed5 | ||
|
|
eec6199413 | ||
|
|
fe48955b94 | ||
|
|
03047ae6dd | ||
|
|
856d4867b4 | ||
|
|
b1b1aa0e13 | ||
|
|
ab68472698 | ||
|
|
5ae4ee1411 | ||
|
|
f1989bac21 | ||
|
|
1b20188b98 | ||
|
|
140a79d18c | ||
|
|
a9610ced0e | ||
|
|
17faee7719 | ||
|
|
120ca1c02c | ||
|
|
ef2d1ebe4f | ||
|
|
b9c931c97f | ||
|
|
60dd963d7d | ||
|
|
c414fac815 | ||
|
|
95041d6a0c | ||
|
|
1fab9c3279 | ||
|
|
3b8661249e | ||
|
|
2fba831851 | ||
|
|
fd4ac14ab3 | ||
|
|
1d9b9e9bfa | ||
|
|
14b2277313 | ||
|
|
0f57bf5235 | ||
|
|
4bb7fae1e3 | ||
|
|
ec0b8ebc92 | ||
|
|
1727633f5d | ||
|
|
668a77cb5a | ||
|
|
e87ee5e091 | ||
|
|
b7acc0e6da | ||
|
|
109f34680f | ||
|
|
fd57ccc1a7 | ||
|
|
e3dfae246a | ||
|
|
7186bbbe6c | ||
|
|
c7b2ce37b1 | ||
|
|
5b3a602911 | ||
|
|
6d51edd94d | ||
|
|
dcb68a6538 | ||
|
|
d549b86c81 | ||
|
|
ac415f1384 | ||
|
|
80ed5800a0 | ||
|
|
291ed44621 | ||
|
|
a7e8915ea0 | ||
|
|
a027c76af3 | ||
|
|
ec375bd068 | ||
|
|
f5a28c83f0 | ||
|
|
813240e233 | ||
|
|
051ac0e669 | ||
|
|
5c798c4569 | ||
|
|
cc2146e397 | ||
|
|
8356514bf8 | ||
|
|
464bc10860 | ||
|
|
5436c8bed1 | ||
|
|
1b163b161b | ||
|
|
f0ee5fd946 | ||
|
|
066a9f871c | ||
|
|
e23841f5bb | ||
|
|
571e66fae5 | ||
|
|
402aa280e0 | ||
|
|
65de5df319 | ||
|
|
63168afc85 | ||
|
|
1c61f54df6 | ||
|
|
50b5ad1799 | ||
|
|
6457c02a35 | ||
|
|
7b242c9733 | ||
|
|
e0ab6137e7 | ||
|
|
49b23a33e7 | ||
|
|
c5e9d14199 | ||
|
|
f7e8662bdf | ||
|
|
7d356d1e34 | ||
|
|
f9bb70f395 | ||
|
|
4335809468 | ||
|
|
2a1b1d28bd | ||
|
|
38d69428d8 | ||
|
|
0c22a8135d | ||
|
|
2b50665f9e | ||
|
|
59607efa62 | ||
|
|
cd5cfbe009 | ||
|
|
84e6d96ce0 | ||
|
|
8f00b53fab | ||
|
|
e2164a1a9c | ||
|
|
6e5dcaf08d | ||
|
|
18a2aae66a | ||
|
|
4c0aab15fa | ||
|
|
75a7d4188b | ||
|
|
ff0da57aeb | ||
|
|
d716cba46b | ||
|
|
5d41e44141 | ||
|
|
281936ecd0 | ||
|
|
da23321c0d | ||
|
|
110e2b7580 | ||
|
|
546d4353e2 | ||
|
|
15b5d36cd6 | ||
|
|
6a935a49ea | ||
|
|
f7f515481f | ||
|
|
3aab37c0c0 | ||
|
|
123e626df6 | ||
|
|
758985675d | ||
|
|
631ebc657b | ||
|
|
d0ca714482 | ||
|
|
1fe3787186 | ||
|
|
c8a952616f | ||
|
|
efcbecc218 | ||
|
|
487bafa5cf | ||
|
|
93aed33e2a | ||
|
|
a7765cb635 | ||
|
|
e6a5be6c66 | ||
|
|
5443532266 | ||
|
|
990aca2e1a | ||
|
|
49297adf97 | ||
|
|
b4d26fd35a | ||
|
|
b31f580760 | ||
|
|
7dc33f1956 | ||
|
|
e3470dd68b | ||
|
|
410c653654 | ||
|
|
bef3cf4c93 | ||
|
|
d8961d2acb | ||
|
|
17873ef7aa | ||
|
|
1d6e7eabc1 | ||
|
|
ba956abb1c | ||
|
|
7a2c2df89e | ||
|
|
bd44e659a8 | ||
|
|
8a1a27f5a1 | ||
|
|
7825071f61 | ||
|
|
7047bcbb1e | ||
|
|
193ef74e63 | ||
|
|
8a193d4dcd | ||
|
|
35cc662b26 | ||
|
|
9173bcc742 | ||
|
|
267f4273ee | ||
|
|
f1e675d662 | ||
|
|
c228534218 | ||
|
|
181fa5f32a | ||
|
|
7d5c37ec33 | ||
|
|
2eb9736d23 | ||
|
|
9f89ad2a08 | ||
|
|
490db3a026 | ||
|
|
2428f073bc | ||
|
|
e1231585ce | ||
|
|
839adaf559 | ||
|
|
839e004c08 | ||
|
|
ecf98828ff | ||
|
|
4dbc3c2353 | ||
|
|
c953a337fe | ||
|
|
6f026e6043 | ||
|
|
a775e29aef | ||
|
|
cb7b37fca4 | ||
|
|
cc79a67a9f | ||
|
|
72e7e3a3c4 | ||
|
|
4ffbeebd85 | ||
|
|
2e4b4e4736 | ||
|
|
a0acb0b658 | ||
|
|
feeaea4d64 | ||
|
|
0b7eb96e48 | ||
|
|
f0c4b628bf | ||
|
|
c66fc102ee | ||
|
|
9947a23343 | ||
|
|
abc204b313 | ||
|
|
425a0812bd | ||
|
|
030d466b11 | ||
|
|
27e9cd04a9 | ||
|
|
e42aea9444 | ||
|
|
3e1ee720f1 | ||
|
|
cb9f12482e | ||
|
|
bcf273a435 | ||
|
|
6230ada2cc | ||
|
|
db4de65683 | ||
|
|
931f7f07ca | ||
|
|
0f764c9400 | ||
|
|
89c98ab257 | ||
|
|
a49255c471 | ||
|
|
39d5a4908f | ||
|
|
fbaeaddff7 | ||
|
|
bc97db982e | ||
|
|
9a2f260bc5 | ||
|
|
954f1e3c7f | ||
|
|
0626f8d678 | ||
|
|
aa8c9676fe | ||
|
|
bb2ab768a8 | ||
|
|
8192c68f38 | ||
|
|
454561c2a1 | ||
|
|
deabcc9605 | ||
|
|
c3bdb9dd16 | ||
|
|
86d77c908f | ||
|
|
c5cbba5971 | ||
|
|
aea5493180 | ||
|
|
431832e50e | ||
|
|
daf239c93c | ||
|
|
2c669deae5 | ||
|
|
81969de76e | ||
|
|
5a29e7a83e | ||
|
|
9ed5aef40f | ||
|
|
9e025b4329 | ||
|
|
543da4fe24 | ||
|
|
e82e50a57a | ||
|
|
288d62d666 | ||
|
|
8ed224eeab | ||
|
|
22bafff943 | ||
|
|
02403cf566 | ||
|
|
eabbc6f037 | ||
|
|
27e60edaf4 | ||
|
|
f7d0b64dea | ||
|
|
b874484101 | ||
|
|
0ee72db2e9 | ||
|
|
4f07bac9ec | ||
|
|
80abaf70f2 | ||
|
|
d6acbc2fb1 | ||
|
|
caf93c019a | ||
|
|
7044efa7c3 | ||
|
|
5b4d8f971f | ||
|
|
75e8feb9b8 | ||
|
|
5d59419a06 | ||
|
|
ff2e47bf70 | ||
|
|
4721ed7c15 | ||
|
|
d9da328088 | ||
|
|
2ee506ac06 | ||
|
|
16c4e4dc5b | ||
|
|
28b8fb1b97 | ||
|
|
c8bae9fba6 | ||
|
|
920764ad33 | ||
|
|
a45d114527 | ||
|
|
62cca09045 | ||
|
|
ea126b90e2 | ||
|
|
777a20182e | ||
|
|
df62a9d32b | ||
|
|
47e211bf3c | ||
|
|
650ca3844e | ||
|
|
8de49fa027 | ||
|
|
35b7633f4d | ||
|
|
2c96a80280 | ||
|
|
c7000c8ac6 | ||
|
|
1dca2b4387 | ||
|
|
ccbe9ad21e | ||
|
|
1fe6f11ef4 | ||
|
|
9d4ab60fc4 | ||
|
|
ea8c5ad6e1 | ||
|
|
63a0f5f91c | ||
|
|
e8bacbdb6f | ||
|
|
bcc8226ccc | ||
|
|
ddec91a0c5 | ||
|
|
2c262bb29d | ||
|
|
ae0afb7b53 | ||
|
|
84e7fe5b60 | ||
|
|
ccb32b045a | ||
|
|
b2e29ac4bd | ||
|
|
7df6309a68 | ||
|
|
a203ad9f64 | ||
|
|
44d6e09337 | ||
|
|
7c59ec019a | ||
|
|
b457f64ec8 | ||
|
|
e152e59a61 | ||
|
|
2c620ad69a | ||
|
|
c08b405fc2 | ||
|
|
585e39c591 | ||
|
|
60e857dba9 | ||
|
|
9a4aa2b08f | ||
|
|
2900b08b70 | ||
|
|
b0936563a2 | ||
|
|
3d327b245a | ||
|
|
9a14de3448 | ||
|
|
59f83545cf | ||
|
|
d9da1ef085 | ||
|
|
279a27f740 | ||
|
|
6ba822ee48 | ||
|
|
e4445949c8 | ||
|
|
8178ff583d | ||
|
|
0e0e19a802 | ||
|
|
1e2e7d841f | ||
|
|
263c2f00eb | ||
|
|
39c0b57652 | ||
|
|
677baf549c | ||
|
|
848478c28b | ||
|
|
98ad33a589 | ||
|
|
3f33733f40 | ||
|
|
cdc4ae4fb3 | ||
|
|
3edfa8a6ce | ||
|
|
6e99b667af | ||
|
|
23c8735568 | ||
|
|
f39065044f | ||
|
|
7d2ae47b0f | ||
|
|
7247de6908 | ||
|
|
4c2aef8504 | ||
|
|
ac8717cf1f | ||
|
|
7761928064 | ||
|
|
198047406b | ||
|
|
1d57309db9 | ||
|
|
07af9f36b2 | ||
|
|
d157fea9be | ||
|
|
7692caf622 | ||
|
|
dc65a8823f | ||
|
|
2210932fdf | ||
|
|
0cb1bf4b7f | ||
|
|
44d175dd40 | ||
|
|
097feb6437 | ||
|
|
250c7e5f20 | ||
|
|
83740f77bb | ||
|
|
8267e13d22 | ||
|
|
a7e9396f35 | ||
|
|
0f6c8601d2 | ||
|
|
66e68092d5 | ||
|
|
09616a594c | ||
|
|
7ae58ea7ea | ||
|
|
9f1f85a3c4 | ||
|
|
9992cdfce0 | ||
|
|
f5fa08ce94 | ||
|
|
67f70f7f85 | ||
|
|
840bd56a3d | ||
|
|
c1e2d31cfd | ||
|
|
c35322d44e | ||
|
|
9867e46c3f | ||
|
|
946a120038 | ||
|
|
2cd1a17aa2 | ||
|
|
6e29fe2932 | ||
|
|
e6c6bf6613 | ||
|
|
72c53169df | ||
|
|
b78cce7b4f | ||
|
|
c40499ec31 | ||
|
|
ee158e517e | ||
|
|
70aebcf9aa | ||
|
|
b15870d441 | ||
|
|
fe7074736a | ||
|
|
69c523ffad | ||
|
|
6aeefdf43d | ||
|
|
5f4c8be3d3 | ||
|
|
3092e4c557 | ||
|
|
ea119068da | ||
|
|
14371ecf94 | ||
|
|
45b0fcfe15 | ||
|
|
00aa5f5586 | ||
|
|
79fd53fd4c | ||
|
|
357ee3daf0 | ||
|
|
c2f7897f10 | ||
|
|
75455c0c48 | ||
|
|
a394bb9f8e | ||
|
|
2f5a846493 | ||
|
|
90376b361d | ||
|
|
229cf6bf5f | ||
|
|
bc46737353 | ||
|
|
1db2243a2e | ||
|
|
7cf836b3cb | ||
|
|
f7bbd295d6 | ||
|
|
62dbd95b48 | ||
|
|
df07e9c719 | ||
|
|
9aaf72726e | ||
|
|
5289927619 | ||
|
|
fa8c686f75 | ||
|
|
df5f28b7c4 | ||
|
|
280d8368fa | ||
|
|
80dbff1f21 | ||
|
|
7ee68a8481 | ||
|
|
ac8dd42c45 | ||
|
|
eed2148b2a | ||
|
|
dc5345b6d3 | ||
|
|
221af0b5bb | ||
|
|
a10ccc1eb0 | ||
|
|
b72d858480 | ||
|
|
60412cc90b | ||
|
|
512ac87dc9 | ||
|
|
d97020d1c5 | ||
|
|
f82cb617ba | ||
|
|
f79281a1a0 | ||
|
|
7be1dbb78b | ||
|
|
f875787799 | ||
|
|
f82c208556 | ||
|
|
f2150e3d85 | ||
|
|
ecc198e8a0 | ||
|
|
949bc58a80 | ||
|
|
7d79fff16f | ||
|
|
3aeb678292 | ||
|
|
160ac41bed | ||
|
|
9dfbcbe89c | ||
|
|
30da529348 | ||
|
|
dd8d114711 | ||
|
|
2191a4a848 | ||
|
|
8b9ea8d988 | ||
|
|
46dda8567d | ||
|
|
6953da4d9a | ||
|
|
e987d6647e | ||
|
|
359d85727e | ||
|
|
a994bf9dd8 | ||
|
|
14c4e095f6 | ||
|
|
59dce0e56f | ||
|
|
1f54a893a7 | ||
|
|
9489f1ee3d | ||
|
|
dc3d720e8d | ||
|
|
efe30b598b | ||
|
|
42515bfb2d | ||
|
|
acb3657d95 | ||
|
|
e7159c9d36 | ||
|
|
f3fdca368b | ||
|
|
4ea3e08a45 | ||
|
|
1eebc72b21 | ||
|
|
9a91be7e36 | ||
|
|
48476f9b88 | ||
|
|
68c991eb9b | ||
|
|
b51c77b01b | ||
|
|
57105db554 | ||
|
|
4bd3bdaddf | ||
|
|
9cf59b8d73 | ||
|
|
a793b0bb42 | ||
|
|
4d9e2e1471 | ||
|
|
1719887e55 | ||
|
|
2be00aca9d | ||
|
|
6fd05c5ad7 | ||
|
|
65e404374f | ||
|
|
0b78731bb3 | ||
|
|
65d318ed88 | ||
|
|
1bfcea55a9 | ||
|
|
6780eb004d | ||
|
|
0e12b2d021 | ||
|
|
1b356f87ec | ||
|
|
bf24d0bae1 | ||
|
|
97c831d4bb | ||
|
|
e3e48ffa6d | ||
|
|
b678416122 | ||
|
|
df722925fa | ||
|
|
4cbc0d9806 | ||
|
|
b0f3711b4e | ||
|
|
14ec6579b2 | ||
|
|
30bf039473 | ||
|
|
33bea317b0 | ||
|
|
c6814dc05e | ||
|
|
16808069ec | ||
|
|
e813974e29 | ||
|
|
7e0010b536 | ||
|
|
93171adcb3 | ||
|
|
9501cc76a4 | ||
|
|
5d7db046ac | ||
|
|
46c259bc3e | ||
|
|
3bacff91d3 | ||
|
|
bd79d483d2 | ||
|
|
16e31f4881 | ||
|
|
afa23c393d | ||
|
|
54d23cb781 | ||
|
|
c757e410e9 | ||
|
|
39dd25567d | ||
|
|
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 |
13
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
#github: [J-Jamet] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
|
#patreon: # Replace with a single Patreon username
|
||||||
|
#open_collective: # Replace with a single Open Collective username
|
||||||
|
#ko_fi: # Replace with a single Ko-fi username
|
||||||
|
#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: Kunzisoft # Replace with a single Liberapay username
|
||||||
|
issuehunt: Kunzisoft/KeePassDX # Replace with a single IssueHunt username
|
||||||
|
#otechie: # Replace with a single Otechie username
|
||||||
|
#lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
custom: ['https://www.keepassdx.com/#donation'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
14
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -8,9 +8,11 @@ assignees: ''
|
|||||||
---
|
---
|
||||||
|
|
||||||
**Describe the bug**
|
**Describe the bug**
|
||||||
|
|
||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
|
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
@@ -18,24 +20,30 @@ Steps to reproduce the behavior:
|
|||||||
4. See error
|
4. See error
|
||||||
|
|
||||||
**Expected behavior**
|
**Expected behavior**
|
||||||
|
|
||||||
A clear and concise description of what you expected to happen.
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
** Keepass Database **
|
**KeePass Database**
|
||||||
|
|
||||||
- Created with: [e.g Windows KeePass 2.42]
|
- Created with: [e.g Windows KeePass 2.42]
|
||||||
- Version: [e.g. 2]
|
- Version: [e.g. 2]
|
||||||
- Location: [e.g. Remote file retrieved with GDrive app]
|
- Location: [e.g. Remote file retrieved with GDrive app]
|
||||||
|
- File provider (`content://` URI): [e.g. `content://com.google.android.apps.docs.storage/5`]
|
||||||
- Size: [e.g. 150Mo]
|
- Size: [e.g. 150Mo]
|
||||||
- Contains attachment: [e.g. Yes]
|
- Contains attachment: [e.g. Yes]
|
||||||
|
|
||||||
**KeePassDX (please complete the following information):**
|
**KeePassDX:**
|
||||||
|
|
||||||
- Version: [e.g. 2.5.0.0beta23]
|
- Version: [e.g. 2.5.0.0beta23]
|
||||||
- Build: [e.g. Free]
|
- Build: [e.g. Free]
|
||||||
- Language: [e.g. French]
|
- Language: [e.g. French]
|
||||||
|
|
||||||
**Android (please complete the following information):**
|
**Android:**
|
||||||
|
|
||||||
- Device: [e.g. GalaxyS8]
|
- Device: [e.g. GalaxyS8]
|
||||||
- Version: [e.g. 8.1]
|
- Version: [e.g. 8.1]
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
|
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
- Browser for Autofill: [e.g. Chrome version X]
|
- Browser for Autofill: [e.g. Chrome version X]
|
||||||
|
|||||||
3
.gitignore
vendored
@@ -80,6 +80,9 @@ art/screen*.png
|
|||||||
art/logo_512.png
|
art/logo_512.png
|
||||||
art/store_screens/
|
art/store_screens/
|
||||||
|
|
||||||
|
# Release
|
||||||
|
releases/*
|
||||||
|
|
||||||
# Dir linux
|
# Dir linux
|
||||||
.directory
|
.directory
|
||||||
*/.directory
|
*/.directory
|
||||||
|
|||||||
353
CHANGELOG
@@ -1,3 +1,356 @@
|
|||||||
|
KeePassDX(4.0.0)
|
||||||
|
* New UX/UI with Material 3 #1183 #1529 #1428 #1441 #1607
|
||||||
|
* Material You theme (follow system colors) #1469
|
||||||
|
* Refactoring inner code #1371
|
||||||
|
* Migration to API 33
|
||||||
|
* Cut, copy and delete from search #891 #1308 #1263
|
||||||
|
* Fix behaviors #1351 #874 #1327
|
||||||
|
* Fix bugs #1589 #1584 #1545 #1563 #1371 #1609
|
||||||
|
|
||||||
|
KeePassDX(3.5.1)
|
||||||
|
* Fix action dialog with YubiKey challenge-response #1506
|
||||||
|
|
||||||
|
KeePassDX(3.5.0)
|
||||||
|
* Support YubiKey challenge-response #8 #137
|
||||||
|
* Better exception management during database save #1346
|
||||||
|
* Add "Screenshot mode" setting #459 #1377 #1354 (Thx @GianpaMX)
|
||||||
|
* Hide clipboard sensitive text when copy entry field #1386
|
||||||
|
* Fix attachment download button #1401
|
||||||
|
* Add monochrome icon #1403 #1404 (Thx @Sandelinos)
|
||||||
|
* Fix lock with back button #1412 #1414 (Thx @ryg-git)
|
||||||
|
* Vanadium compatibility #1447 (Thx @flawedworld)
|
||||||
|
|
||||||
|
KeePassDX(3.4.5)
|
||||||
|
* Fix custom data in group (fix KeeShare) #1335
|
||||||
|
* Fix device credential unlocking #1344
|
||||||
|
* New clipboard manager #1343
|
||||||
|
* Keep screen on by default when viewing an entry
|
||||||
|
* Change the order of the search filters
|
||||||
|
* Fix searchable selection
|
||||||
|
|
||||||
|
KeePassDX(3.4.4)
|
||||||
|
* Fix crash in New Android 13 #1321
|
||||||
|
* Better backstack management for selection mode
|
||||||
|
* Prevent Tapjacking #1318
|
||||||
|
* Small changes #1298
|
||||||
|
|
||||||
|
KeePassDX(3.4.3)
|
||||||
|
* Remove "Select share info" setting for Magikeyboard #1304
|
||||||
|
* Fix quick search and better loadGroup implementation #1302
|
||||||
|
* Fix small bugs
|
||||||
|
|
||||||
|
KeePassDX(3.4.2)
|
||||||
|
* Fix service parameter and workflow to remove notification when service is killed
|
||||||
|
* Fix color
|
||||||
|
|
||||||
|
KeePassDX(3.4.1)
|
||||||
|
* Fix search mode with Magikeyboard #1292
|
||||||
|
* Fix select another entry with Magikeyboard #1293
|
||||||
|
* Fix unexpected lock with Magikeyboard #1294
|
||||||
|
* Small UI changes
|
||||||
|
|
||||||
|
KeePassDX(3.4.0)
|
||||||
|
* Passphrase implementation #218
|
||||||
|
* Show visual password strength indicator with entropy #631 #869 #454 #1270
|
||||||
|
* Dynamically save password generator configuration #618 #696
|
||||||
|
* Add advanced password filters #1052 #448 #983 #271 #539
|
||||||
|
* Better search implementation #175 #1254 #1267
|
||||||
|
* Manage package name from Magikeyboard #1010 #1261
|
||||||
|
* Ask confirmation to lock if changes without save #970
|
||||||
|
* Fix small bugs #1282
|
||||||
|
|
||||||
|
KeePassDX(3.3.3)
|
||||||
|
* Fix shared otpauth link if database not open #1274
|
||||||
|
* Ellipsize attachment name #1253
|
||||||
|
* Add a warning to inform about KeyStore usage #1269
|
||||||
|
* Fingerprint unlock no more by default #1273
|
||||||
|
* Tabs to show main and advanced content separately
|
||||||
|
* Fix URL color
|
||||||
|
|
||||||
|
KeePassDX(3.3.2)
|
||||||
|
* Merge KeePassDX & KeePassDX Pro #1257
|
||||||
|
* Create new Contributor Pro app
|
||||||
|
|
||||||
|
KeePassDX(3.3.1)
|
||||||
|
* Fix Japanese keyboard in search #1248
|
||||||
|
* Better OOM management #256
|
||||||
|
* Fix filters #1249
|
||||||
|
* Fix temp advanced unlocking #1245
|
||||||
|
* Best autofill recognition #1250
|
||||||
|
* Workaround to fill OTP token in multiple fields with Magikeyboard (long press) #1158
|
||||||
|
|
||||||
|
KeePassDX(3.3.0)
|
||||||
|
* Quick search and dynamic filters #163 #462 #521
|
||||||
|
* Keep search context #1141
|
||||||
|
* Add searchable groups #905 #1006
|
||||||
|
* Search with regular expression #175
|
||||||
|
* Merge from file and save as copy #1221 #1204 #840
|
||||||
|
* Fix custom data #1236
|
||||||
|
* Fix education hints #1192
|
||||||
|
* Fix save and app instance in selection mode
|
||||||
|
* New UI and fix styles
|
||||||
|
* Add "Simple" and "Reply" themes
|
||||||
|
|
||||||
|
KeePassDX(3.2.0)
|
||||||
|
* Manage data merge #840 #977
|
||||||
|
* Manage Tags #633
|
||||||
|
* Inherit colors and icon from template #1213 #1130
|
||||||
|
* Entry colors setting #1207
|
||||||
|
* Setting to keep the screen on when watching the entry #1119
|
||||||
|
* Add path in quick search
|
||||||
|
* Small fixes
|
||||||
|
|
||||||
|
KeePassDX(3.1.0)
|
||||||
|
* Add breadcrumb
|
||||||
|
* Add path in search results #1148
|
||||||
|
* Add group info dialog #1177
|
||||||
|
* Manage colors #64 #913
|
||||||
|
* Fix UI in Android 8 #509
|
||||||
|
* Upgrade libs and SDK to 31 #833
|
||||||
|
* Fix parser of database v1 #1201
|
||||||
|
* Stop asking WRITE_EXTERNAL_STORAGE permission
|
||||||
|
|
||||||
|
KeePassDX(3.0.4)
|
||||||
|
* Fix autofill inline bugs #1173 #1165
|
||||||
|
* Small UI change
|
||||||
|
|
||||||
|
KeePassDX(3.0.3)
|
||||||
|
* Change default Argon2 parameters #1098
|
||||||
|
* Add & edit custom icon name #976
|
||||||
|
* Fix templates #1128 #1133 #1138
|
||||||
|
* Update Autofill compatibility list #725 #1154
|
||||||
|
* Improve fingerprint usage #1137 #1145
|
||||||
|
* Change backup configuration #1144
|
||||||
|
* Add lock button in database notification
|
||||||
|
|
||||||
|
KeePassDX(3.0.2)
|
||||||
|
* Samsung DeX mode #1114 #245 (Thx @chenxiaolong)
|
||||||
|
|
||||||
|
KeePassDX(3.0.1)
|
||||||
|
* Fix text size and smallest margin #1085
|
||||||
|
* Fix number of lines during an edition #1073
|
||||||
|
* Fix Magikeyboard URL auto action #1100
|
||||||
|
* Fix exception after group name change and save #1112
|
||||||
|
* Fix timeout reset #1107
|
||||||
|
* Fix search actions #1091 #1092
|
||||||
|
* Small changes #1106 #1085
|
||||||
|
|
||||||
|
KeePassDX(3.0.0)
|
||||||
|
* Add / Manage dynamic templates #191
|
||||||
|
* Manually select RecycleBin group and Templates group #191
|
||||||
|
* Setting to display OTP Token in list #655
|
||||||
|
* Fix timeout in dialogs #716
|
||||||
|
* Check URI permissions #626
|
||||||
|
* Better autofill implementation #943 #946 #984 #1070 (Thx @uduerholz)
|
||||||
|
* Improvements #680 #1035 #1043 #942 #1021 #1027 #1046 #1082 #1083 (Thx @chenxiaolong)
|
||||||
|
|
||||||
|
KeePassDX(2.10.5)
|
||||||
|
* Increase the saving speed of database #1028
|
||||||
|
* Fix advanced unlocking by device credential #1029
|
||||||
|
|
||||||
|
KeePassDX(2.10.4)
|
||||||
|
* Hot fix to increase the opening speed of database #1028
|
||||||
|
|
||||||
|
KeePassDX(2.10.3)
|
||||||
|
* Improve Magikeyboard options description #1022 #1023 (Thx @djibux)
|
||||||
|
* Fix database opened without notification (database is now closed when screen is killed in background #1025)
|
||||||
|
* Fix biometric prompt #1018
|
||||||
|
|
||||||
|
KeePassDX(2.10.2)
|
||||||
|
* Fix search fields references #987
|
||||||
|
* Fix Auto-Types with same key #997
|
||||||
|
|
||||||
|
KeePassDX(2.10.1)
|
||||||
|
* Fix parcelable with custom data #986
|
||||||
|
|
||||||
|
KeePassDX(2.10.0)
|
||||||
|
* Manage new database format 4.1 #956
|
||||||
|
* Fix show button consistency #980
|
||||||
|
* Fix persistent notification #979
|
||||||
|
|
||||||
|
KeePassDX(2.9.20)
|
||||||
|
* Fix search with non-latin chars #971
|
||||||
|
* Fix action mode with search #972 (rollback ignore accents #945)
|
||||||
|
* Fix timeout with 0s #974
|
||||||
|
|
||||||
|
KeePassDX(2.9.19)
|
||||||
|
* Fix search slowdown #964
|
||||||
|
* Fix closing notification after lock request #965
|
||||||
|
* Better temp advanced unlocking code implementation #965
|
||||||
|
* Fix OTP token generation #967
|
||||||
|
|
||||||
|
KeePassDX(2.9.18)
|
||||||
|
* Move groups #658
|
||||||
|
* Improve autofill recognition #960
|
||||||
|
* Remove diacritical marks in search string #945
|
||||||
|
* Fix search in references #962
|
||||||
|
* Fix themes in Libre version
|
||||||
|
|
||||||
|
KeePassDX(2.9.17)
|
||||||
|
* Import / Export app properties #839
|
||||||
|
* Force twofish padding compatibility #955
|
||||||
|
* Better timeout preference #579
|
||||||
|
|
||||||
|
KeePassDX(2.9.16)
|
||||||
|
* Fix small bugs #948
|
||||||
|
|
||||||
|
KeePassDX(2.9.15)
|
||||||
|
* Fix themes #935 #926
|
||||||
|
* Decrease default clipboard time #934
|
||||||
|
* Better opening performance #929 #933
|
||||||
|
* Fix memory usage setting #941
|
||||||
|
|
||||||
|
KeePassDX(2.9.14)
|
||||||
|
* Add custom icons #96
|
||||||
|
* Dark Themes #532 #714
|
||||||
|
* Fix binary deduplication #715
|
||||||
|
* Fix IconId #901
|
||||||
|
* Resize image stream dynamically to prevent slowdown #919
|
||||||
|
* Small changes #795 #900 #903 #909 #914
|
||||||
|
|
||||||
|
KeePassDX(2.9.13)
|
||||||
|
* Binary image viewer #473 #749
|
||||||
|
* Fix TOTP plugin settings #878
|
||||||
|
* Allow Emoji #796
|
||||||
|
* Scroll and better UI in entry edition screen #876
|
||||||
|
* Better UI #876
|
||||||
|
* Fix themes and add Purple Dark #889
|
||||||
|
* Allow OTP with many padding #585
|
||||||
|
* Add notes in groups #734
|
||||||
|
|
||||||
|
KeePassDX(2.9.12)
|
||||||
|
* Fix OTP token type #863
|
||||||
|
* Fix auto open biometric prompt #862
|
||||||
|
* Fix back appearance setting #865
|
||||||
|
* Fix orientation change in settings #872
|
||||||
|
* Change memory unit to MiB #851
|
||||||
|
* Small changes #642
|
||||||
|
|
||||||
|
KeePassDX(2.9.11)
|
||||||
|
* Add Keyfile XML version 2 (fix hex) #844
|
||||||
|
* Fix hex Keyfile #861
|
||||||
|
|
||||||
|
KeePassDX(2.9.10)
|
||||||
|
* Try to fix autofill #852
|
||||||
|
* Fix database change dialog displayed too often #853
|
||||||
|
|
||||||
|
KeePassDX(2.9.9)
|
||||||
|
* Detect file changes and reload database #794
|
||||||
|
* Inline suggestions autofill with compatible keyboard (Android R) #827
|
||||||
|
* Add Keyfile XML version 2 #844
|
||||||
|
* Fix binaries of 64 bytes #835
|
||||||
|
* Special search in title fields #830
|
||||||
|
* Priority to OTP button in notifications #845
|
||||||
|
* Fix OTP generation for long secret key #848
|
||||||
|
* Fix small bugs #849
|
||||||
|
|
||||||
|
KeePassDX(2.9.8)
|
||||||
|
* Fix specific attachments with kdbx3.1 databases #828
|
||||||
|
* Fix small bugs
|
||||||
|
|
||||||
|
KeePassDX(2.9.7)
|
||||||
|
* Remove write permission since Android 10 #823
|
||||||
|
* Fix small bugs
|
||||||
|
|
||||||
|
KeePassDX(2.9.6)
|
||||||
|
* Fix KeyFile bug #820
|
||||||
|
|
||||||
|
KeePassDX(2.9.5)
|
||||||
|
* Unlock database by device credentials (PIN/Password/Pattern) with Android M+ #102 #152 #811
|
||||||
|
* Prevent auto switch back to previous keyboard if otp field exists #814
|
||||||
|
* Fix timeout reset #817
|
||||||
|
|
||||||
|
KeePassDX(2.9.4)
|
||||||
|
* Fix small bugs #812
|
||||||
|
* Argon2ID implementation #791
|
||||||
|
|
||||||
|
KeePassDX(2.9.3)
|
||||||
|
* Unlock database by device credentials (PIN/Password/Pattern) #779 #102
|
||||||
|
* Advanced unlock with timeout #102 #437 #566
|
||||||
|
* Remove default database parameter when the file is no longer accessible #803
|
||||||
|
* Move OTP button to the first view level in Magikeyboard #587
|
||||||
|
* Tooltips for Magikeyboard #586
|
||||||
|
* Fix small bugs #805
|
||||||
|
|
||||||
|
KeePassDX(2.9.2)
|
||||||
|
* Managing OTP links from QR applications #556
|
||||||
|
* Prevent manual creation of existing field name #718
|
||||||
|
* Harmonization of field names #789
|
||||||
|
* Different channels for each type of notification #688
|
||||||
|
* Fix OTP #780 #781
|
||||||
|
* Fix same save shared info #783
|
||||||
|
* Fix switch back to previous keyboard #782
|
||||||
|
* Fix read only #792
|
||||||
|
* Better UI #719 #534 #617 #793
|
||||||
|
|
||||||
|
KeePassDX(2.9.1)
|
||||||
|
* Copy password from generator #697
|
||||||
|
* Fix Magikeyboard not fully visible #772
|
||||||
|
* Fix change font size #770
|
||||||
|
* Fix crash #774
|
||||||
|
* Small fixes #771
|
||||||
|
|
||||||
|
KeePassDX(2.9)
|
||||||
|
* Upgrade to Android API 30 #723
|
||||||
|
* Save new credentials with Autofill #524
|
||||||
|
* Setting to close database after Autofill selection #755
|
||||||
|
* Setting to switch keyboard when database is locked #625
|
||||||
|
* Fix biometric issues #724 #740 #731
|
||||||
|
* Fix autofill #725 #551
|
||||||
|
* Fix subdomain search #728
|
||||||
|
* Fix backup search #759
|
||||||
|
* Small fixes and translations #732 #736 #737 #738 #742 #767
|
||||||
|
|
||||||
|
KeePassDX(2.8.7)
|
||||||
|
* Downgrade to Android API 29 (crash on startup with API 30 on some devices)
|
||||||
|
Sorry for the inconvenience
|
||||||
|
|
||||||
|
KeePassDX(2.8.6)
|
||||||
|
* Fix Autofill recognition #712
|
||||||
|
* Keep value after renaming custom field #709
|
||||||
|
* Prevent random binary bug #713
|
||||||
|
* Fix dialog background #717
|
||||||
|
* Better domain recognition for autofill #702
|
||||||
|
* Write custom data #651
|
||||||
|
* Fix autolink #720
|
||||||
|
|
||||||
|
KeePassDX(2.8.5)
|
||||||
|
* Fix Base 64 #708
|
||||||
|
|
||||||
|
KeePassDX(2.8.4)
|
||||||
|
* Fix incomplete attachment deletion #684
|
||||||
|
* Fix opening database v1 without backup folder #692
|
||||||
|
* Fix ANR during first entry education #685
|
||||||
|
* Entry edition as fragment and manual views to fix focus #686
|
||||||
|
* Fix opening database with corrupted attachment #691
|
||||||
|
* Manage empty keyfile #679
|
||||||
|
|
||||||
|
KeePassDX(2.8.3)
|
||||||
|
* Upload attachments
|
||||||
|
* Visibility button for each hidden field
|
||||||
|
* Fix read header file
|
||||||
|
* Fix deletion in KDB database
|
||||||
|
* Fix minor issues
|
||||||
|
|
||||||
|
KeePassDX(2.8.2)
|
||||||
|
* Fix themes / new UI
|
||||||
|
* Fix multiples notifications
|
||||||
|
* 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)
|
KeePassDX(2.7)
|
||||||
* Add blocklists for autofill
|
* Add blocklists for autofill
|
||||||
* Add autofill compatibility mode (usefull for Browser not compatible)
|
* Add autofill compatibility mode (usefull for Browser not compatible)
|
||||||
|
|||||||
10
Gemfile
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Autogenerated by fastlane
|
||||||
|
#
|
||||||
|
# Ensure this file is checked in to source control!
|
||||||
|
|
||||||
|
source "https://rubygems.org"
|
||||||
|
|
||||||
|
gem 'fastlane'
|
||||||
|
|
||||||
|
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
|
||||||
|
eval_gemfile(plugins_path) if File.exist?(plugins_path)
|
||||||
220
Gemfile.lock
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
GEM
|
||||||
|
remote: https://rubygems.org/
|
||||||
|
specs:
|
||||||
|
CFPropertyList (3.0.6)
|
||||||
|
rexml
|
||||||
|
addressable (2.8.4)
|
||||||
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
|
artifactory (3.0.15)
|
||||||
|
atomos (0.1.3)
|
||||||
|
aws-eventstream (1.2.0)
|
||||||
|
aws-partitions (1.794.0)
|
||||||
|
aws-sdk-core (3.180.0)
|
||||||
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
|
aws-sigv4 (~> 1.5)
|
||||||
|
jmespath (~> 1, >= 1.6.1)
|
||||||
|
aws-sdk-kms (1.71.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.177.0)
|
||||||
|
aws-sigv4 (~> 1.1)
|
||||||
|
aws-sdk-s3 (1.132.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.179.0)
|
||||||
|
aws-sdk-kms (~> 1)
|
||||||
|
aws-sigv4 (~> 1.6)
|
||||||
|
aws-sigv4 (1.6.0)
|
||||||
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
|
babosa (1.0.4)
|
||||||
|
claide (1.1.0)
|
||||||
|
colored (1.2)
|
||||||
|
colored2 (3.1.2)
|
||||||
|
commander (4.6.0)
|
||||||
|
highline (~> 2.0.0)
|
||||||
|
declarative (0.0.20)
|
||||||
|
digest-crc (0.6.5)
|
||||||
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
|
domain_name (0.5.20190701)
|
||||||
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
|
dotenv (2.8.1)
|
||||||
|
emoji_regex (3.2.3)
|
||||||
|
excon (0.100.0)
|
||||||
|
faraday (1.10.3)
|
||||||
|
faraday-em_http (~> 1.0)
|
||||||
|
faraday-em_synchrony (~> 1.0)
|
||||||
|
faraday-excon (~> 1.1)
|
||||||
|
faraday-httpclient (~> 1.0)
|
||||||
|
faraday-multipart (~> 1.0)
|
||||||
|
faraday-net_http (~> 1.0)
|
||||||
|
faraday-net_http_persistent (~> 1.0)
|
||||||
|
faraday-patron (~> 1.0)
|
||||||
|
faraday-rack (~> 1.0)
|
||||||
|
faraday-retry (~> 1.0)
|
||||||
|
ruby2_keywords (>= 0.0.4)
|
||||||
|
faraday-cookie_jar (0.0.7)
|
||||||
|
faraday (>= 0.8.0)
|
||||||
|
http-cookie (~> 1.0.0)
|
||||||
|
faraday-em_http (1.0.0)
|
||||||
|
faraday-em_synchrony (1.0.0)
|
||||||
|
faraday-excon (1.1.0)
|
||||||
|
faraday-httpclient (1.0.1)
|
||||||
|
faraday-multipart (1.0.4)
|
||||||
|
multipart-post (~> 2)
|
||||||
|
faraday-net_http (1.0.1)
|
||||||
|
faraday-net_http_persistent (1.2.0)
|
||||||
|
faraday-patron (1.0.0)
|
||||||
|
faraday-rack (1.0.0)
|
||||||
|
faraday-retry (1.0.3)
|
||||||
|
faraday_middleware (1.2.0)
|
||||||
|
faraday (~> 1.0)
|
||||||
|
fastimage (2.2.7)
|
||||||
|
fastlane (2.214.0)
|
||||||
|
CFPropertyList (>= 2.3, < 4.0.0)
|
||||||
|
addressable (>= 2.8, < 3.0.0)
|
||||||
|
artifactory (~> 3.0)
|
||||||
|
aws-sdk-s3 (~> 1.0)
|
||||||
|
babosa (>= 1.0.3, < 2.0.0)
|
||||||
|
bundler (>= 1.12.0, < 3.0.0)
|
||||||
|
colored
|
||||||
|
commander (~> 4.6)
|
||||||
|
dotenv (>= 2.1.1, < 3.0.0)
|
||||||
|
emoji_regex (>= 0.1, < 4.0)
|
||||||
|
excon (>= 0.71.0, < 1.0.0)
|
||||||
|
faraday (~> 1.0)
|
||||||
|
faraday-cookie_jar (~> 0.0.6)
|
||||||
|
faraday_middleware (~> 1.0)
|
||||||
|
fastimage (>= 2.1.0, < 3.0.0)
|
||||||
|
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||||
|
google-apis-androidpublisher_v3 (~> 0.3)
|
||||||
|
google-apis-playcustomapp_v1 (~> 0.1)
|
||||||
|
google-cloud-storage (~> 1.31)
|
||||||
|
highline (~> 2.0)
|
||||||
|
json (< 3.0.0)
|
||||||
|
jwt (>= 2.1.0, < 3)
|
||||||
|
mini_magick (>= 4.9.4, < 5.0.0)
|
||||||
|
multipart-post (>= 2.0.0, < 3.0.0)
|
||||||
|
naturally (~> 2.2)
|
||||||
|
optparse (~> 0.1.1)
|
||||||
|
plist (>= 3.1.0, < 4.0.0)
|
||||||
|
rubyzip (>= 2.0.0, < 3.0.0)
|
||||||
|
security (= 0.1.3)
|
||||||
|
simctl (~> 1.6.3)
|
||||||
|
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||||
|
terminal-table (>= 1.4.5, < 2.0.0)
|
||||||
|
tty-screen (>= 0.6.3, < 1.0.0)
|
||||||
|
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||||
|
word_wrap (~> 1.0.0)
|
||||||
|
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||||
|
xcpretty (~> 0.3.0)
|
||||||
|
xcpretty-travis-formatter (>= 0.0.3)
|
||||||
|
fastlane-plugin-versioning_android (0.1.1)
|
||||||
|
gh_inspector (1.1.3)
|
||||||
|
google-apis-androidpublisher_v3 (0.46.0)
|
||||||
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
|
google-apis-core (0.11.1)
|
||||||
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
|
httpclient (>= 2.8.1, < 3.a)
|
||||||
|
mini_mime (~> 1.0)
|
||||||
|
representable (~> 3.0)
|
||||||
|
retriable (>= 2.0, < 4.a)
|
||||||
|
rexml
|
||||||
|
webrick
|
||||||
|
google-apis-iamcredentials_v1 (0.17.0)
|
||||||
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
|
google-apis-playcustomapp_v1 (0.13.0)
|
||||||
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
|
google-apis-storage_v1 (0.19.0)
|
||||||
|
google-apis-core (>= 0.9.0, < 2.a)
|
||||||
|
google-cloud-core (1.6.0)
|
||||||
|
google-cloud-env (~> 1.0)
|
||||||
|
google-cloud-errors (~> 1.0)
|
||||||
|
google-cloud-env (1.6.0)
|
||||||
|
faraday (>= 0.17.3, < 3.0)
|
||||||
|
google-cloud-errors (1.3.1)
|
||||||
|
google-cloud-storage (1.44.0)
|
||||||
|
addressable (~> 2.8)
|
||||||
|
digest-crc (~> 0.4)
|
||||||
|
google-apis-iamcredentials_v1 (~> 0.1)
|
||||||
|
google-apis-storage_v1 (~> 0.19.0)
|
||||||
|
google-cloud-core (~> 1.6)
|
||||||
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
|
mini_mime (~> 1.0)
|
||||||
|
googleauth (1.7.0)
|
||||||
|
faraday (>= 0.17.3, < 3.a)
|
||||||
|
jwt (>= 1.4, < 3.0)
|
||||||
|
memoist (~> 0.16)
|
||||||
|
multi_json (~> 1.11)
|
||||||
|
os (>= 0.9, < 2.0)
|
||||||
|
signet (>= 0.16, < 2.a)
|
||||||
|
highline (2.0.3)
|
||||||
|
http-cookie (1.0.5)
|
||||||
|
domain_name (~> 0.5)
|
||||||
|
httpclient (2.8.3)
|
||||||
|
jmespath (1.6.2)
|
||||||
|
json (2.6.3)
|
||||||
|
jwt (2.7.1)
|
||||||
|
memoist (0.16.2)
|
||||||
|
mini_magick (4.12.0)
|
||||||
|
mini_mime (1.1.2)
|
||||||
|
multi_json (1.15.0)
|
||||||
|
multipart-post (2.3.0)
|
||||||
|
nanaimo (0.3.0)
|
||||||
|
naturally (2.2.1)
|
||||||
|
optparse (0.1.1)
|
||||||
|
os (1.1.4)
|
||||||
|
plist (3.7.0)
|
||||||
|
public_suffix (5.0.3)
|
||||||
|
rake (13.0.6)
|
||||||
|
representable (3.2.0)
|
||||||
|
declarative (< 0.1.0)
|
||||||
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
|
uber (< 0.2.0)
|
||||||
|
retriable (3.1.2)
|
||||||
|
rexml (3.2.6)
|
||||||
|
rouge (2.0.7)
|
||||||
|
ruby2_keywords (0.0.5)
|
||||||
|
rubyzip (2.3.2)
|
||||||
|
security (0.1.3)
|
||||||
|
signet (0.17.0)
|
||||||
|
addressable (~> 2.8)
|
||||||
|
faraday (>= 0.17.5, < 3.a)
|
||||||
|
jwt (>= 1.5, < 3.0)
|
||||||
|
multi_json (~> 1.10)
|
||||||
|
simctl (1.6.10)
|
||||||
|
CFPropertyList
|
||||||
|
naturally
|
||||||
|
terminal-notifier (2.0.0)
|
||||||
|
terminal-table (1.8.0)
|
||||||
|
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||||
|
trailblazer-option (0.1.2)
|
||||||
|
tty-cursor (0.7.1)
|
||||||
|
tty-screen (0.8.1)
|
||||||
|
tty-spinner (0.9.3)
|
||||||
|
tty-cursor (~> 0.7)
|
||||||
|
uber (0.1.0)
|
||||||
|
unf (0.1.4)
|
||||||
|
unf_ext
|
||||||
|
unf_ext (0.0.8.2)
|
||||||
|
unicode-display_width (1.8.0)
|
||||||
|
webrick (1.8.1)
|
||||||
|
word_wrap (1.0.0)
|
||||||
|
xcodeproj (1.22.0)
|
||||||
|
CFPropertyList (>= 2.3.3, < 4.0)
|
||||||
|
atomos (~> 0.1.3)
|
||||||
|
claide (>= 1.0.2, < 2.0)
|
||||||
|
colored2 (~> 3.1)
|
||||||
|
nanaimo (~> 0.3.0)
|
||||||
|
rexml (~> 3.2.4)
|
||||||
|
xcpretty (0.3.0)
|
||||||
|
rouge (~> 2.0.7)
|
||||||
|
xcpretty-travis-formatter (1.0.1)
|
||||||
|
xcpretty (~> 0.2, >= 0.0.7)
|
||||||
|
|
||||||
|
PLATFORMS
|
||||||
|
ruby
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
fastlane
|
||||||
|
fastlane-plugin-versioning_android
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
2.1.4
|
||||||
202
LICENSES/LICENSE_APACHE
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
7
LICENSES/LICENSE_BOUNCY_CASTLE
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Copyright (c) 2000 - 2011 The Legion Of The Bouncy Castle (http://www.bouncycastle.org)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
111
LICENSES/LICENSE_ICON_PACK_CLASSIC
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
Files ic00.png to ic61.png under res/drawable, res/drawable-hdpi and res/drawable-ldpi
|
||||||
|
|
||||||
|
TITLE: NUVOLA ICON THEME for KDE 3.x
|
||||||
|
AUTHOR: David Vignoni | ICON KING
|
||||||
|
SITE: http://www.icon-king.com
|
||||||
|
MAILING LIST: http://mail.icon-king.com/mailman/listinfo/nuvola_icon-king.com
|
||||||
|
|
||||||
|
Copyright (c) 2003-2004 David Vignoni.
|
||||||
|
|
||||||
|
This library is free software; you can redistribute it and/or
|
||||||
|
modify it under the terms of the GNU Lesser General Public
|
||||||
|
License as published by the Free Software Foundation,
|
||||||
|
version 2.1 of the License.
|
||||||
|
This library is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
Lesser General Public License for more details.
|
||||||
|
You should have received a copy of the GNU Lesser General Public
|
||||||
|
License along with this library (see the the license.txt file); if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#######**** NOTE THIS ADD-ON ****#######
|
||||||
|
The GNU Lesser General Public License or LGPL is written for software libraries
|
||||||
|
in the first place. The LGPL has to be considered valid for this artwork
|
||||||
|
library too.
|
||||||
|
Nuvola icon theme for KDE 3.x is a special kind of software library, it is an
|
||||||
|
artwork library, it's elements can be used in a Graphical User Interface, or
|
||||||
|
GUI.
|
||||||
|
Source code, for this library means:
|
||||||
|
- raster png image* .
|
||||||
|
The LGPL in some sections obliges you to make the files carry
|
||||||
|
notices. With images this is in some cases impossible or hardly usefull.
|
||||||
|
With this library a notice is placed at a prominent place in the directory
|
||||||
|
containing the elements. You may follow this practice.
|
||||||
|
The exception in section 6 of the GNU Lesser General Public License covers
|
||||||
|
the use of elements of this art library in a GUI.
|
||||||
|
dave [at] icon-king.com
|
||||||
|
|
||||||
|
Date: 15 october 2004
|
||||||
|
Version: 1.0
|
||||||
|
|
||||||
|
DESCRIPTION:
|
||||||
|
|
||||||
|
Icon theme for KDE 3.x.
|
||||||
|
Icons where designed using Adobe Illustrator, and then exported to PNG format.
|
||||||
|
Icons shadows and minor corrections were done using Adobe Photoshop.
|
||||||
|
Kiconedit was used to correct some 16x16 and 22x22 icons.
|
||||||
|
|
||||||
|
LICENSE
|
||||||
|
|
||||||
|
Released under GNU Lesser General Public License (LGPL)
|
||||||
|
Look at the license.txt file.
|
||||||
|
|
||||||
|
CONTACT
|
||||||
|
|
||||||
|
David Vignoni
|
||||||
|
e-mail : david [at] icon-king.com
|
||||||
|
ICQ : 117761009
|
||||||
|
http: http://www.icon-king.com
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Files ic62.png under res/drawable, res/drawable-hdpi and res/drawable-ldpi
|
||||||
|
|
||||||
|
Based on http://de.wikipedia.org/w/index.php?title=Datei:Tux.svg&filetimestamp=20090927073505
|
||||||
|
|
||||||
|
The copyright holder of this file allows anyone to use it for any purpose,
|
||||||
|
provided that the copyright holders Larry Ewing, Simon Budig and
|
||||||
|
Anja Gerwinski are mentioned.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Files ic63.png under res/drawable, res/drawable-hdpi and res/drawable-ldpi
|
||||||
|
|
||||||
|
Based on http://en.wikipedia.org/wiki/File:ASF-logo.svg
|
||||||
|
|
||||||
|
Apache logo
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License. You may
|
||||||
|
obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
License for the specific language governing permissions and limitations under
|
||||||
|
the License.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Files ic64.png under res/drawable, res/drawable-hdpi and res/drawable-ldpi
|
||||||
|
|
||||||
|
Created by Jeremy JAMET and licensed under the terms of the GPLv3.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Files ic65.png, ic67.png and ic68.png under res/drawable, res/drawable-hdpi and res/drawable-ldpi
|
||||||
|
|
||||||
|
Created by Tobias Selig and licensed under the terms of the GPLv2 or GPLv3.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
File ic66.png under under res/drawable, res/drawable-hdpi and res/drawable-ldpi
|
||||||
|
|
||||||
|
Based on http://commons.wikimedia.org/wiki/File:Dollar_symbol_gold.svg
|
||||||
|
|
||||||
|
Author: Rugby471
|
||||||
|
|
||||||
|
Permission is granted to copy, distribute and/or modify this document under
|
||||||
|
the terms of the GNU Free Documentation License, Version 1.2 or any later
|
||||||
|
version published by the Free Software Foundation; with no Invariant Sections,
|
||||||
|
no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is
|
||||||
|
included in the section entitled "GNU Free Documentation License".
|
||||||
|
|
||||||
|
---
|
||||||
373
LICENSES/LICENSE_MOZILLA_COMPONENTS
Normal file
@@ -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.
|
||||||
19
README.md
@@ -1,6 +1,6 @@
|
|||||||
# Android KeepassDX
|
# Android KeePassDX
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> KeepassDX is a **multi-format KeePass manager for Android devices**. The app allows creating keys and passwords in a secure way by integrating with the Android design standards.
|
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> **Lightweight password manager for Android**, KeePassDX allows editing encrypted data in a single file in KeePass format and fill in the forms in a secure way.
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
|
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
|
||||||
|
|
||||||
@@ -8,18 +8,19 @@
|
|||||||
|
|
||||||
- Create database files / entries and groups.
|
- Create database files / entries and groups.
|
||||||
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
|
- 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, …).
|
- **Compatible** with the majority of alternative programs (KeePass, KeePassXC, KeeWeb, …).
|
||||||
- Allows opening and **copying URI / URL fields quickly**.
|
- Allows opening and **copying URI / URL fields quickly**.
|
||||||
- **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*.
|
- **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*.
|
||||||
- **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA).
|
- **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA).
|
||||||
- Material design with **themes**.
|
- Material design with **themes**.
|
||||||
- **Auto-Fill** and Integration.
|
- **Auto-Fill** and Integration.
|
||||||
- Field filling **keyboard**.
|
- Field filling **keyboard**.
|
||||||
|
- Dynamic **templates**
|
||||||
- **History** of each entry.
|
- **History** of each entry.
|
||||||
- Precise management of **settings**.
|
- Precise management of **settings**.
|
||||||
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
|
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
|
||||||
|
|
||||||
KeepassDX is **open source** and **ad-free**.
|
KeePassDX is **open source** and **ad-free**.
|
||||||
|
|
||||||
## What is KeePassDX?
|
## What is KeePassDX?
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ Optional visual styles are accessible after a contribution (and a congratulatory
|
|||||||
|
|
||||||
* Add features by making a **[pull request](https://help.github.com/articles/about-pull-requests/)**.
|
* 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/)).
|
* 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.
|
* **[Donate](https://www.keepassdx.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.
|
* Buy the **[Pro version](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro)** of KeePassDX.
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
@@ -52,10 +53,12 @@ Optional visual styles are accessible after a contribution (and a congratulatory
|
|||||||
[<img src="https://f-droid.org/badge/get-it-on.png"
|
[<img src="https://f-droid.org/badge/get-it-on.png"
|
||||||
alt="Get it on F-Droid"
|
alt="Get it on F-Droid"
|
||||||
height="80">](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/)
|
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"
|
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
|
||||||
alt="Get it on Google Play"
|
alt="Get it on Google Play"
|
||||||
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
|
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
|
||||||
|
[<img src="https://raw.githubusercontent.com/Kunzisoft/Github-badge/main/get-it-on-github.png"
|
||||||
|
alt="Get it on Github"
|
||||||
|
height="80">](https://github.com/Kunzisoft/KeePassDX/releases)
|
||||||
|
|
||||||
## Frequently Asked Questions
|
## Frequently Asked Questions
|
||||||
|
|
||||||
@@ -71,7 +74,7 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright © 2020 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
Copyright © 2023 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||||
|
|
||||||
This file is part of KeePassDX.
|
This file is part of KeePassDX.
|
||||||
|
|
||||||
@@ -88,4 +91,4 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
*This project is a fork of [KeepassDroid](https://github.com/bpellin/keepassdroid) by bpellin.*
|
*This project is a fork of [KeePassDroid](https://github.com/bpellin/keepassdroid) by bpellin.*
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
theme: jekyll-theme-cayman
|
|
||||||
117
app/build.gradle
@@ -1,25 +1,26 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-parcelize'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 29
|
namespace 'com.kunzisoft.keepass'
|
||||||
buildToolsVersion '29.0.3'
|
compileSdkVersion 33
|
||||||
|
buildToolsVersion "33.0.2"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 14
|
minSdkVersion 15
|
||||||
targetSdkVersion 29
|
targetSdkVersion 33
|
||||||
versionCode = 35
|
versionCode = 123
|
||||||
versionName = "2.7"
|
versionName = "4.0.0"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
testInstrumentationRunner = "android.test.InstrumentationTestRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
|
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
|
||||||
manifestPlaceholders = [ googleAndroidBackupAPIKey:"" ]
|
manifestPlaceholders = [ googleAndroidBackupAPIKey:"unused" ]
|
||||||
|
|
||||||
kapt {
|
kapt {
|
||||||
arguments {
|
arguments {
|
||||||
@@ -29,16 +30,9 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
path "src/main/jni/CMakeLists.txt"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled = false
|
minifyEnabled = false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,28 +42,34 @@ android {
|
|||||||
dimension "version"
|
dimension "version"
|
||||||
applicationIdSuffix = ".libre"
|
applicationIdSuffix = ".libre"
|
||||||
buildConfigField "String", "BUILD_VERSION", "\"libre\""
|
buildConfigField "String", "BUILD_VERSION", "\"libre\""
|
||||||
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_Red_Night\"," +
|
||||||
|
"\"KeepassDXStyle_Reply\"," +
|
||||||
|
"\"KeepassDXStyle_Reply_Night\"," +
|
||||||
|
"\"KeepassDXStyle_Purple\"," +
|
||||||
|
"\"KeepassDXStyle_Purple_Dark\"," +
|
||||||
|
"\"KeepassDXStyle_Dynamic_Light\"," +
|
||||||
|
"\"KeepassDXStyle_Dynamic_Night\"}"
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||||
}
|
}
|
||||||
pro {
|
|
||||||
dimension "version"
|
|
||||||
applicationIdSuffix = ".pro"
|
|
||||||
buildConfigField "String", "BUILD_VERSION", "\"pro\""
|
|
||||||
buildConfigField "boolean", "FULL_VERSION", "true"
|
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "true"
|
|
||||||
buildConfigField "String[]", "STYLES_DISABLED", "{}"
|
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
|
||||||
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIZiXvrQCzSV9LNI6-p7cjTKENZLHIrz_zaqZuQQ" ]
|
|
||||||
}
|
|
||||||
free {
|
free {
|
||||||
dimension "version"
|
dimension "version"
|
||||||
applicationIdSuffix = ".free"
|
applicationIdSuffix = ".free"
|
||||||
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
||||||
buildConfigField "boolean", "FULL_VERSION", "false"
|
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "true"
|
buildConfigField "boolean", "CLOSED_STORE", "true"
|
||||||
buildConfigField "String[]", "STYLES_DISABLED", "{}"
|
buildConfigField "String[]", "STYLES_DISABLED",
|
||||||
|
"{\"KeepassDXStyle_Blue\"," +
|
||||||
|
"\"KeepassDXStyle_Blue_Night\"," +
|
||||||
|
"\"KeepassDXStyle_Red\"," +
|
||||||
|
"\"KeepassDXStyle_Red_Night\"," +
|
||||||
|
"\"KeepassDXStyle_Reply\"," +
|
||||||
|
"\"KeepassDXStyle_Reply_Night\"," +
|
||||||
|
"\"KeepassDXStyle_Purple\"," +
|
||||||
|
"\"KeepassDXStyle_Purple_Dark\"," +
|
||||||
|
"\"KeepassDXStyle_Dynamic_Light\"," +
|
||||||
|
"\"KeepassDXStyle_Dynamic_Night\"}"
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||||
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
|
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
|
||||||
}
|
}
|
||||||
@@ -77,10 +77,13 @@ android {
|
|||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
libre.res.srcDir 'src/libre/res'
|
libre.res.srcDir 'src/libre/res'
|
||||||
pro.res.srcDir 'src/pro/res'
|
|
||||||
free.res.srcDir 'src/free/res'
|
free.res.srcDir 'src/free/res'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests.includeAndroidResources = true
|
||||||
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
@@ -91,38 +94,48 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def room_version = "2.2.5"
|
def room_version = "2.5.1"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
implementation "com.android.support:multidex:1.0.3"
|
||||||
implementation 'androidx.preference:preference:1.1.1'
|
implementation "androidx.appcompat:appcompat:$android_appcompat_version"
|
||||||
|
implementation 'androidx.preference:preference-ktx:1.2.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.1.4'
|
||||||
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
||||||
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'
|
||||||
implementation 'androidx.core:core-ktx:1.2.0'
|
implementation 'androidx.media:media:1.6.0'
|
||||||
// To upgrade with style
|
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||||
implementation 'com.google.android.material:material:1.0.0'
|
implementation "androidx.core:core-ktx:$android_core_version"
|
||||||
|
implementation 'androidx.fragment:fragment-ktx:1.6.0'
|
||||||
|
implementation "com.google.android.material:material:$android_material_version"
|
||||||
|
// Token auto complete
|
||||||
|
// From sources until https://github.com/splitwise/TokenAutoComplete/pull/422 fixed
|
||||||
|
implementation "com.splitwise:tokenautocomplete:4.0.0-beta05"
|
||||||
// Database
|
// Database
|
||||||
implementation "androidx.room:room-runtime:$room_version"
|
implementation "androidx.room:room-runtime:$room_version"
|
||||||
kapt "androidx.room:room-compiler:$room_version"
|
kapt "androidx.room:room-compiler:$room_version"
|
||||||
// Crypto
|
// Autofill
|
||||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01'
|
implementation "androidx.autofill:autofill:1.1.0"
|
||||||
// Time
|
// Time
|
||||||
implementation 'joda-time:joda-time:2.10.6'
|
implementation 'joda-time:joda-time:2.10.13'
|
||||||
// Color
|
// Color
|
||||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.3'
|
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.6'
|
||||||
// Education
|
// Education
|
||||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
|
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3'
|
||||||
// Apache Commons Collections
|
// Apache Commons
|
||||||
implementation 'commons-collections:commons-collections:3.2.2'
|
implementation 'commons-io:commons-io:2.8.0'
|
||||||
// Apache Commons Codec
|
implementation 'commons-codec:commons-codec:1.15'
|
||||||
implementation 'commons-codec:commons-codec:1.14'
|
// Password generator
|
||||||
// Icon pack
|
implementation 'me.gosimple:nbvcxz:1.5.0'
|
||||||
implementation project(path: ':icon-pack-classic')
|
|
||||||
implementation project(path: ':icon-pack-material')
|
// Modules import
|
||||||
|
implementation project(path: ':database')
|
||||||
|
implementation project(path: ':icon-pack')
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
androidTestImplementation 'junit:junit:4.13'
|
androidTestImplementation "androidx.test:runner:$android_test_version"
|
||||||
|
androidTestImplementation "androidx.test:rules:$android_test_version"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 2,
|
||||||
|
"identityHash": "f8fb4aed546de19ae7ca0797f49b26a4",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "file_database_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `database_alias` TEXT NOT NULL, `keyfile_uri` TEXT, `hardware_key` TEXT, `updated` INTEGER NOT NULL, PRIMARY KEY(`database_uri`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "databaseUri",
|
||||||
|
"columnName": "database_uri",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "databaseAlias",
|
||||||
|
"columnName": "database_alias",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "keyFileUri",
|
||||||
|
"columnName": "keyfile_uri",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "hardwareKey",
|
||||||
|
"columnName": "hardware_key",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "updated",
|
||||||
|
"columnName": "updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"database_uri"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "cipher_database",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `encrypted_value` TEXT NOT NULL, `specs_parameters` TEXT NOT NULL, PRIMARY KEY(`database_uri`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "databaseUri",
|
||||||
|
"columnName": "database_uri",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "encryptedValue",
|
||||||
|
"columnName": "encrypted_value",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "specParameters",
|
||||||
|
"columnName": "specs_parameters",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"database_uri"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f8fb4aed546de19ae7ca0797f49b26a4')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,67 +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.crypto
|
|
||||||
|
|
||||||
import org.junit.Assert.assertArrayEquals
|
|
||||||
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.Random
|
|
||||||
|
|
||||||
import junit.framework.TestCase
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.finalkey.AndroidAESKeyTransformer
|
|
||||||
import com.kunzisoft.keepass.crypto.finalkey.NativeAESKeyTransformer
|
|
||||||
|
|
||||||
class AESKeyTest : TestCase() {
|
|
||||||
private lateinit var mRand: Random
|
|
||||||
|
|
||||||
@Throws(Exception::class)
|
|
||||||
override fun setUp() {
|
|
||||||
super.setUp()
|
|
||||||
|
|
||||||
mRand = Random()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun testAES() {
|
|
||||||
// Test both an old and an even number to test my flip variable
|
|
||||||
testAESFinalKey(5)
|
|
||||||
testAESFinalKey(6)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun testAESFinalKey(rounds: Long) {
|
|
||||||
val seed = ByteArray(32)
|
|
||||||
val key = ByteArray(32)
|
|
||||||
val nativeKey: ByteArray?
|
|
||||||
val androidKey: ByteArray?
|
|
||||||
|
|
||||||
mRand.nextBytes(seed)
|
|
||||||
mRand.nextBytes(key)
|
|
||||||
|
|
||||||
val androidAESKey = AndroidAESKeyTransformer()
|
|
||||||
androidKey = androidAESKey.transformMasterKey(seed, key, rounds)
|
|
||||||
|
|
||||||
val nativeAESKey = NativeAESKeyTransformer()
|
|
||||||
nativeKey = nativeAESKey.transformMasterKey(seed, key, rounds)
|
|
||||||
|
|
||||||
assertArrayEquals("Does not match", androidKey, nativeKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +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.crypto
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
|
||||||
|
|
||||||
import junit.framework.TestCase
|
|
||||||
|
|
||||||
import java.security.InvalidAlgorithmParameterException
|
|
||||||
import java.security.InvalidKeyException
|
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.util.Random
|
|
||||||
|
|
||||||
import javax.crypto.BadPaddingException
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.IllegalBlockSizeException
|
|
||||||
import javax.crypto.NoSuchPaddingException
|
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
import org.junit.Assert.assertArrayEquals
|
|
||||||
|
|
||||||
class AESTest : TestCase() {
|
|
||||||
|
|
||||||
private val mRand = Random()
|
|
||||||
|
|
||||||
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, IllegalBlockSizeException::class, BadPaddingException::class, InvalidAlgorithmParameterException::class)
|
|
||||||
fun testEncrypt() {
|
|
||||||
// Test above below and at the blocksize
|
|
||||||
testFinal(15)
|
|
||||||
testFinal(16)
|
|
||||||
testFinal(17)
|
|
||||||
|
|
||||||
// Test random larger sizes
|
|
||||||
val size = mRand.nextInt(494) + 18
|
|
||||||
testFinal(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, IllegalBlockSizeException::class, BadPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
|
||||||
private fun testFinal(dataSize: Int) {
|
|
||||||
|
|
||||||
// Generate some input
|
|
||||||
val input = ByteArray(dataSize)
|
|
||||||
mRand.nextBytes(input)
|
|
||||||
|
|
||||||
// Generate key
|
|
||||||
val keyArray = ByteArray(32)
|
|
||||||
mRand.nextBytes(keyArray)
|
|
||||||
val key = SecretKeySpec(keyArray, "AES")
|
|
||||||
|
|
||||||
// Generate IV
|
|
||||||
val ivArray = ByteArray(16)
|
|
||||||
mRand.nextBytes(ivArray)
|
|
||||||
val iv = IvParameterSpec(ivArray)
|
|
||||||
|
|
||||||
val android = CipherFactory.getInstance("AES/CBC/PKCS5Padding", true)
|
|
||||||
android.init(Cipher.ENCRYPT_MODE, key, iv)
|
|
||||||
val outAndroid = android.doFinal(input, 0, dataSize)
|
|
||||||
|
|
||||||
val nat = CipherFactory.getInstance("AES/CBC/PKCS5Padding")
|
|
||||||
nat.init(Cipher.ENCRYPT_MODE, key, iv)
|
|
||||||
val outNative = nat.doFinal(input, 0, dataSize)
|
|
||||||
|
|
||||||
assertArrayEquals("Arrays differ on size: $dataSize", outAndroid, outNative)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +1,31 @@
|
|||||||
<?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"
|
android:width="108dp"
|
||||||
android:viewportHeight="108"
|
android:height="108dp"
|
||||||
android:width="108dp"
|
android:viewportWidth="120"
|
||||||
android:height="108dp">
|
android:viewportHeight="120">
|
||||||
<group
|
<group
|
||||||
android:translateY="-332">
|
android:translateX="6"
|
||||||
<group
|
android:translateY="8">
|
||||||
android:translateY="332">
|
<path
|
||||||
<path
|
android:fillColor="#24000000"
|
||||||
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:strokeWidth="1.99999297"
|
||||||
android:fillColor="@color/long_shadow"
|
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||||
android:strokeLineJoin="round"
|
<path
|
||||||
android:strokeLineCap="round"
|
android:fillColor="#24000000"
|
||||||
android:strokeMiterLimit="4" />
|
android:strokeWidth="1.99999297"
|
||||||
</group>
|
android:pathData="M63.9961,34.0059 C61.5643,34.096,59.2564,35.102,57.5352,36.8223 C53.7682,40.589,53.7682,46.6982,57.5352,50.4649 C61.3017,54.232,67.4073,54.232,71.1739,50.4649 C74.9409,46.6982,74.9409,40.589,71.1739,36.8223 C69.2766,34.9258,66.6768,33.9054,63.9962,34.0059 Z M68.1992,40.6954 C69.8278,40.6958,71.148,42.016,71.1484,43.6446 C71.148,45.2732,69.8278,46.5934,68.1992,46.5938 C66.5706,46.5934,65.2504,45.2732,65.25,43.6446 C65.2504,42.016,66.5706,40.6958,68.1992,40.6954 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||||
<group
|
</group>
|
||||||
android:scaleX="0.3939503"
|
<group
|
||||||
android:scaleY="0.3939503"
|
android:translateX="6"
|
||||||
android:translateX="33.66343"
|
android:translateY="6">
|
||||||
android:translateY="233.998">
|
<path
|
||||||
<path
|
android:fillColor="#ffa726"
|
||||||
android:pathData="M88.76953 339.91602L4.1718754 424.59766 4.0000004 436 15.400391 435.82813 27.240234 424 40 424l0 -12 12 0 0 -12.73438 34.01172 -33.97656A8 8 0 0 1 84 360a8 8 0 0 1 8 -8 8 8 0 0 1 5.296882 2.01367l2.787098 -2.7832 -11.31445 -11.31445z"
|
android:strokeWidth="1.99999297"
|
||||||
android:fillColor="#eaeaea"
|
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||||
android:strokeWidth="1"
|
<path
|
||||||
android:strokeColor="#58000000" />
|
android:fillColor="#ffffff"
|
||||||
</group>
|
android:strokeWidth="1.99999297"
|
||||||
<group
|
android:pathData="M63.9961,34.0059 C61.5643,34.096,59.2564,35.102,57.5352,36.8223 C53.7682,40.589,53.7682,46.6982,57.5352,50.4649 C61.3017,54.232,67.4073,54.232,71.1739,50.4649 C74.9409,46.6982,74.9409,40.589,71.1739,36.8223 C69.2766,34.9258,66.6768,33.9054,63.9962,34.0059 Z M68.1992,40.6954 C69.8278,40.6958,71.148,42.016,71.1484,43.6446 C71.148,45.2732,69.8278,46.5934,68.1992,46.5938 C66.5706,46.5934,65.2504,45.2732,65.25,43.6446 C65.2504,42.016,66.5706,40.6958,68.1992,40.6954 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||||
android:scaleX="0.3939503"
|
|
||||||
android:scaleY="0.3939503"
|
|
||||||
android:translateX="33.66343"
|
|
||||||
android:translateY="233.998">
|
|
||||||
<path
|
|
||||||
android:pathData="M4.0000004 340L4.1718754 351.40137 88.59863 435.82812 100 436 99.828122 424.59863 15.401367 340.17188Z"
|
|
||||||
android:fillColor="#81c784" />
|
|
||||||
</group>
|
|
||||||
<group
|
|
||||||
android:scaleX="0.3939503"
|
|
||||||
android:scaleY="0.3939503"
|
|
||||||
android:translateX="33.66343"
|
|
||||||
android:translateY="233.998">
|
|
||||||
<path
|
|
||||||
android:pathData="M81.39454 332.00195a27 27 0 0 0 -19.48634 7.90625 27 27 0 0 0 0 38.1836 27 27 0 0 0 38.1836 0 27 27 0 0 0 0 -38.1836 27 27 0 0 0 -18.69726 -7.90625zM92 352a8 8 0 0 1 8 8 8 8 0 0 1 -8 8 8 8 0 0 1 -8 -8 8 8 0 0 1 8 -8z"
|
|
||||||
android:fillColor="#eaeaea"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="#58000000" />
|
|
||||||
</group>
|
|
||||||
</group>
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
15
app/src/free/res/drawable-v24/ic_launcher_monochrome.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="120"
|
||||||
|
android:viewportHeight="120">
|
||||||
|
<group
|
||||||
|
android:translateX="6"
|
||||||
|
android:translateY="6">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeWidth="1.99999297"
|
||||||
|
android:pathData="M63.9961,34.0059 C61.5643,34.096,59.2564,35.102,57.5352,36.8223 C53.7682,40.589,53.7682,46.6982,57.5352,50.4649 C61.3017,54.232,67.4073,54.232,71.1739,50.4649 C74.9409,46.6982,74.9409,40.589,71.1739,36.8223 C69.2766,34.9258,66.6768,33.9054,63.9962,34.0059 Z M68.1992,40.6954 C69.8278,40.6958,71.148,42.016,71.1484,43.6446 C71.148,45.2732,69.8278,46.5934,68.1992,46.5938 C66.5706,46.5934,65.2504,45.2732,65.25,43.6446 C65.2504,42.016,66.5706,40.6958,68.1992,40.6954 Z M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
15
app/src/free/res/drawable/ic_app_key_white_24dp.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="84"
|
||||||
|
android:viewportHeight="84">
|
||||||
|
<group
|
||||||
|
android:translateX="-12"
|
||||||
|
android:translateY="-12">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeWidth="1.99999297"
|
||||||
|
android:pathData="M63.9961,34.0059 C61.5643,34.096,59.2564,35.102,57.5352,36.8223 C53.7682,40.589,53.7682,46.6982,57.5352,50.4649 C61.3017,54.232,67.4073,54.232,71.1739,50.4649 C74.9409,46.6982,74.9409,40.589,71.1739,36.8223 C69.2766,34.9258,66.6768,33.9054,63.9962,34.0059 Z M68.1992,40.6954 C69.8278,40.6958,71.148,42.016,71.1484,43.6446 C71.148,45.2732,69.8278,46.5934,68.1992,46.5938 C66.5706,46.5934,65.2504,45.2732,65.25,43.6446 C65.2504,42.016,66.5706,40.6958,68.1992,40.6954 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
15
app/src/free/res/drawable/ic_app_lock_white_24dp.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="84"
|
||||||
|
android:viewportHeight="84">
|
||||||
|
<group
|
||||||
|
android:translateX="-12"
|
||||||
|
android:translateY="-12">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeWidth="1.99999297"
|
||||||
|
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
BIN
app/src/free/res/drawable/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 13 KiB |
@@ -1,50 +1,31 @@
|
|||||||
<?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"
|
android:width="108dp"
|
||||||
android:viewportHeight="108"
|
android:height="108dp"
|
||||||
android:width="108dp"
|
android:viewportWidth="120"
|
||||||
android:height="108dp">
|
android:viewportHeight="120">
|
||||||
<group
|
<group
|
||||||
android:translateY="-332">
|
android:translateX="6"
|
||||||
<group
|
android:translateY="8">
|
||||||
android:translateY="332">
|
<path
|
||||||
<path
|
android:fillColor="#24000000"
|
||||||
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:strokeWidth="1.99999297"
|
||||||
android:fillColor="@color/long_shadow"
|
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||||
android:strokeLineJoin="round"
|
<path
|
||||||
android:strokeLineCap="round"
|
android:fillColor="#24000000"
|
||||||
android:strokeMiterLimit="4" />
|
android:strokeWidth="1.99999297"
|
||||||
</group>
|
android:pathData="M64.501,35.0576 C63.7095,35.0576,62.918,35.3613,62.3115,35.9678 L55.0127,43.2666 C53.7998,44.4795,53.7998,46.4306,55.0127,47.6436 L62.3115,54.9424 C63.5244,56.1553,65.4775,56.1553,66.6904,54.9424 L73.9873,47.6436 C75.2002,46.4307,75.2002,44.4796,73.9873,43.2666 L66.6904,35.9678 C66.0839,35.3613,65.2924,35.0576,64.5009,35.0576 Z M67.6729,42.6006 C69.3298,42.6006,70.6729,43.9437,70.6729,45.6006 C70.6729,47.2575,69.3298,48.6006,67.6729,48.6006 C66.016,48.6006,64.6729,47.2575,64.6729,45.6006 C64.6729,43.9437,66.016,42.6006,67.6729,42.6006 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||||
<group
|
</group>
|
||||||
android:scaleX="0.3939503"
|
<group
|
||||||
android:scaleY="0.3939503"
|
android:translateX="6"
|
||||||
android:translateX="33.66343"
|
android:translateY="6">
|
||||||
android:translateY="233.998">
|
<path
|
||||||
<path
|
android:fillColor="#ffa726"
|
||||||
android:pathData="M88.76953 339.91602L4.1718754 424.59766 4.0000004 436 15.400391 435.82813 27.240234 424 40 424l0 -12 12 0 0 -12.73438 34.01172 -33.97656A8 8 0 0 1 84 360a8 8 0 0 1 8 -8 8 8 0 0 1 5.296882 2.01367l2.787098 -2.7832 -11.31445 -11.31445z"
|
android:strokeWidth="1.99999297"
|
||||||
android:fillColor="#eaeaea"
|
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||||
android:strokeWidth="1"
|
<path
|
||||||
android:strokeColor="#58000000"/>
|
android:fillColor="#ffffff"
|
||||||
</group>
|
android:strokeWidth="1.99999297"
|
||||||
<group
|
android:pathData="M64.501,35.0576 C63.7095,35.0576,62.918,35.3613,62.3115,35.9678 L55.0127,43.2666 C53.7998,44.4795,53.7998,46.4306,55.0127,47.6436 L62.3115,54.9424 C63.5244,56.1553,65.4775,56.1553,66.6904,54.9424 L73.9873,47.6436 C75.2002,46.4307,75.2002,44.4796,73.9873,43.2666 L66.6904,35.9678 C66.0839,35.3613,65.2924,35.0576,64.5009,35.0576 Z M67.6729,42.6006 C69.3298,42.6006,70.6729,43.9437,70.6729,45.6006 C70.6729,47.2575,69.3298,48.6006,67.6729,48.6006 C66.016,48.6006,64.6729,47.2575,64.6729,45.6006 C64.6729,43.9437,66.016,42.6006,67.6729,42.6006 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||||
android:scaleX="0.3939503"
|
|
||||||
android:scaleY="0.3939503"
|
|
||||||
android:translateX="33.66343"
|
|
||||||
android:translateY="233.998">
|
|
||||||
<path
|
|
||||||
android:pathData="M4.0000004 340L4.1718754 351.40137 88.59863 435.82812 100 436 99.828122 424.59863 15.401367 340.17188Z"
|
|
||||||
android:fillColor="#64b5f6" />
|
|
||||||
</group>
|
|
||||||
<group
|
|
||||||
android:scaleX="0.3939503"
|
|
||||||
android:scaleY="0.3939503"
|
|
||||||
android:translateX="33.66343"
|
|
||||||
android:translateY="233.998">
|
|
||||||
<path
|
|
||||||
android:pathData="M81.39454 332.00195a27 27 0 0 0 -19.48634 7.90625 27 27 0 0 0 0 38.1836 27 27 0 0 0 38.1836 0 27 27 0 0 0 0 -38.1836 27 27 0 0 0 -18.69726 -7.90625zM92 352a8 8 0 0 1 8 8 8 8 0 0 1 -8 8 8 8 0 0 1 -8 -8 8 8 0 0 1 8 -8z"
|
|
||||||
android:fillColor="#eaeaea"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="#58000000" />
|
|
||||||
</group>
|
|
||||||
</group>
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
15
app/src/libre/res/drawable-v24/ic_launcher_monochrome.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="120"
|
||||||
|
android:viewportHeight="120">
|
||||||
|
<group
|
||||||
|
android:translateX="6"
|
||||||
|
android:translateY="6">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeWidth="1.99999297"
|
||||||
|
android:pathData="M64.501,35.0576 C63.7095,35.0576,62.918,35.3613,62.3115,35.9678 L55.0127,43.2666 C53.7998,44.4795,53.7998,46.4306,55.0127,47.6436 L62.3115,54.9424 C63.5244,56.1553,65.4775,56.1553,66.6904,54.9424 L73.9873,47.6436 C75.2002,46.4307,75.2002,44.4796,73.9873,43.2666 L66.6904,35.9678 C66.0839,35.3613,65.2924,35.0576,64.5009,35.0576 Z M67.6729,42.6006 C69.3298,42.6006,70.6729,43.9437,70.6729,45.6006 C70.6729,47.2575,69.3298,48.6006,67.6729,48.6006 C66.016,48.6006,64.6729,47.2575,64.6729,45.6006 C64.6729,43.9437,66.016,42.6006,67.6729,42.6006 Z M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
15
app/src/libre/res/drawable/ic_app_key_white_24dp.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="84"
|
||||||
|
android:viewportHeight="84">
|
||||||
|
<group
|
||||||
|
android:translateX="-12"
|
||||||
|
android:translateY="-12">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeWidth="1.99999297"
|
||||||
|
android:pathData="M64.501,35.0576 C63.7095,35.0576,62.918,35.3613,62.3115,35.9678 L55.0127,43.2666 C53.7998,44.4795,53.7998,46.4306,55.0127,47.6436 L62.3115,54.9424 C63.5244,56.1553,65.4775,56.1553,66.6904,54.9424 L73.9873,47.6436 C75.2002,46.4307,75.2002,44.4796,73.9873,43.2666 L66.6904,35.9678 C66.0839,35.3613,65.2924,35.0576,64.5009,35.0576 Z M67.6729,42.6006 C69.3298,42.6006,70.6729,43.9437,70.6729,45.6006 C70.6729,47.2575,69.3298,48.6006,67.6729,48.6006 C66.016,48.6006,64.6729,47.2575,64.6729,45.6006 C64.6729,43.9437,66.016,42.6006,67.6729,42.6006 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
15
app/src/libre/res/drawable/ic_app_lock_white_24dp.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="84"
|
||||||
|
android:viewportHeight="84">
|
||||||
|
<group
|
||||||
|
android:translateX="-12"
|
||||||
|
android:translateY="-12">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeWidth="1.99999297"
|
||||||
|
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
BIN
app/src/libre/res/drawable/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/green" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/green" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 12 KiB |
@@ -1,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.kunzisoft.keepass"
|
|
||||||
android:installLocation="auto">
|
android:installLocation="auto">
|
||||||
<supports-screens
|
<supports-screens
|
||||||
android:smallScreens="true"
|
android:smallScreens="true"
|
||||||
@@ -10,12 +9,24 @@
|
|||||||
android:anyDensity="true" />
|
android:anyDensity="true" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.FOREGROUND_SERVICE" />
|
android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.USE_BIOMETRIC" />
|
android:name="android.permission.USE_BIOMETRIC" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.VIBRATE"/>
|
android:name="android.permission.VIBRATE"/>
|
||||||
|
<!-- Open apps from links -->
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.CREATE_DOCUMENT" />
|
||||||
|
<data android:mimeType="application/octet-stream" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
@@ -23,31 +34,32 @@
|
|||||||
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_rules"
|
android:fullBackupContent="@xml/old_backup_rules"
|
||||||
|
android:dataExtractionRules="@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="s">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.backup.api_key"
|
android:name="com.google.android.backup.api_key"
|
||||||
android:value="${googleAndroidBackupAPIKey}" />
|
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:label="@string/app_name"
|
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
|
android:exported="true"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:windowSoftInputMode="stateHidden" >
|
android:windowSoftInputMode="stateHidden|stateAlwaysHidden" >
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.PasswordActivity"
|
android:name="com.kunzisoft.keepass.activities.MainCredentialActivity"
|
||||||
|
android:exported="true"
|
||||||
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" />
|
||||||
@@ -104,9 +116,9 @@
|
|||||||
<!-- Main Activity -->
|
<!-- Main Activity -->
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.GroupActivity"
|
android:name="com.kunzisoft.keepass.activities.GroupActivity"
|
||||||
|
android:exported="false"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:windowSoftInputMode="adjustPan"
|
android:windowSoftInputMode="adjustPan">
|
||||||
android:launchMode="singleTask">
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.default_searchable"
|
android:name="android.app.default_searchable"
|
||||||
android:value="com.kunzisoft.keepass.search.SearchResults"
|
android:value="com.kunzisoft.keepass.search.SearchResults"
|
||||||
@@ -122,10 +134,18 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.EntryActivity"
|
android:name="com.kunzisoft.keepass.activities.EntryActivity"
|
||||||
android:configChanges="keyboardHidden" />
|
android:configChanges="keyboardHidden" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.IconPickerActivity"
|
||||||
|
android:configChanges="keyboardHidden" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.KeyGeneratorActivity"
|
||||||
|
android:configChanges="keyboardHidden" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.ImageViewerActivity"
|
||||||
|
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" />
|
||||||
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"
|
||||||
@@ -136,51 +156,66 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
|
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
|
||||||
android:theme="@style/Theme.Transparent"
|
android:theme="@style/Theme.Transparent"
|
||||||
android:configChanges="keyboardHidden" />
|
android:configChanges="keyboardHidden"
|
||||||
|
android:excludeFromRecents="true"/>
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
|
android:name="com.kunzisoft.keepass.settings.AdvancedUnlockSettingsActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity"
|
||||||
|
android:theme="@style/Theme.Transparent" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
|
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
|
||||||
android:theme="@style/Theme.Transparent">
|
android:theme="@style/Theme.Transparent"
|
||||||
|
android:launchMode="singleInstance"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="text/plain" />
|
<data android:mimeType="text/plain" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="otpauth"/>
|
||||||
|
<data android:host="totp"/>
|
||||||
|
<data android:host="hotp"/>
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name="com.kunzisoft.keepass.activities.MagikeyboardLauncherActivity"
|
|
||||||
android:theme="@style/Theme.Transparent" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
||||||
android:label="@string/keyboard_setting_label">
|
android:label="@string/keyboard_setting_label"
|
||||||
|
android:exported="true">
|
||||||
<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
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService"
|
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService"
|
android:name="com.kunzisoft.keepass.services.AttachmentFileNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name=".notifications.AttachmentFileNotificationService"
|
android:name="com.kunzisoft.keepass.services.ClipboardEntryNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService"
|
android:name="com.kunzisoft.keepass.services.AdvancedUnlockNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<!-- Receiver for Autofill -->
|
<!-- Receiver for Autofill -->
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
|
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
|
||||||
android:label="@string/autofill_service_name"
|
android:label="@string/autofill_service_name"
|
||||||
|
android:exported="true"
|
||||||
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.autofill"
|
android:name="android.autofill"
|
||||||
@@ -190,8 +225,9 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.magikeyboard.MagikIME"
|
android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService"
|
||||||
android:label="@string/keyboard_label"
|
android:label="@string/keyboard_label"
|
||||||
|
android:exported="true"
|
||||||
android:permission="android.permission.BIND_INPUT_METHOD" >
|
android:permission="android.permission.BIND_INPUT_METHOD" >
|
||||||
<meta-data android:name="android.view.im"
|
<meta-data android:name="android.view.im"
|
||||||
android:resource="@xml/keyboard_method"/>
|
android:resource="@xml/keyboard_method"/>
|
||||||
@@ -200,9 +236,17 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService"
|
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
<receiver
|
||||||
|
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.app.action.ENTER_KNOX_DESKTOP_MODE" />
|
||||||
|
<action android:name="android.app.action.EXIT_KNOX_DESKTOP_MODE" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
||||||
</application>
|
</application>
|
||||||
|
|||||||
BIN
app/src/main/assets/publicsuffixes
Normal file
1064
app/src/main/java/com/igreenwood/loupe/Loupe.kt
Normal file
@@ -30,6 +30,8 @@ import androidx.core.text.HtmlCompat
|
|||||||
import com.kunzisoft.keepass.BuildConfig
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
||||||
|
import com.kunzisoft.keepass.utils.getPackageInfoCompat
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
class AboutActivity : StylishActivity() {
|
class AboutActivity : StylishActivity() {
|
||||||
@@ -45,10 +47,16 @@ class AboutActivity : StylishActivity() {
|
|||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
|
val appName = if (this.isContributingUser())
|
||||||
|
getString(R.string.app_name) + " " + getString(R.string.app_name_part3)
|
||||||
|
else
|
||||||
|
getString(R.string.app_name)
|
||||||
|
findViewById<TextView>(R.id.activity_about_app_name).text = appName
|
||||||
|
|
||||||
var version: String
|
var version: String
|
||||||
var build: String
|
var build: String
|
||||||
try {
|
try {
|
||||||
version = packageManager.getPackageInfo(packageName, 0).versionName
|
version = packageManager.getPackageInfoCompat(packageName).versionName
|
||||||
build = BuildConfig.BUILD_VERSION
|
build = BuildConfig.BUILD_VERSION
|
||||||
} catch (e: NameNotFoundException) {
|
} catch (e: NameNotFoundException) {
|
||||||
Log.w(javaClass.simpleName, "Unable to get the app or the build version", e)
|
Log.w(javaClass.simpleName, "Unable to get the app or the build version", e)
|
||||||
@@ -70,6 +78,12 @@ class AboutActivity : StylishActivity() {
|
|||||||
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findViewById<TextView>(R.id.activity_about_privacy_text).apply {
|
||||||
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
text = HtmlCompat.fromHtml(getString(R.string.html_about_privacy),
|
||||||
|
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||||
|
}
|
||||||
|
|
||||||
findViewById<TextView>(R.id.activity_about_contribution_text).apply {
|
findViewById<TextView>(R.id.activity_about_contribution_text).apply {
|
||||||
movementMethod = LinkMovementMethod.getInstance()
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
text = HtmlCompat.fromHtml(getString(R.string.html_about_contribution),
|
text = HtmlCompat.fromHtml(getString(R.string.html_about_contribution),
|
||||||
|
|||||||
@@ -23,95 +23,247 @@ import android.app.Activity
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentSender
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.autofill.CompatInlineSuggestionsRequest
|
||||||
import com.kunzisoft.keepass.autofill.KeeAutofillService
|
import com.kunzisoft.keepass.autofill.KeeAutofillService
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.helper.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.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
import com.kunzisoft.keepass.utils.WebDomain
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class AutofillLauncherActivity : AppCompatActivity() {
|
class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||||
|
AutofillHelper.buildActivityResultLauncher(this, true)
|
||||||
|
else null
|
||||||
|
|
||||||
// Build search param
|
override fun applyCustomStyle(): Boolean {
|
||||||
val searchInfo = SearchInfo().apply {
|
return false
|
||||||
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
|
}
|
||||||
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
|
|
||||||
|
override fun finishActivityIfReloadRequested(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
|
// Retrieve selection mode
|
||||||
|
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
|
||||||
|
when (specialMode) {
|
||||||
|
SpecialMode.SELECTION -> {
|
||||||
|
intent.getBundleExtra(KEY_SELECTION_BUNDLE)?.let { bundle ->
|
||||||
|
// To pass extra inline request
|
||||||
|
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
compatInlineSuggestionsRequest = bundle.getParcelableCompat(KEY_INLINE_SUGGESTION)
|
||||||
|
}
|
||||||
|
// Build search param
|
||||||
|
bundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
|
||||||
|
WebDomain.getConcreteWebDomain(
|
||||||
|
this,
|
||||||
|
searchInfo.webDomain
|
||||||
|
) { concreteWebDomain ->
|
||||||
|
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
||||||
|
val assistStructure = AutofillHelper
|
||||||
|
.retrieveAutofillComponent(intent)
|
||||||
|
?.assistStructure
|
||||||
|
val newAutofillComponent = if (assistStructure != null) {
|
||||||
|
AutofillComponent(
|
||||||
|
assistStructure,
|
||||||
|
compatInlineSuggestionsRequest
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
searchInfo.webDomain = concreteWebDomain
|
||||||
|
launchSelection(database, newAutofillComponent, searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove bundle
|
||||||
|
intent.removeExtra(KEY_SELECTION_BUNDLE)
|
||||||
|
}
|
||||||
|
SpecialMode.REGISTRATION -> {
|
||||||
|
// To register info
|
||||||
|
val registerInfo = intent.getParcelableExtraCompat<RegisterInfo>(KEY_REGISTER_INFO)
|
||||||
|
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
||||||
|
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||||
|
searchInfo.webDomain = concreteWebDomain
|
||||||
|
launchRegistration(database, searchInfo, registerInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Not an autofill call
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
private fun launchSelection(database: ContextualDatabase?,
|
||||||
val assistStructure = AutofillHelper.retrieveAssistStructure(intent)
|
autofillComponent: AutofillComponent?,
|
||||||
|
searchInfo: SearchInfo) {
|
||||||
if (assistStructure == null) {
|
if (autofillComponent == null) {
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(Activity.RESULT_CANCELED)
|
||||||
finish()
|
finish()
|
||||||
} else if (!KeeAutofillService.searchAllowedFor(searchInfo.applicationId,
|
} else if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
|
||||||
PreferencesUtil.applicationIdBlocklist(this))
|
PreferencesUtil.applicationIdBlocklist(this))
|
||||||
|| !KeeAutofillService.searchAllowedFor(searchInfo.webDomain,
|
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
|
||||||
PreferencesUtil.webDomainBlocklist(this))) {
|
PreferencesUtil.webDomainBlocklist(this))) {
|
||||||
// If item not allowed, show a toast
|
showBlockRestartMessage()
|
||||||
Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show()
|
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(Activity.RESULT_CANCELED)
|
||||||
finish()
|
finish()
|
||||||
} else {
|
} else {
|
||||||
// If database is open
|
// If database is open
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
Database.getInstance(),
|
database,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
{ items ->
|
{ openedDatabase, items ->
|
||||||
// Items found
|
// Items found
|
||||||
AutofillHelper.buildResponse(this, items)
|
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
|
||||||
finish()
|
finish()
|
||||||
},
|
},
|
||||||
{
|
{ openedDatabase ->
|
||||||
// Show the database UI to select the entry
|
// Show the database UI to select the entry
|
||||||
GroupActivity.launchForAutofillResult(this,
|
GroupActivity.launchForAutofillResult(this,
|
||||||
assistStructure,
|
openedDatabase,
|
||||||
false,
|
mAutofillActivityResultLauncher,
|
||||||
searchInfo)
|
autofillComponent,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// If database not open
|
// If database not open
|
||||||
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
||||||
assistStructure,
|
mAutofillActivityResultLauncher,
|
||||||
|
autofillComponent,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
private fun launchRegistration(database: ContextualDatabase?,
|
||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
searchInfo: SearchInfo,
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
registerInfo: RegisterInfo?) {
|
||||||
|
if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
|
||||||
|
PreferencesUtil.applicationIdBlocklist(this))
|
||||||
|
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
|
||||||
|
PreferencesUtil.webDomainBlocklist(this))) {
|
||||||
|
showBlockRestartMessage()
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
} else {
|
||||||
|
val readOnly = database?.isReadOnly != false
|
||||||
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
|
database,
|
||||||
|
searchInfo,
|
||||||
|
{ openedDatabase, _ ->
|
||||||
|
if (!readOnly) {
|
||||||
|
// Show the database UI to select the entry
|
||||||
|
GroupActivity.launchForRegistration(this,
|
||||||
|
openedDatabase,
|
||||||
|
registerInfo)
|
||||||
|
} else {
|
||||||
|
showReadOnlySaveMessage()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ openedDatabase ->
|
||||||
|
if (!readOnly) {
|
||||||
|
// Show the database UI to select the entry
|
||||||
|
GroupActivity.launchForRegistration(this,
|
||||||
|
openedDatabase,
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
|
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
|
||||||
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
|
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
||||||
|
private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION"
|
||||||
|
|
||||||
fun getAuthIntentSenderForResponse(context: Context,
|
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
|
||||||
searchInfo: SearchInfo? = null): IntentSender {
|
|
||||||
|
fun getPendingIntentForSelection(context: Context,
|
||||||
|
searchInfo: SearchInfo? = null,
|
||||||
|
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent {
|
||||||
return PendingIntent.getActivity(context, 0,
|
return PendingIntent.getActivity(context, 0,
|
||||||
// Doesn't work with Parcelable (don't know why?)
|
// Doesn't work with direct extra Parcelable (don't know why?)
|
||||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
// Wrap into a bundle to bypass the problem
|
||||||
searchInfo?.let {
|
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||||
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
|
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
|
||||||
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
|
putParcelable(KEY_SEARCH_INFO, searchInfo)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
},
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
} else {
|
||||||
|
PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPendingIntentForRegistration(context: Context,
|
||||||
|
registerInfo: RegisterInfo): PendingIntent {
|
||||||
|
return PendingIntent.getActivity(context, 0,
|
||||||
|
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||||
|
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
||||||
|
putExtra(KEY_REGISTER_INFO, registerInfo)
|
||||||
|
},
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
} else {
|
||||||
|
PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,75 +23,109 @@ import android.content.Intent
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.Toast
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||||
|
import androidx.core.graphics.BlendModeCompat
|
||||||
|
import androidx.core.graphics.ColorUtils
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||||
|
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
|
import com.kunzisoft.keepass.adapters.TagsAdapter
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.AttachmentState
|
import com.kunzisoft.keepass.otp.OtpType
|
||||||
import com.kunzisoft.keepass.model.EntryAttachment
|
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
|
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
import com.kunzisoft.keepass.utils.createDocument
|
import com.kunzisoft.keepass.view.WindowInsetPosition
|
||||||
import com.kunzisoft.keepass.utils.onCreateDocumentResult
|
import com.kunzisoft.keepass.view.applyWindowInsets
|
||||||
import com.kunzisoft.keepass.view.EntryContentsView
|
import com.kunzisoft.keepass.view.changeControlColor
|
||||||
import com.kunzisoft.keepass.view.showActionError
|
import com.kunzisoft.keepass.view.changeTitleColor
|
||||||
import java.util.*
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
import kotlin.collections.HashMap
|
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
||||||
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
|
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class EntryActivity : LockingActivity() {
|
class EntryActivity : DatabaseLockActivity() {
|
||||||
|
|
||||||
|
private var footer: ViewGroup? = null
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
||||||
|
private var appBarLayout: AppBarLayout? = null
|
||||||
private var titleIconView: ImageView? = null
|
private var titleIconView: ImageView? = null
|
||||||
private var historyView: View? = null
|
private var historyView: View? = null
|
||||||
private var entryContentsView: EntryContentsView? = null
|
private var tagsListView: RecyclerView? = null
|
||||||
private var entryProgress: ProgressBar? = null
|
private var entryContentTab: TabLayout? = null
|
||||||
|
private var tagsAdapter: TagsAdapter? = null
|
||||||
|
private var entryProgress: LinearProgressIndicator? = null
|
||||||
private var lockView: View? = null
|
private var lockView: View? = null
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
|
private var loadingView: ProgressBar? = null
|
||||||
|
|
||||||
private var mDatabase: Database? = null
|
private val mEntryViewModel: EntryViewModel by viewModels()
|
||||||
|
|
||||||
private var mEntry: Entry? = null
|
private val mEntryActivityEducation = EntryActivityEducation(this)
|
||||||
|
|
||||||
private var mIsHistory: Boolean = false
|
private var mMainEntryId: NodeId<UUID>? = null
|
||||||
private var mEntryLastVersion: Entry? = null
|
private var mHistoryPosition: Int = -1
|
||||||
private var mEntryHistoryPosition: Int = -1
|
private var mEntryIsHistory: Boolean = false
|
||||||
|
private var mEntryLoaded = 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 mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
private var mAttachmentSelected: Attachment? = null
|
||||||
|
|
||||||
private var clipboardHelper: ClipboardHelper? = null
|
private var mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) {
|
||||||
private var firstLaunchOfActivity: Boolean = false
|
// Reload the current id from database
|
||||||
|
mEntryViewModel.loadDatabase(mDatabase)
|
||||||
|
}
|
||||||
|
|
||||||
private var iconColor: Int = 0
|
private var mIcon: IconImage? = null
|
||||||
|
private var mColorSecondary: Int = 0
|
||||||
|
private var mColorSurface: Int = 0
|
||||||
|
private var mColorOnSurface: Int = 0
|
||||||
|
private var mColorBackground: Int = 0
|
||||||
|
private var mBackgroundColor: Int? = null
|
||||||
|
private var mForegroundColor: Int? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -103,51 +137,217 @@ class EntryActivity : LockingActivity() {
|
|||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
mDatabase = Database.getInstance()
|
|
||||||
mReadOnly = mDatabase!!.isReadOnly || mReadOnly
|
|
||||||
|
|
||||||
mShowPassword = !PreferencesUtil.isPasswordMask(this)
|
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
|
||||||
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
|
||||||
iconColor = taIconColor.getColor(0, Color.BLACK)
|
|
||||||
taIconColor.recycle()
|
|
||||||
|
|
||||||
// Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set
|
|
||||||
invalidateOptionsMenu()
|
|
||||||
|
|
||||||
// Get views
|
// Get views
|
||||||
|
footer = findViewById(R.id.activity_entry_footer)
|
||||||
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
||||||
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
||||||
|
appBarLayout = findViewById(R.id.app_bar)
|
||||||
titleIconView = findViewById(R.id.entry_icon)
|
titleIconView = findViewById(R.id.entry_icon)
|
||||||
historyView = findViewById(R.id.history_container)
|
historyView = findViewById(R.id.history_container)
|
||||||
entryContentsView = findViewById(R.id.entry_contents)
|
tagsListView = findViewById(R.id.entry_tags_list_view)
|
||||||
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
entryContentTab = findViewById(R.id.entry_content_tab)
|
||||||
entryProgress = findViewById(R.id.entry_progress)
|
entryProgress = findViewById(R.id.entry_progress)
|
||||||
lockView = findViewById(R.id.lock_button)
|
lockView = findViewById(R.id.lock_button)
|
||||||
|
loadingView = findViewById(R.id.loading)
|
||||||
|
|
||||||
|
// To apply fit window with transparency
|
||||||
|
setTransparentNavigationBar {
|
||||||
|
// To fix margin with API 27
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(collapsingToolbarLayout!!, null)
|
||||||
|
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP)
|
||||||
|
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty title
|
||||||
|
collapsingToolbarLayout?.title = " "
|
||||||
|
toolbar?.title = " "
|
||||||
|
|
||||||
|
// Retrieve the textColor to tint the toolbar
|
||||||
|
val taColorSecondary = theme.obtainStyledAttributes(intArrayOf(R.attr.colorSecondary))
|
||||||
|
val taColorSurface = theme.obtainStyledAttributes(intArrayOf(R.attr.colorSurface))
|
||||||
|
val taColorOnSurface = theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnSurface))
|
||||||
|
val taColorBackground = theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
|
||||||
|
mColorSecondary = taColorSecondary.getColor(0, Color.BLACK)
|
||||||
|
mColorSurface = taColorSurface.getColor(0, Color.BLACK)
|
||||||
|
mColorOnSurface = taColorOnSurface.getColor(0, Color.BLACK)
|
||||||
|
mColorBackground = taColorBackground.getColor(0, Color.BLACK)
|
||||||
|
taColorSecondary.recycle()
|
||||||
|
taColorSurface.recycle()
|
||||||
|
taColorOnSurface.recycle()
|
||||||
|
taColorBackground.recycle()
|
||||||
|
|
||||||
|
// Init Tags adapter
|
||||||
|
tagsAdapter = TagsAdapter(this)
|
||||||
|
tagsListView?.apply {
|
||||||
|
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
adapter = tagsAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init content tab
|
||||||
|
entryContentTab?.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||||
|
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||||
|
mEntryViewModel.selectSection(EntryViewModel.EntrySection.
|
||||||
|
getEntrySectionByPosition(tab?.position ?: 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
||||||
|
|
||||||
|
override fun onTabReselected(tab: TabLayout.Tab?) {}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get Entry from UUID
|
||||||
|
try {
|
||||||
|
intent.getParcelableExtraCompat<NodeId<UUID>>(KEY_ENTRY)?.let { mainEntryId ->
|
||||||
|
intent.removeExtra(KEY_ENTRY)
|
||||||
|
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
|
||||||
|
intent.removeExtra(KEY_ENTRY_HISTORY_POSITION)
|
||||||
|
|
||||||
|
mEntryViewModel.loadEntry(mDatabase, mainEntryId, historyPosition)
|
||||||
|
}
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
Log.e(TAG, "Unable to retrieve the entry key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init SAF manager
|
||||||
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
mExternalFileHelper?.buildCreateDocument { createdFileUri ->
|
||||||
|
mAttachmentSelected?.let { attachment ->
|
||||||
|
if (createdFileUri != null) {
|
||||||
|
mAttachmentFileBinderManager
|
||||||
|
?.startDownloadAttachment(createdFileUri, attachment)
|
||||||
|
}
|
||||||
|
mAttachmentSelected = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Init attachment service binder manager
|
||||||
|
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||||
|
|
||||||
lockView?.setOnClickListener {
|
lockView?.setOnClickListener {
|
||||||
lockAndExit()
|
lockAndExit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init the clipboard helper
|
mEntryViewModel.sectionSelected.observe(this) { entrySection ->
|
||||||
clipboardHelper = ClipboardHelper(this)
|
entryContentTab?.getTabAt(entrySection.position)?.select()
|
||||||
firstLaunchOfActivity = true
|
}
|
||||||
|
|
||||||
// Init attachment service binder manager
|
mEntryViewModel.entryInfoHistory.observe(this) { entryInfoHistory ->
|
||||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
if (entryInfoHistory != null) {
|
||||||
|
this.mMainEntryId = entryInfoHistory.mainEntryId
|
||||||
|
|
||||||
mProgressDialogThread?.onActionFinish = { actionTask, result ->
|
// Manage history position
|
||||||
when (actionTask) {
|
val historyPosition = entryInfoHistory.historyPosition
|
||||||
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
|
this.mHistoryPosition = historyPosition
|
||||||
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
|
val entryIsHistory = historyPosition > -1
|
||||||
// Close the current activity after an history action
|
this.mEntryIsHistory = entryIsHistory
|
||||||
if (result.isSuccess)
|
// Assign history dedicated view
|
||||||
finish()
|
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
|
||||||
|
// TODO History badge
|
||||||
|
/*
|
||||||
|
if (entryIsHistory) {
|
||||||
|
}*/
|
||||||
|
|
||||||
|
val entryInfo = entryInfoHistory.entryInfo
|
||||||
|
// Manage entry copy to start notification if allowed (at the first start)
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
// Manage entry to launch copying notification if allowed
|
||||||
|
ClipboardEntryNotificationService.checkAndLaunchNotification(this, entryInfo)
|
||||||
|
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
||||||
|
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
|
||||||
|
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Assign title icon
|
||||||
|
mIcon = entryInfo.icon
|
||||||
|
// Assign title text
|
||||||
|
val entryTitle =
|
||||||
|
if (entryInfo.title.isNotEmpty()) entryInfo.title else UuidUtil.toHexString(entryInfo.id)
|
||||||
|
collapsingToolbarLayout?.title = entryTitle
|
||||||
|
toolbar?.title = entryTitle
|
||||||
|
// Assign tags
|
||||||
|
val tags = entryInfo.tags
|
||||||
|
tagsListView?.visibility = if (tags.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
tagsAdapter?.setTags(tags)
|
||||||
|
// Assign colors
|
||||||
|
val showEntryColors = PreferencesUtil.showEntryColors(this)
|
||||||
|
mBackgroundColor = if (showEntryColors) entryInfo.backgroundColor else null
|
||||||
|
mForegroundColor = if (showEntryColors) entryInfo.foregroundColor else null
|
||||||
|
|
||||||
|
loadingView?.hideByFading()
|
||||||
|
mEntryLoaded = true
|
||||||
|
} else {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
// Refresh Menu
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement ->
|
||||||
|
if (otpElement == null) {
|
||||||
|
entryProgress?.visibility = View.GONE
|
||||||
|
} else when (otpElement.type) {
|
||||||
|
// Only add token if HOTP
|
||||||
|
OtpType.HOTP -> {
|
||||||
|
entryProgress?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
// Refresh view if TOTP
|
||||||
|
OtpType.TOTP -> {
|
||||||
|
entryProgress?.apply {
|
||||||
|
max = otpElement.period
|
||||||
|
setProgressCompat(otpElement.secondsRemaining, true)
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
coordinatorLayout?.showActionError(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mEntryViewModel.attachmentSelected.observe(this) { attachmentSelected ->
|
||||||
|
mAttachmentSelected = attachmentSelected
|
||||||
|
mExternalFileHelper?.createDocument(attachmentSelected.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryViewModel.historySelected.observe(this) { historySelected ->
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
launch(
|
||||||
|
this,
|
||||||
|
database,
|
||||||
|
historySelected.nodeId,
|
||||||
|
historySelected.historyPosition,
|
||||||
|
mEntryActivityResultLauncher
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finishActivityIfReloadRequested(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun viewToInvalidateTimeout(): View? {
|
||||||
|
return coordinatorLayout
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
|
mEntryViewModel.loadDatabase(database)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseActionFinished(
|
||||||
|
database: ContextualDatabase,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
|
when (actionTask) {
|
||||||
|
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
|
||||||
|
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
|
||||||
|
// Close the current activity after an history action
|
||||||
|
if (result.isSuccess)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
coordinatorLayout?.showActionErrorIfNeeded(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -160,62 +360,19 @@ class EntryActivity : LockingActivity() {
|
|||||||
View.GONE
|
View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Entry from UUID
|
|
||||||
try {
|
|
||||||
val keyEntry: NodeId<UUID>? = intent.getParcelableExtra(KEY_ENTRY)
|
|
||||||
if (keyEntry != null) {
|
|
||||||
mEntry = mDatabase?.getEntryById(keyEntry)
|
|
||||||
mEntryLastVersion = mEntry
|
|
||||||
}
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
Log.e(TAG, "Unable to retrieve the entry key")
|
|
||||||
}
|
|
||||||
|
|
||||||
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, mEntryHistoryPosition)
|
|
||||||
mEntryHistoryPosition = historyPosition
|
|
||||||
if (historyPosition >= 0) {
|
|
||||||
mIsHistory = true
|
|
||||||
mEntry = mEntry?.getHistory()?.get(historyPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mEntry == null) {
|
|
||||||
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show()
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update last access time.
|
|
||||||
mEntry?.touch(modified = false, touchParents = false)
|
|
||||||
|
|
||||||
mEntry?.let { entry ->
|
|
||||||
// Fill data in resume to update from EntryEditActivity
|
|
||||||
fillEntryDataInContentsView(entry)
|
|
||||||
// Refresh Menu
|
|
||||||
invalidateOptionsMenu()
|
|
||||||
|
|
||||||
val entryInfo = entry.getEntryInfo(Database.getInstance())
|
|
||||||
|
|
||||||
// Manage entry copy to start notification if allowed
|
|
||||||
if (firstLaunchOfActivity) {
|
|
||||||
// Manage entry to launch copying notification if allowed
|
|
||||||
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
|
||||||
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
|
||||||
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
|
|
||||||
MagikIME.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mAttachmentFileBinderManager?.apply {
|
mAttachmentFileBinderManager?.apply {
|
||||||
registerProgressTask()
|
registerProgressTask()
|
||||||
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
||||||
override fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment) {
|
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
||||||
entryContentsView?.updateAttachmentDownloadProgress(attachment)
|
mEntryViewModel.onAttachmentAction(entryAttachmentState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
firstLaunchOfActivity = false
|
// Keep the screen on
|
||||||
|
if (PreferencesUtil.isKeepScreenOnEnabled(this)) {
|
||||||
|
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -224,330 +381,143 @@ class EntryActivity : LockingActivity() {
|
|||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fillEntryDataInContentsView(entry: Entry) {
|
private fun applyToolbarColors() {
|
||||||
|
collapsingToolbarLayout?.setBackgroundColor(mBackgroundColor ?: mColorSurface)
|
||||||
val database = Database.getInstance()
|
collapsingToolbarLayout?.contentScrim = ColorDrawable(mBackgroundColor ?: mColorSurface)
|
||||||
database.startManageEntry(entry)
|
val backgroundDarker = if (mBackgroundColor != null) {
|
||||||
// Assign title icon
|
ColorUtils.blendARGB(mBackgroundColor!!, Color.WHITE, 0.1f)
|
||||||
titleIconView?.assignDatabaseIcon(database.drawFactory, entry.icon, iconColor)
|
|
||||||
|
|
||||||
// Assign title text
|
|
||||||
val entryTitle = entry.title
|
|
||||||
collapsingToolbarLayout?.title = entryTitle
|
|
||||||
toolbar?.title = entryTitle
|
|
||||||
|
|
||||||
// Assign basic fields
|
|
||||||
entryContentsView?.assignUserName(entry.username)
|
|
||||||
entryContentsView?.assignUserNameCopyListener(View.OnClickListener {
|
|
||||||
database.startManageEntry(entry)
|
|
||||||
clipboardHelper?.timeoutCopyToClipboard(entry.username,
|
|
||||||
getString(R.string.copy_field,
|
|
||||||
getString(R.string.entry_user_name)))
|
|
||||||
database.stopManageEntry(entry)
|
|
||||||
})
|
|
||||||
|
|
||||||
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
|
||||||
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
|
|
||||||
val allowCopyPasswordAndProtectedFields =
|
|
||||||
PreferencesUtil.allowCopyPasswordAndProtectedFields(this)
|
|
||||||
|
|
||||||
val showWarningClipboardDialogOnClickListener = View.OnClickListener {
|
|
||||||
AlertDialog.Builder(this@EntryActivity)
|
|
||||||
.setMessage(getString(R.string.allow_copy_password_warning) +
|
|
||||||
"\n\n" +
|
|
||||||
getString(R.string.clipboard_warning))
|
|
||||||
.create().apply {
|
|
||||||
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) { dialog, _ ->
|
|
||||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true)
|
|
||||||
dialog.dismiss()
|
|
||||||
fillEntryDataInContentsView(entry)
|
|
||||||
}
|
|
||||||
setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ ->
|
|
||||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, false)
|
|
||||||
dialog.dismiss()
|
|
||||||
fillEntryDataInContentsView(entry)
|
|
||||||
}
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
entryContentsView?.assignPassword(entry.password, allowCopyPasswordAndProtectedFields)
|
|
||||||
if (allowCopyPasswordAndProtectedFields) {
|
|
||||||
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
|
|
||||||
database.startManageEntry(entry)
|
|
||||||
clipboardHelper?.timeoutCopyToClipboard(entry.password,
|
|
||||||
getString(R.string.copy_field,
|
|
||||||
getString(R.string.entry_password)))
|
|
||||||
database.stopManageEntry(entry)
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
// If dialog not already shown
|
mColorBackground
|
||||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
}
|
||||||
entryContentsView?.assignPasswordCopyListener(showWarningClipboardDialogOnClickListener)
|
titleIconView?.background?.colorFilter = BlendModeColorFilterCompat
|
||||||
} else {
|
.createBlendModeColorFilterCompat(backgroundDarker, BlendModeCompat.SRC_IN)
|
||||||
entryContentsView?.assignPasswordCopyListener(null)
|
mIcon?.let { icon ->
|
||||||
|
titleIconView?.let { iconView ->
|
||||||
|
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(
|
||||||
|
iconView,
|
||||||
|
icon,
|
||||||
|
mForegroundColor ?: mColorSecondary
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
toolbar?.changeControlColor(mForegroundColor ?: mColorOnSurface)
|
||||||
//Assign OTP field
|
collapsingToolbarLayout?.changeTitleColor(mForegroundColor ?: mColorOnSurface)
|
||||||
entryContentsView?.assignOtp(entry.getOtpElement(), entryProgress,
|
|
||||||
View.OnClickListener {
|
|
||||||
entry.getOtpElement()?.let { otpElement ->
|
|
||||||
clipboardHelper?.timeoutCopyToClipboard(
|
|
||||||
otpElement.token,
|
|
||||||
getString(R.string.copy_field, getString(R.string.entry_otp))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
entryContentsView?.assignURL(entry.url)
|
|
||||||
entryContentsView?.assignComment(entry.notes)
|
|
||||||
|
|
||||||
// Assign custom fields
|
|
||||||
if (entry.allowCustomFields()) {
|
|
||||||
entryContentsView?.clearExtraFields()
|
|
||||||
|
|
||||||
for (element in entry.customFields.entries) {
|
|
||||||
val label = element.key
|
|
||||||
val value = element.value
|
|
||||||
|
|
||||||
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
|
||||||
if (allowCopyProtectedField) {
|
|
||||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, View.OnClickListener {
|
|
||||||
clipboardHelper?.timeoutCopyToClipboard(
|
|
||||||
value.toString(),
|
|
||||||
getString(R.string.copy_field, label)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// If dialog not already shown
|
|
||||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
|
||||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
|
|
||||||
} else {
|
|
||||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
|
|
||||||
|
|
||||||
// Manage attachments
|
|
||||||
val attachments = entry.getAttachments()
|
|
||||||
val showAttachmentsView = attachments.isNotEmpty()
|
|
||||||
entryContentsView?.showAttachments(showAttachmentsView)
|
|
||||||
if (showAttachmentsView) {
|
|
||||||
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
|
|
||||||
entryContentsView?.assignCreationDate(entry.creationTime)
|
|
||||||
entryContentsView?.assignModificationDate(entry.lastModificationTime)
|
|
||||||
entryContentsView?.assignLastAccessDate(entry.lastAccessTime)
|
|
||||||
entryContentsView?.setExpires(entry.isCurrentlyExpires)
|
|
||||||
if (entry.expires) {
|
|
||||||
entryContentsView?.assignExpiresDate(entry.expiryTime)
|
|
||||||
} else {
|
|
||||||
entryContentsView?.assignExpiresDate(getString(R.string.never))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Manage history
|
|
||||||
historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE
|
|
||||||
if (mIsHistory) {
|
|
||||||
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
|
||||||
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
|
||||||
taColorAccent.recycle()
|
|
||||||
}
|
|
||||||
val entryHistory = entry.getHistory()
|
|
||||||
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
|
|
||||||
entryContentsView?.assignUUID(entry.nodeId.id)
|
|
||||||
|
|
||||||
database.stopManageEntry(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
|
|
||||||
when (requestCode) {
|
|
||||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE ->
|
|
||||||
// Not directly get the entry from intent data but from database
|
|
||||||
mEntry?.let {
|
|
||||||
fillEntryDataInContentsView(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
|
||||||
if (createdFileUri != null) {
|
|
||||||
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
|
|
||||||
mAttachmentFileBinderManager
|
|
||||||
?.startDownloadAttachment(createdFileUri, attachmentToDownload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
|
if (mEntryLoaded) {
|
||||||
|
val inflater = menuInflater
|
||||||
|
|
||||||
val inflater = menuInflater
|
inflater.inflate(R.menu.entry, menu)
|
||||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
inflater.inflate(R.menu.database, menu)
|
||||||
inflater.inflate(R.menu.entry, menu)
|
|
||||||
inflater.inflate(R.menu.database, menu)
|
|
||||||
if (mIsHistory && !mReadOnly) {
|
|
||||||
inflater.inflate(R.menu.entry_history, menu)
|
|
||||||
}
|
|
||||||
if (mIsHistory || mReadOnly) {
|
|
||||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
|
||||||
menu.findItem(R.id.menu_edit)?.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
val togglePassword = menu.findItem(R.id.menu_toggle_pass)
|
if (mEntryIsHistory && !mDatabaseReadOnly) {
|
||||||
entryContentsView?.let {
|
inflater.inflate(R.menu.entry_history, menu)
|
||||||
if (it.isPasswordPresent || it.atLeastOneFieldProtectedPresent()) {
|
}
|
||||||
changeShowPasswordIcon(togglePassword)
|
|
||||||
} else {
|
// Show education views
|
||||||
togglePassword?.isVisible = false
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
performedNextEducation(menu)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val gotoUrl = menu.findItem(R.id.menu_goto_url)
|
|
||||||
gotoUrl?.apply {
|
|
||||||
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
|
|
||||||
// so mEntry may not be set
|
|
||||||
if (mEntry == null) {
|
|
||||||
isVisible = false
|
|
||||||
} else {
|
|
||||||
if (mEntry?.url?.isEmpty() != false) {
|
|
||||||
// disable button if url is not available
|
|
||||||
isVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show education views
|
|
||||||
Handler().post { performedNextEducation(EntryActivityEducation(this), menu) }
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||||
menu: Menu) {
|
if (mEntryIsHistory || mDatabaseReadOnly) {
|
||||||
val entryCopyEducationPerformed = entryContentsView?.isUserNamePresent == true
|
menu?.findItem(R.id.menu_save_database)?.isVisible = false
|
||||||
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||||
findViewById(R.id.entry_user_name_action_image),
|
menu?.findItem(R.id.menu_edit)?.isVisible = false
|
||||||
{
|
}
|
||||||
clipboardHelper?.timeoutCopyToClipboard(mEntry!!.username,
|
if (!mMergeDataAllowed) {
|
||||||
getString(R.string.copy_field,
|
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||||
getString(R.string.entry_user_name)))
|
}
|
||||||
},
|
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||||
{
|
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||||
performedNextEducation(entryActivityEducation, menu)
|
menu?.findItem(R.id.menu_reload_database)?.isVisible = false
|
||||||
})
|
}
|
||||||
|
applyToolbarColors()
|
||||||
|
return super.onPrepareOptionsMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performedNextEducation(menu: Menu) {
|
||||||
|
val entryFragment = supportFragmentManager.findFragmentByTag(ENTRY_FRAGMENT_TAG)
|
||||||
|
as? EntryFragment?
|
||||||
|
val entryFieldCopyView: View? = entryFragment?.firstEntryFieldCopyView()
|
||||||
|
val entryCopyEducationPerformed = entryFieldCopyView != null
|
||||||
|
&& mEntryActivityEducation.checkAndPerformedEntryCopyEducation(
|
||||||
|
entryFieldCopyView,
|
||||||
|
{
|
||||||
|
entryFragment.launchEntryCopyEducationAction()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
performedNextEducation(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 && mEntryActivityEducation.checkAndPerformedEntryEditEducation(
|
||||||
toolbar!!.findViewById(R.id.menu_edit),
|
menuEditView,
|
||||||
{
|
{
|
||||||
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(entryActivityEducation, menu)
|
performedNextEducation(menu)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.menu_contribute -> {
|
|
||||||
MenuUtil.onContributionItemSelected(this)
|
|
||||||
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 {
|
mDatabase?.let { database ->
|
||||||
EntryEditActivity.launch(this@EntryActivity, it)
|
mMainEntryId?.let { entryId ->
|
||||||
|
EntryEditActivity.launchToUpdate(
|
||||||
|
this,
|
||||||
|
database,
|
||||||
|
entryId,
|
||||||
|
mEntryActivityResultLauncher
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_goto_url -> {
|
|
||||||
var url: String = mEntry?.url ?: ""
|
|
||||||
|
|
||||||
// Default http:// if no protocol specified
|
|
||||||
if (!url.contains("://")) {
|
|
||||||
url = "http://$url"
|
|
||||||
}
|
|
||||||
|
|
||||||
UriUtil.gotoUrl(this, url)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
R.id.menu_restore_entry_history -> {
|
R.id.menu_restore_entry_history -> {
|
||||||
mEntryLastVersion?.let { mainEntry ->
|
mMainEntryId?.let { mainEntryId ->
|
||||||
mProgressDialogThread?.startDatabaseRestoreEntryHistory(
|
restoreEntryHistory(
|
||||||
mainEntry,
|
mainEntryId,
|
||||||
mEntryHistoryPosition,
|
mHistoryPosition)
|
||||||
!mReadOnly && mAutoSaveEnable)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
R.id.menu_delete_entry_history -> {
|
R.id.menu_delete_entry_history -> {
|
||||||
mEntryLastVersion?.let { mainEntry ->
|
mMainEntryId?.let { mainEntryId ->
|
||||||
mProgressDialogThread?.startDatabaseDeleteEntryHistory(
|
deleteEntryHistory(
|
||||||
mainEntry,
|
mainEntryId,
|
||||||
mEntryHistoryPosition,
|
mHistoryPosition)
|
||||||
!mReadOnly && mAutoSaveEnable)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
R.id.menu_save_database -> {
|
R.id.menu_save_database -> {
|
||||||
mProgressDialogThread?.startDatabaseSave(!mReadOnly)
|
saveDatabase()
|
||||||
|
}
|
||||||
|
R.id.menu_merge_database -> {
|
||||||
|
mergeDatabase()
|
||||||
|
}
|
||||||
|
R.id.menu_reload_database -> {
|
||||||
|
reloadDatabase()
|
||||||
}
|
}
|
||||||
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
|
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
// Transit data in previous Activity after an update
|
// Transit data in previous Activity after an update
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry)
|
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
|
||||||
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, this)
|
setResult(Activity.RESULT_OK, this)
|
||||||
}
|
}
|
||||||
super.finish()
|
super.finish()
|
||||||
}
|
}
|
||||||
@@ -558,14 +528,39 @@ class EntryActivity : LockingActivity() {
|
|||||||
const val KEY_ENTRY = "KEY_ENTRY"
|
const val KEY_ENTRY = "KEY_ENTRY"
|
||||||
const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION"
|
const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION"
|
||||||
|
|
||||||
fun launch(activity: Activity, entry: Entry, readOnly: Boolean, historyPosition: Int? = null) {
|
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
|
||||||
val intent = Intent(activity, EntryActivity::class.java)
|
/**
|
||||||
intent.putExtra(KEY_ENTRY, entry.nodeId)
|
* Open standard Entry activity
|
||||||
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
*/
|
||||||
if (historyPosition != null)
|
fun launch(activity: Activity,
|
||||||
|
database: ContextualDatabase,
|
||||||
|
entryId: NodeId<UUID>,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
|
if (database.loaded) {
|
||||||
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
|
val intent = Intent(activity, EntryActivity::class.java)
|
||||||
|
intent.putExtra(KEY_ENTRY, entryId)
|
||||||
|
activityResultLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open history Entry activity
|
||||||
|
*/
|
||||||
|
fun launch(activity: Activity,
|
||||||
|
database: ContextualDatabase,
|
||||||
|
entryId: NodeId<UUID>,
|
||||||
|
historyPosition: Int,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
|
if (database.loaded) {
|
||||||
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
|
val intent = Intent(activity, EntryActivity::class.java)
|
||||||
|
intent.putExtra(KEY_ENTRY, entryId)
|
||||||
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
||||||
activity.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
activityResultLauncher.launch(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,112 +19,208 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.app.Activity
|
import android.content.Context
|
||||||
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 androidx.appcompat.app.AppCompatActivity
|
import android.widget.Toast
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||||
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.otp.OtpEntryFields
|
||||||
|
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
import com.kunzisoft.keepass.utils.WebDomain
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity to search or select entry in database,
|
* Activity to search or select entry in database,
|
||||||
* Commonly used with Magikeyboard
|
* Commonly used with Magikeyboard
|
||||||
*/
|
*/
|
||||||
class EntrySelectionLauncherActivity : AppCompatActivity() {
|
class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun applyCustomStyle(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
var sharedWebDomain: String? = null
|
override fun finishActivityIfReloadRequested(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
when (intent?.action) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
Intent.ACTION_SEND -> {
|
super.onDatabaseRetrieved(database)
|
||||||
if ("text/plain" == intent.type) {
|
|
||||||
// Retrieve web domain
|
val keySelectionBundle = intent.getBundleExtra(KEY_SELECTION_BUNDLE)
|
||||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
if (keySelectionBundle != null) {
|
||||||
sharedWebDomain = Uri.parse(it).host
|
// To manage package name
|
||||||
|
var searchInfo = SearchInfo()
|
||||||
|
keySelectionBundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { mSearchInfo ->
|
||||||
|
searchInfo = mSearchInfo
|
||||||
|
}
|
||||||
|
launch(database, searchInfo)
|
||||||
|
} else {
|
||||||
|
// To manage share
|
||||||
|
var sharedWebDomain: String? = null
|
||||||
|
var otpString: String? = null
|
||||||
|
|
||||||
|
when (intent?.action) {
|
||||||
|
Intent.ACTION_SEND -> {
|
||||||
|
if ("text/plain" == intent.type) {
|
||||||
|
// Retrieve web domain or OTP
|
||||||
|
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
|
||||||
|
if (OtpEntryFields.isOTPUri(extra))
|
||||||
|
otpString = extra
|
||||||
|
else
|
||||||
|
sharedWebDomain = Uri.parse(extra).host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launchSelection(database, sharedWebDomain, otpString)
|
||||||
|
}
|
||||||
|
Intent.ACTION_VIEW -> {
|
||||||
|
// Retrieve OTP
|
||||||
|
intent.dataString?.let { extra ->
|
||||||
|
if (OtpEntryFields.isOTPUri(extra))
|
||||||
|
otpString = extra
|
||||||
|
}
|
||||||
|
launchSelection(database, sharedWebDomain, otpString)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
if (database != null) {
|
||||||
|
GroupActivity.launch(this, database)
|
||||||
|
} else {
|
||||||
|
FileDatabaseSelectActivity.launch(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {}
|
|
||||||
}
|
}
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchSelection(database: ContextualDatabase?,
|
||||||
|
sharedWebDomain: String?,
|
||||||
|
otpString: String?) {
|
||||||
|
// Build domain search param
|
||||||
|
val searchInfo = SearchInfo().apply {
|
||||||
|
this.webDomain = sharedWebDomain
|
||||||
|
this.otpString = otpString
|
||||||
|
}
|
||||||
|
|
||||||
|
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||||
|
searchInfo.webDomain = concreteWebDomain
|
||||||
|
launch(database, searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launch(database: ContextualDatabase?,
|
||||||
|
searchInfo: SearchInfo) {
|
||||||
|
|
||||||
// Setting to integrate Magikeyboard
|
// Setting to integrate Magikeyboard
|
||||||
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
|
val searchShareForMagikeyboard = isKeyboardActivatedInSettings()
|
||||||
|
|
||||||
// Build search param
|
|
||||||
val searchInfo = SearchInfo().apply {
|
|
||||||
webDomain = sharedWebDomain
|
|
||||||
}
|
|
||||||
|
|
||||||
// If database is open
|
// If database is open
|
||||||
|
val readOnly = database?.isReadOnly != false
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
Database.getInstance(),
|
database,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
{ items ->
|
{ openedDatabase, items ->
|
||||||
// Items found
|
// Items found
|
||||||
if (searchShareForMagikeyboard) {
|
if (searchInfo.otpString != null) {
|
||||||
if (items.size == 1) {
|
if (!readOnly) {
|
||||||
// Automatically populate keyboard
|
GroupActivity.launchForSaveResult(
|
||||||
val entryPopulate = items[0]
|
this,
|
||||||
populateKeyboardAndMoveAppToBackground(this,
|
openedDatabase,
|
||||||
entryPopulate,
|
searchInfo,
|
||||||
intent)
|
false)
|
||||||
} else {
|
} else {
|
||||||
// Select the one we want
|
Toast.makeText(applicationContext,
|
||||||
GroupActivity.launchForEntrySelectionResult(this,
|
R.string.autofill_read_only_save,
|
||||||
true,
|
Toast.LENGTH_LONG)
|
||||||
searchInfo)
|
.show()
|
||||||
}
|
}
|
||||||
|
} else if (searchShareForMagikeyboard) {
|
||||||
|
MagikeyboardService.performSelection(
|
||||||
|
items,
|
||||||
|
{ entryInfo ->
|
||||||
|
// Automatically populate keyboard
|
||||||
|
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||||
|
this,
|
||||||
|
entryInfo
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ autoSearch ->
|
||||||
|
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||||
|
openedDatabase,
|
||||||
|
searchInfo,
|
||||||
|
autoSearch)
|
||||||
|
}
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
GroupActivity.launch(this,
|
GroupActivity.launchForSearchResult(this,
|
||||||
true,
|
openedDatabase,
|
||||||
searchInfo)
|
searchInfo,
|
||||||
|
true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{ openedDatabase ->
|
||||||
// Show the database UI to select the entry
|
// Show the database UI to select the entry
|
||||||
if (searchShareForMagikeyboard) {
|
if (searchInfo.otpString != null) {
|
||||||
GroupActivity.launchForEntrySelectionResult(this,
|
if (!readOnly) {
|
||||||
false,
|
GroupActivity.launchForSaveResult(this,
|
||||||
searchInfo)
|
openedDatabase,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(applicationContext,
|
||||||
|
R.string.autofill_read_only_save,
|
||||||
|
Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
} else if (searchShareForMagikeyboard) {
|
||||||
|
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||||
|
openedDatabase,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
} else {
|
} else {
|
||||||
GroupActivity.launch(this,
|
GroupActivity.launchForSearchResult(this,
|
||||||
false,
|
openedDatabase,
|
||||||
searchInfo)
|
searchInfo,
|
||||||
|
false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// If database not open
|
// If database not open
|
||||||
if (searchShareForMagikeyboard) {
|
if (searchInfo.otpString != null) {
|
||||||
FileDatabaseSelectActivity.launchForEntrySelectionResult(this,
|
FileDatabaseSelectActivity.launchForSaveResult(this,
|
||||||
|
searchInfo)
|
||||||
|
} else if (searchShareForMagikeyboard) {
|
||||||
|
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
} else {
|
} else {
|
||||||
FileDatabaseSelectActivity.launch(this,
|
FileDatabaseSelectActivity.launchForSearchResult(this,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
finish()
|
companion object {
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
|
||||||
|
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
||||||
|
|
||||||
|
fun launch(context: Context,
|
||||||
|
searchInfo: SearchInfo? = null) {
|
||||||
|
val intent = Intent(context, EntrySelectionLauncherActivity::class.java).apply {
|
||||||
|
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
|
||||||
|
putParcelable(KEY_SEARCH_INFO, searchInfo)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// New task needed because don't launch from an Activity context
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||||
|
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
|
||||||
entry: EntryInfo,
|
|
||||||
intent: Intent,
|
|
||||||
toast: Boolean = true) {
|
|
||||||
// Populate Magikeyboard with entry
|
|
||||||
MagikIME.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
|
||||||
// Consume the selection mode
|
|
||||||
EntrySelectionHelper.removeEntrySelectionModeFromIntent(intent)
|
|
||||||
activity.moveTaskToBack(true)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -19,56 +19,78 @@
|
|||||||
*/
|
*/
|
||||||
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.content.Context
|
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.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import com.google.android.material.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.AssignMasterKeyDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||||
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
|
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.services.DatabaseTaskNotificationService
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.*
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.utils.DexUtil
|
||||||
|
import com.kunzisoft.keepass.utils.MagikeyboardUtil
|
||||||
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
import com.kunzisoft.keepass.utils.parseUri
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
|
import com.kunzisoft.keepass.utils.allowCreateDocumentByStorageAccessFramework
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
import kotlinx.android.synthetic.main.activity_file_selection.*
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
class FileDatabaseSelectActivity : SpecialModeActivity(),
|
class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||||
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
SetMainCredentialDialogFragment.AssignMainCredentialDialogListener {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||||
private var fileManagerExplanationButton: View? = null
|
private var specialTitle: View? = null
|
||||||
private var databaseButtonsContainerView: View? = null
|
|
||||||
private var createDatabaseButtonView: View? = null
|
private var createDatabaseButtonView: View? = null
|
||||||
private var openDatabaseButtonView: View? = null
|
private var openDatabaseButtonView: View? = null
|
||||||
|
|
||||||
|
private val databaseFilesViewModel: DatabaseFilesViewModel by viewModels()
|
||||||
|
|
||||||
|
private val mFileDatabaseSelectActivityEducation = FileDatabaseSelectActivityEducation(this)
|
||||||
|
|
||||||
// Adapter to manage database history list
|
// Adapter to manage database history list
|
||||||
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
|
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
|
||||||
|
|
||||||
@@ -76,13 +98,22 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
|
|
||||||
private var mDatabaseFileUri: Uri? = null
|
private var mDatabaseFileUri: Uri? = null
|
||||||
|
|
||||||
private var mOpenFileHelper: OpenFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var mProgressDialogThread: ProgressDialogThread? = null
|
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||||
|
AutofillHelper.buildActivityResultLauncher(this)
|
||||||
|
else null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Enabling/disabling MagikeyboardService is normally done by DexModeReceiver, but this
|
||||||
|
// additional check will allow the keyboard to be reenabled more easily if the app crashes
|
||||||
|
// or is force quit within DeX mode and then the user leaves DeX mode. Without this, the
|
||||||
|
// user would need to enter and exit DeX mode once to reenable the service.
|
||||||
|
MagikeyboardUtil.setEnabled(this, !DexUtil.isDexMode(resources.configuration))
|
||||||
|
|
||||||
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
|
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
|
||||||
|
|
||||||
setContentView(R.layout.activity_file_selection)
|
setContentView(R.layout.activity_file_selection)
|
||||||
@@ -92,26 +123,33 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
toolbar.title = ""
|
toolbar.title = ""
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
|
|
||||||
fileManagerExplanationButton = findViewById(R.id.file_manager_explanation_button)
|
// Special title
|
||||||
fileManagerExplanationButton?.setOnClickListener {
|
specialTitle = findViewById(R.id.file_selection_title_part_3)
|
||||||
UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
|
|
||||||
}
|
|
||||||
|
|
||||||
databaseButtonsContainerView = findViewById(R.id.database_buttons_container)
|
|
||||||
|
|
||||||
// Create database button
|
// Create database button
|
||||||
createDatabaseButtonView = findViewById(R.id.create_database_button)
|
createDatabaseButtonView = findViewById(R.id.create_database_button)
|
||||||
createDatabaseButtonView?.setOnClickListener { createNewFile() }
|
createDatabaseButtonView?.setOnClickListener { createNewFile() }
|
||||||
|
|
||||||
// Open database button
|
// Open database button
|
||||||
mOpenFileHelper = OpenFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
openDatabaseButtonView?.apply {
|
uri?.let {
|
||||||
mOpenFileHelper?.openFileOnClickViewListener?.let {
|
launchPasswordActivityWithPath(uri)
|
||||||
setOnClickListener(it)
|
|
||||||
setOnLongClickListener(it)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
|
||||||
|
mDatabaseFileUri = databaseFileCreatedUri
|
||||||
|
if (mDatabaseFileUri != null) {
|
||||||
|
SetMainCredentialDialogFragment.getInstance(true)
|
||||||
|
.show(supportFragmentManager, "passwordDialog")
|
||||||
|
} else {
|
||||||
|
val error = getString(R.string.error_create_database)
|
||||||
|
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
Log.e(TAG, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
openDatabaseButtonView = findViewById(R.id.open_database_button)
|
||||||
|
openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
|
|
||||||
// History list
|
// History list
|
||||||
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
||||||
@@ -120,26 +158,25 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
(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,
|
||||||
|
fileDatabaseHistoryEntityToOpen.hardwareKey
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
||||||
// Remove from app database
|
databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete)
|
||||||
mFileDatabaseHistoryAction?.deleteFileDatabaseHistory(fileDatabaseHistoryToDelete) { fileHistoryDeleted ->
|
|
||||||
// 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
|
||||||
|
|
||||||
@@ -149,7 +186,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
|
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
|
||||||
val databasePath = PreferencesUtil.getDefaultDatabasePath(this)
|
val databasePath = PreferencesUtil.getDefaultDatabasePath(this)
|
||||||
|
|
||||||
UriUtil.parse(databasePath)?.let { databaseFileUri ->
|
databasePath?.parseUri()?.let { databaseFileUri ->
|
||||||
launchPasswordActivityWithPath(databaseFileUri)
|
launchPasswordActivityWithPath(databaseFileUri)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
Log.i(TAG, "No default database to prepare")
|
Log.i(TAG, "No default database to prepare")
|
||||||
@@ -159,108 +196,134 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
// Retrieve the database URI provided by file manager after an orientation change
|
// Retrieve the database URI provided by file manager after an orientation change
|
||||||
if (savedInstanceState != null
|
if (savedInstanceState != null
|
||||||
&& savedInstanceState.containsKey(EXTRA_DATABASE_URI)) {
|
&& savedInstanceState.containsKey(EXTRA_DATABASE_URI)) {
|
||||||
mDatabaseFileUri = savedInstanceState.getParcelable(EXTRA_DATABASE_URI)
|
mDatabaseFileUri = savedInstanceState.getParcelableCompat(EXTRA_DATABASE_URI)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach the dialog thread to this activity
|
// Observe list of databases
|
||||||
mProgressDialogThread = ProgressDialogThread(this).apply {
|
databaseFilesViewModel.databaseFilesLoaded.observe(this) { databaseFiles ->
|
||||||
onActionFinish = { actionTask, _ ->
|
try {
|
||||||
when (actionTask) {
|
when (databaseFiles.databaseFileAction) {
|
||||||
ACTION_DATABASE_CREATE_TASK -> {
|
DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
|
||||||
GroupActivity.launch(this@FileDatabaseSelectActivity)
|
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
|
||||||
|
}
|
||||||
|
DatabaseFilesViewModel.DatabaseFileAction.ADD -> {
|
||||||
|
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
|
||||||
|
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
|
||||||
|
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
|
||||||
|
mAdapterDatabaseHistory?.updateDatabaseFileHistory(databaseFileToUpdate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DatabaseFilesViewModel.DatabaseFileAction.DELETE -> {
|
||||||
|
databaseFiles.databaseFileToActivate?.let { databaseFileToDelete ->
|
||||||
|
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileToDelete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
databaseFilesViewModel.consumeAction()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to observe database action", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observe default database
|
||||||
|
databaseFilesViewModel.defaultDatabase.observe(this) {
|
||||||
|
// Retrieve settings for default database
|
||||||
|
mAdapterDatabaseHistory?.setDefaultDatabase(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
if (database != null) {
|
||||||
|
launchGroupActivityIfLoaded(database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseActionFinished(
|
||||||
|
database: ContextualDatabase,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
|
|
||||||
|
if (result.isSuccess) {
|
||||||
|
// Update list
|
||||||
|
when (actionTask) {
|
||||||
|
ACTION_DATABASE_CREATE_TASK,
|
||||||
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
|
result.data?.getParcelableCompat<Uri>(DATABASE_URI_KEY)?.let { databaseUri ->
|
||||||
|
val mainCredential =
|
||||||
|
result.data?.getParcelableCompat(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY)
|
||||||
|
?: MainCredential()
|
||||||
|
databaseFilesViewModel.addDatabaseFile(
|
||||||
|
databaseUri,
|
||||||
|
mainCredential.keyFileUri,
|
||||||
|
mainCredential.hardwareKey
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Launch activity
|
||||||
|
when (actionTask) {
|
||||||
|
ACTION_DATABASE_CREATE_TASK -> {
|
||||||
|
GroupActivity.launch(
|
||||||
|
this@FileDatabaseSelectActivity,
|
||||||
|
database,
|
||||||
|
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
|
launchGroupActivityIfLoaded(database)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
coordinatorLayout.showActionErrorIfNeeded(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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) +
|
mExternalFileHelper?.createDocument(
|
||||||
getString(R.string.database_file_extension_default), "application/x-keepass")
|
getString(R.string.database_file_name_default) +
|
||||||
|
getString(R.string.database_file_extension_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fileNoFoundAction(e: FileNotFoundException) {
|
private fun fileNoFoundAction(e: FileNotFoundException) {
|
||||||
val error = getString(R.string.file_not_found_content)
|
val error = getString(R.string.file_not_found_content)
|
||||||
coordinatorLayout?.let {
|
|
||||||
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
|
|
||||||
}
|
|
||||||
Log.e(TAG, error, e)
|
Log.e(TAG, error, e)
|
||||||
|
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
|
||||||
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
|
MainCredentialActivity.launch(this,
|
||||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
databaseUri,
|
||||||
{
|
keyFile,
|
||||||
try {
|
hardwareKey,
|
||||||
PasswordActivity.launch(this@FileDatabaseSelectActivity,
|
{ exception ->
|
||||||
databaseUri, keyFile,
|
fileNoFoundAction(exception)
|
||||||
searchInfo)
|
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
fileNoFoundAction(e)
|
|
||||||
}
|
|
||||||
// Remove the search info from intent
|
|
||||||
if (searchInfo != null) {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{ onCancelSpecialMode() },
|
||||||
try {
|
{ onLaunchActivitySpecialMode() },
|
||||||
PasswordActivity.launchForKeyboardResult(this@FileDatabaseSelectActivity,
|
mAutofillActivityResultLauncher)
|
||||||
databaseUri, keyFile,
|
|
||||||
searchInfo)
|
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
fileNoFoundAction(e)
|
|
||||||
}
|
|
||||||
finish()
|
|
||||||
},
|
|
||||||
{ assistStructure ->
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
try {
|
|
||||||
PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
|
||||||
databaseUri, keyFile,
|
|
||||||
assistStructure,
|
|
||||||
searchInfo)
|
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
fileNoFoundAction(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchGroupActivity(readOnly: Boolean) {
|
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
||||||
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
|
if (database.loaded) {
|
||||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
GroupActivity.launch(this,
|
||||||
{
|
database,
|
||||||
GroupActivity.launch(this@FileDatabaseSelectActivity,
|
{ onValidateSpecialMode() },
|
||||||
false,
|
{ onCancelSpecialMode() },
|
||||||
searchInfo,
|
{ onLaunchActivitySpecialMode() },
|
||||||
readOnly)
|
mAutofillActivityResultLauncher)
|
||||||
},
|
}
|
||||||
{
|
|
||||||
GroupActivity.launchForEntrySelectionResult(this@FileDatabaseSelectActivity,
|
|
||||||
false,
|
|
||||||
searchInfo,
|
|
||||||
readOnly)
|
|
||||||
// Do not keep history
|
|
||||||
finish()
|
|
||||||
},
|
|
||||||
{ assistStructure ->
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
GroupActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
|
||||||
assistStructure,
|
|
||||||
false,
|
|
||||||
searchInfo,
|
|
||||||
readOnly)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
||||||
launchPasswordActivity(databaseUri, null)
|
launchPasswordActivity(databaseUri, null, null)
|
||||||
// Delete flickering for kitkat <=
|
// Delete flickering for kitkat <=
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
||||||
overridePendingTransition(0, 0)
|
overridePendingTransition(0, 0)
|
||||||
@@ -269,59 +332,38 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
|
// Define special title
|
||||||
|
specialTitle?.isVisible = this.isContributingUser()
|
||||||
|
|
||||||
// Show open and create button or special mode
|
// Show open and create button or special mode
|
||||||
if (mSelectionMode) {
|
when (mSpecialMode) {
|
||||||
// Disable buttons if in selection mode or request for autofill
|
SpecialMode.DEFAULT -> {
|
||||||
databaseButtonsContainerView?.visibility = View.GONE
|
if (packageManager.allowCreateDocumentByStorageAccessFramework()) {
|
||||||
} else {
|
// There is an activity which can handle this intent.
|
||||||
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
createDatabaseButtonView?.visibility = View.VISIBLE
|
||||||
// There is an activity which can handle this intent.
|
} else{
|
||||||
createDatabaseButtonView?.visibility = View.VISIBLE
|
// No Activity found that can handle this intent.
|
||||||
} else{
|
createDatabaseButtonView?.visibility = View.GONE
|
||||||
// No Activity found that can handle this intent.
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Disable create button if in selection mode or request for autofill
|
||||||
createDatabaseButtonView?.visibility = View.GONE
|
createDatabaseButtonView?.visibility = View.GONE
|
||||||
}
|
}
|
||||||
databaseButtonsContainerView?.visibility = View.VISIBLE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val database = Database.getInstance()
|
mDatabase?.let { database ->
|
||||||
if (database.loaded) {
|
launchGroupActivityIfLoaded(database)
|
||||||
launchGroupActivity(database.isReadOnly)
|
}
|
||||||
|
|
||||||
|
// Show recent files if allowed
|
||||||
|
if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
|
||||||
|
databaseFilesViewModel.loadListOfDatabases()
|
||||||
} else {
|
} else {
|
||||||
// Construct adapter with listeners
|
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
||||||
if (PreferencesUtil.showRecentFiles(this)) {
|
|
||||||
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
|
|
||||||
databaseFileHistoryList?.let { historyList ->
|
|
||||||
val hideBrokenLocations = PreferencesUtil.hideBrokenLocations(this@FileDatabaseSelectActivity)
|
|
||||||
mAdapterDatabaseHistory?.addDatabaseFileHistoryList(
|
|
||||||
// Show only uri accessible
|
|
||||||
historyList.filter {
|
|
||||||
if (hideBrokenLocations) {
|
|
||||||
FileDatabaseInfo(this@FileDatabaseSelectActivity,
|
|
||||||
it.databaseUri).exists
|
|
||||||
} else
|
|
||||||
true
|
|
||||||
})
|
|
||||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
|
||||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register progress task
|
|
||||||
mProgressDialogThread?.registerProgressTask()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
// Unregister progress task
|
|
||||||
mProgressDialogThread?.unregisterProgressTask()
|
|
||||||
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
// only to keep the current activity
|
// only to keep the current activity
|
||||||
@@ -330,108 +372,71 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAssignKeyDialogPositiveClick(
|
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
|
||||||
masterPasswordChecked: Boolean, masterPassword: String?,
|
|
||||||
keyFileChecked: Boolean, keyFile: Uri?) {
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mDatabaseFileUri?.let { databaseUri ->
|
mDatabaseFileUri?.let { databaseUri ->
|
||||||
|
|
||||||
// Create the new database
|
// Create the new database
|
||||||
mProgressDialogThread?.startDatabaseCreate(
|
createDatabase(databaseUri, mainCredential)
|
||||||
databaseUri,
|
|
||||||
masterPasswordChecked,
|
|
||||||
masterPassword,
|
|
||||||
keyFileChecked,
|
|
||||||
keyFile
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val error = getString(R.string.error_create_database_file)
|
val error = getString(R.string.error_create_database_file)
|
||||||
Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show()
|
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
Log.e(TAG, error, e)
|
Log.e(TAG, error, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAssignKeyDialogNegativeClick(
|
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
|
||||||
masterPasswordChecked: Boolean, masterPassword: String?,
|
|
||||||
keyFileChecked: Boolean, keyFile: Uri?) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
|
||||||
) { uri ->
|
|
||||||
if (uri != null) {
|
|
||||||
launchPasswordActivityWithPath(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the created URI from the file manager
|
|
||||||
onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
|
|
||||||
mDatabaseFileUri = databaseFileCreatedUri
|
|
||||||
if (mDatabaseFileUri != null) {
|
|
||||||
AssignMasterKeyDialogFragment.getInstance(true)
|
|
||||||
.show(supportFragmentManager, "passwordDialog")
|
|
||||||
} else {
|
|
||||||
val error = getString(R.string.error_create_database)
|
|
||||||
coordinatorLayout?.let {
|
|
||||||
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
|
|
||||||
}
|
|
||||||
Log.e(TAG, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
|
|
||||||
if (!mSelectionMode) {
|
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||||
MenuUtil.defaultMenuInflater(menuInflater, menu)
|
MenuUtil.defaultMenuInflater(this, menuInflater, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
Handler().post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
performedNextEducation()
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
|
private fun performedNextEducation() {
|
||||||
// If no recent files
|
// If no recent files
|
||||||
val createDatabaseEducationPerformed = createDatabaseButtonView != null && createDatabaseButtonView!!.visibility == View.VISIBLE
|
val createDatabaseEducationPerformed =
|
||||||
&& mAdapterDatabaseHistory != null
|
createDatabaseButtonView != null
|
||||||
&& mAdapterDatabaseHistory!!.itemCount > 0
|
&& createDatabaseButtonView!!.visibility == View.VISIBLE
|
||||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
&& mFileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
||||||
createDatabaseButtonView!!,
|
createDatabaseButtonView!!,
|
||||||
{
|
{
|
||||||
createNewFile()
|
createNewFile()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// But if the user cancel, it can also select a database
|
// But if the user cancel, it can also select a database
|
||||||
performedNextEducation(fileDatabaseSelectActivityEducation)
|
performedNextEducation()
|
||||||
})
|
})
|
||||||
if (!createDatabaseEducationPerformed) {
|
if (!createDatabaseEducationPerformed) {
|
||||||
// selectDatabaseEducationPerformed
|
// selectDatabaseEducationPerformed
|
||||||
openDatabaseButtonView != null
|
openDatabaseButtonView != null
|
||||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
&& mFileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
||||||
openDatabaseButtonView!!,
|
openDatabaseButtonView!!,
|
||||||
{tapTargetView ->
|
{ tapTargetView ->
|
||||||
tapTargetView?.let {
|
tapTargetView?.let {
|
||||||
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it)
|
mExternalFileHelper?.openDocument()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{}
|
{
|
||||||
)
|
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> this.openUrl(R.string.file_manager_explanation_url)
|
||||||
|
}
|
||||||
|
MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -442,17 +447,38 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Launch only to standard search, else pass by PasswordActivity
|
* Standard Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun launch(context: Context,
|
fun launch(context: Context) {
|
||||||
searchInfo: SearchInfo? = null) {
|
context.startActivity(Intent(context, FileDatabaseSelectActivity::class.java))
|
||||||
val intent = Intent(context, FileDatabaseSelectActivity::class.java)
|
}
|
||||||
searchInfo?.let {
|
|
||||||
intent.putExtra(KEY_SEARCH_INFO, it)
|
/*
|
||||||
}
|
* -------------------------
|
||||||
context.startActivity(intent)
|
* Search Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun launchForSearchResult(context: Context,
|
||||||
|
searchInfo: SearchInfo) {
|
||||||
|
EntrySelectionHelper.startActivityForSearchModeResult(context,
|
||||||
|
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Save Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun launchForSaveResult(context: Context,
|
||||||
|
searchInfo: SearchInfo) {
|
||||||
|
EntrySelectionHelper.startActivityForSaveModeResult(context,
|
||||||
|
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||||
|
searchInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -461,9 +487,9 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun launchForEntrySelectionResult(activity: Activity,
|
fun launchForKeyboardSelectionResult(activity: Activity,
|
||||||
searchInfo: SearchInfo? = null) {
|
searchInfo: SearchInfo? = null) {
|
||||||
EntrySelectionHelper.startActivityForEntrySelectionResult(activity,
|
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(activity,
|
||||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
@@ -475,13 +501,27 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
fun launchForAutofillResult(activity: Activity,
|
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||||
assistStructure: AssistStructure,
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo? = null) {
|
searchInfo: SearchInfo? = null) {
|
||||||
AutofillHelper.startActivityForAutofillResult(activity,
|
AutofillHelper.startActivityForAutofillResult(activity,
|
||||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||||
assistStructure,
|
activityResultLauncher,
|
||||||
|
autofillComponent,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Registration Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
fun launchForRegistration(context: Context,
|
||||||
|
registerInfo: RegisterInfo? = null) {
|
||||||
|
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
|
||||||
|
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||||
|
registerInfo)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,364 @@
|
|||||||
|
/*
|
||||||
|
* 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 android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.fragment.app.commit
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.IconEditDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
|
import com.kunzisoft.keepass.view.asError
|
||||||
|
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
||||||
|
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
|
||||||
|
class IconPickerActivity : DatabaseLockActivity() {
|
||||||
|
|
||||||
|
private lateinit var toolbar: Toolbar
|
||||||
|
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||||
|
private lateinit var uploadButton: View
|
||||||
|
private var lockView: View? = null
|
||||||
|
|
||||||
|
private var mIconImage: IconImage = IconImage()
|
||||||
|
|
||||||
|
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
|
private val iconPickerViewModel: IconPickerViewModel by viewModels()
|
||||||
|
private var mCustomIconsSelectionMode = false
|
||||||
|
private var mIconsSelected: List<IconImageCustom> = ArrayList()
|
||||||
|
|
||||||
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_icon_picker)
|
||||||
|
|
||||||
|
toolbar = findViewById(R.id.toolbar)
|
||||||
|
toolbar.title = " "
|
||||||
|
setSupportActionBar(toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
updateIconsSelectedViews()
|
||||||
|
|
||||||
|
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
|
||||||
|
|
||||||
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
|
addCustomIcon(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadButton = findViewById(R.id.icon_picker_upload)
|
||||||
|
|
||||||
|
lockView = findViewById(R.id.lock_button)
|
||||||
|
lockView?.setOnClickListener {
|
||||||
|
lockAndExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
intent?.getParcelableExtraCompat<IconImage>(EXTRA_ICON)?.let {
|
||||||
|
mIconImage = it
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
supportFragmentManager.commit {
|
||||||
|
setReorderingAllowed(true)
|
||||||
|
add(R.id.icon_picker_fragment, IconPickerFragment.getInstance(
|
||||||
|
// Default selection tab
|
||||||
|
if (mIconImage.custom.isUnknown)
|
||||||
|
IconPickerFragment.IconTab.STANDARD
|
||||||
|
else
|
||||||
|
IconPickerFragment.IconTab.CUSTOM
|
||||||
|
), ICON_PICKER_FRAGMENT_TAG)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mIconImage = savedInstanceState.getParcelableCompat(EXTRA_ICON) ?: mIconImage
|
||||||
|
}
|
||||||
|
|
||||||
|
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
|
||||||
|
mIconImage.standard = iconStandard
|
||||||
|
// Remove the custom icon if a standard one is selected
|
||||||
|
mIconImage.custom = IconImageCustom()
|
||||||
|
setResult()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
iconPickerViewModel.customIconPicked.observe(this) { iconCustom ->
|
||||||
|
// Keep the standard icon if a custom one is selected
|
||||||
|
mIconImage.custom = iconCustom
|
||||||
|
setResult()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
iconPickerViewModel.customIconsSelected.observe(this) { iconsSelected ->
|
||||||
|
mIconsSelected = iconsSelected
|
||||||
|
updateIconsSelectedViews()
|
||||||
|
}
|
||||||
|
iconPickerViewModel.customIconAdded.observe(this) { iconCustomAdded ->
|
||||||
|
if (iconCustomAdded.error && !iconCustomAdded.errorConsumed) {
|
||||||
|
Snackbar.make(coordinatorLayout, iconCustomAdded.errorStringId, Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
iconCustomAdded.errorConsumed = true
|
||||||
|
}
|
||||||
|
uploadButton.isEnabled = true
|
||||||
|
}
|
||||||
|
iconPickerViewModel.customIconRemoved.observe(this) { iconCustomRemoved ->
|
||||||
|
if (iconCustomRemoved.error && !iconCustomRemoved.errorConsumed) {
|
||||||
|
Snackbar.make(coordinatorLayout, iconCustomRemoved.errorStringId, Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
iconCustomRemoved.errorConsumed = true
|
||||||
|
}
|
||||||
|
uploadButton.isEnabled = true
|
||||||
|
}
|
||||||
|
iconPickerViewModel.customIconUpdated.observe(this) { iconCustomUpdated ->
|
||||||
|
if (iconCustomUpdated.error && !iconCustomUpdated.errorConsumed) {
|
||||||
|
Snackbar.make(coordinatorLayout, iconCustomUpdated.errorStringId, Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
iconCustomUpdated.errorConsumed = true
|
||||||
|
}
|
||||||
|
iconCustomUpdated.iconCustom?.let {
|
||||||
|
mDatabase?.updateCustomIcon(it)
|
||||||
|
}
|
||||||
|
iconPickerViewModel.deselectAllCustomIcons()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun viewToInvalidateTimeout(): View? {
|
||||||
|
return findViewById<ViewGroup>(R.id.icon_picker_container)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finishActivityIfReloadRequested(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
|
if (database?.allowCustomIcons == true) {
|
||||||
|
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
|
} else {
|
||||||
|
uploadButton.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateIconsSelectedViews() {
|
||||||
|
if (mIconsSelected.isEmpty()) {
|
||||||
|
mCustomIconsSelectionMode = false
|
||||||
|
toolbar.title = " "
|
||||||
|
} else {
|
||||||
|
mCustomIconsSelectionMode = true
|
||||||
|
toolbar.title = mIconsSelected.size.toString()
|
||||||
|
}
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
|
||||||
|
outState.putParcelable(EXTRA_ICON, mIconImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// Show the lock button
|
||||||
|
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Padding if lock button visible
|
||||||
|
toolbar.updateLockPaddingLeft()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
|
super.onCreateOptionsMenu(menu)
|
||||||
|
menuInflater.inflate(R.menu.icon, menu)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||||
|
menu?.findItem(R.id.menu_edit)?.apply {
|
||||||
|
isEnabled = mIconsSelected.size == 1
|
||||||
|
isVisible = isEnabled
|
||||||
|
}
|
||||||
|
menu?.findItem(R.id.menu_delete)?.apply {
|
||||||
|
isEnabled = mCustomIconsSelectionMode
|
||||||
|
isVisible = isEnabled
|
||||||
|
}
|
||||||
|
return super.onPrepareOptionsMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> {
|
||||||
|
if (mCustomIconsSelectionMode) {
|
||||||
|
iconPickerViewModel.deselectAllCustomIcons()
|
||||||
|
} else {
|
||||||
|
onDatabaseBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
R.id.menu_edit -> {
|
||||||
|
updateCustomIcon(mIconsSelected[0])
|
||||||
|
}
|
||||||
|
R.id.menu_delete -> {
|
||||||
|
mIconsSelected.forEach { iconToRemove ->
|
||||||
|
removeCustomIcon(iconToRemove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
R.id.menu_external_icon -> {
|
||||||
|
this.openUrl(R.string.external_icon_url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addCustomIcon(iconToUploadUri: Uri?) {
|
||||||
|
uploadButton.isEnabled = false
|
||||||
|
mainScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
// on Progress with thread
|
||||||
|
val asyncResult: Deferred<IconPickerViewModel.IconCustomState?> = async {
|
||||||
|
val iconCustomState = IconPickerViewModel.IconCustomState(null, true, R.string.error_upload_file)
|
||||||
|
iconToUploadUri?.getDocumentFile(this@IconPickerActivity)?.also { documentFile ->
|
||||||
|
if (documentFile.length() > MAX_ICON_SIZE) {
|
||||||
|
iconCustomState.errorStringId = R.string.error_file_to_big
|
||||||
|
} else {
|
||||||
|
mDatabase?.buildNewCustomIcon { customIcon, binary ->
|
||||||
|
if (customIcon != null) {
|
||||||
|
iconCustomState.iconCustom = customIcon
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
BinaryDatabaseManager.resizeBitmapAndStoreDataInBinaryFile(
|
||||||
|
contentResolver,
|
||||||
|
database,
|
||||||
|
iconToUploadUri,
|
||||||
|
binary)
|
||||||
|
when {
|
||||||
|
binary == null -> {
|
||||||
|
}
|
||||||
|
binary.getSize() <= 0 -> {
|
||||||
|
}
|
||||||
|
database.isCustomIconBinaryDuplicate(binary) -> {
|
||||||
|
iconCustomState.errorStringId = R.string.error_duplicate_file
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
iconCustomState.error = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (iconCustomState.error) {
|
||||||
|
mDatabase?.removeCustomIcon(customIcon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iconCustomState
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
asyncResult.await()?.let { customIcon ->
|
||||||
|
iconPickerViewModel.addCustomIcon(customIcon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateCustomIcon(iconImageCustom: IconImageCustom) {
|
||||||
|
IconEditDialogFragment.update(iconImageCustom)
|
||||||
|
.show(supportFragmentManager, IconEditDialogFragment.TAG_UPDATE_ICON)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeCustomIcon(iconImageCustom: IconImageCustom) {
|
||||||
|
uploadButton.isEnabled = false
|
||||||
|
iconPickerViewModel.deselectAllCustomIcons()
|
||||||
|
mDatabase?.removeCustomIcon(iconImageCustom)
|
||||||
|
iconPickerViewModel.removeCustomIcon(
|
||||||
|
IconPickerViewModel.IconCustomState(iconImageCustom, false, R.string.error_remove_file)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setResult() {
|
||||||
|
setResult(Activity.RESULT_OK, Intent().apply {
|
||||||
|
putExtra(EXTRA_ICON, mIconImage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseBackPressed() {
|
||||||
|
setResult()
|
||||||
|
super.onDatabaseBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val ICON_PICKER_FRAGMENT_TAG = "ICON_PICKER_FRAGMENT_TAG"
|
||||||
|
private const val EXTRA_ICON = "EXTRA_ICON"
|
||||||
|
private const val MAX_ICON_SIZE = 5242880
|
||||||
|
|
||||||
|
fun registerIconSelectionForResult(context: FragmentActivity,
|
||||||
|
listener: (icon: IconImage) -> Unit): ActivityResultLauncher<Intent> {
|
||||||
|
return context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
listener.invoke(result.data?.getParcelableExtraCompat(EXTRA_ICON) ?: IconImage())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launch(context: FragmentActivity,
|
||||||
|
previousIcon: IconImage?,
|
||||||
|
resultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
|
// Create an instance to return the picker icon
|
||||||
|
resultLauncher.launch(
|
||||||
|
Intent(context, IconPickerActivity::class.java).apply {
|
||||||
|
if (previousIcon != null)
|
||||||
|
putExtra(EXTRA_ICON, previousIcon)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 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.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.format.Formatter
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import com.igreenwood.loupe.Loupe
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
class ImageViewerActivity : DatabaseLockActivity() {
|
||||||
|
|
||||||
|
private var imageContainerView: ViewGroup? = null
|
||||||
|
private lateinit var imageView: ImageView
|
||||||
|
private lateinit var progressView: View
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_image_viewer)
|
||||||
|
|
||||||
|
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||||
|
setSupportActionBar(toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
toolbar.setOnTouchListener { _, _ ->
|
||||||
|
resetAppTimeout()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
imageContainerView = findViewById(R.id.image_viewer_container)
|
||||||
|
imageView = findViewById(R.id.image_viewer_image)
|
||||||
|
progressView = findViewById(R.id.image_viewer_progress)
|
||||||
|
|
||||||
|
Loupe.create(imageView, imageContainerView!!) {
|
||||||
|
onViewTouchedListener = View.OnTouchListener { _, _ ->
|
||||||
|
// to reset timeout when Loupe image view touched
|
||||||
|
resetAppTimeout()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
onViewTranslateListener = object : Loupe.OnViewTranslateListener {
|
||||||
|
|
||||||
|
override fun onStart(view: ImageView) {
|
||||||
|
// called when the view starts moving
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewTranslate(view: ImageView, amount: Float) {
|
||||||
|
// called whenever the view position changed
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRestore(view: ImageView) {
|
||||||
|
// called when the view drag gesture ended
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDismiss(view: ImageView) {
|
||||||
|
// called when the view drag gesture ended
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun viewToInvalidateTimeout(): View? {
|
||||||
|
// Null to manually manage events
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finishActivityIfReloadRequested(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
|
try {
|
||||||
|
progressView.visibility = View.VISIBLE
|
||||||
|
intent.getParcelableExtraCompat<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
|
||||||
|
|
||||||
|
supportActionBar?.title = attachment.name
|
||||||
|
|
||||||
|
val size = attachment.binaryData.getSize()
|
||||||
|
supportActionBar?.subtitle = Formatter.formatFileSize(this, size)
|
||||||
|
|
||||||
|
// Approximately, to not OOM and allow a zoom
|
||||||
|
val mImagePreviewMaxWidth = max(
|
||||||
|
resources.displayMetrics.widthPixels * 2,
|
||||||
|
resources.displayMetrics.heightPixels * 2
|
||||||
|
)
|
||||||
|
|
||||||
|
database?.let { database ->
|
||||||
|
BinaryDatabaseManager.loadBitmap(
|
||||||
|
database,
|
||||||
|
attachment.binaryData,
|
||||||
|
mImagePreviewMaxWidth
|
||||||
|
) { bitmapLoaded ->
|
||||||
|
if (bitmapLoaded == null) {
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
progressView.visibility = View.GONE
|
||||||
|
imageView.setImageBitmap(bitmapLoaded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: finish()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to view the binary", e)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> finish()
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val TAG = ImageViewerActivity::class.simpleName
|
||||||
|
|
||||||
|
private const val IMAGE_ATTACHMENT_TAG = "IMAGE_ATTACHMENT_TAG"
|
||||||
|
|
||||||
|
fun getInstance(context: Context, imageAttachment: Attachment) {
|
||||||
|
context.startActivity(Intent(context, ImageViewerActivity::class.java).apply {
|
||||||
|
putExtra(IMAGE_ATTACHMENT_TAG, imageAttachment)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.fragment.app.commit
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.fragments.KeyGeneratorFragment
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
||||||
|
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
||||||
|
|
||||||
|
class KeyGeneratorActivity : DatabaseLockActivity() {
|
||||||
|
|
||||||
|
private lateinit var toolbar: Toolbar
|
||||||
|
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||||
|
private lateinit var validationButton: View
|
||||||
|
private var lockView: View? = null
|
||||||
|
|
||||||
|
private val keyGeneratorViewModel: KeyGeneratorViewModel by viewModels()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_key_generator)
|
||||||
|
|
||||||
|
toolbar = findViewById(R.id.toolbar)
|
||||||
|
toolbar.title = " "
|
||||||
|
setSupportActionBar(toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
|
coordinatorLayout = findViewById(R.id.key_generator_coordinator)
|
||||||
|
|
||||||
|
lockView = findViewById(R.id.lock_button)
|
||||||
|
lockView?.setOnClickListener {
|
||||||
|
lockAndExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
validationButton = findViewById(R.id.key_generator_validation)
|
||||||
|
validationButton.setOnClickListener {
|
||||||
|
keyGeneratorViewModel.validateKeyGenerated()
|
||||||
|
}
|
||||||
|
|
||||||
|
supportFragmentManager.commit {
|
||||||
|
replace(R.id.key_generator_fragment, KeyGeneratorFragment.getInstance(
|
||||||
|
// Default selection tab
|
||||||
|
KeyGeneratorFragment.KeyGeneratorTab.PASSWORD
|
||||||
|
), KEY_GENERATED_FRAGMENT_TAG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyGeneratorViewModel.keyGenerated.observe(this) { keyGenerated ->
|
||||||
|
setResult(Activity.RESULT_OK, Intent().apply {
|
||||||
|
putExtra(KEY_GENERATED, keyGenerated)
|
||||||
|
})
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun viewToInvalidateTimeout(): View? {
|
||||||
|
return findViewById<ViewGroup>(R.id.key_generator_container)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// Show the lock button
|
||||||
|
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Padding if lock button visible
|
||||||
|
toolbar.updateLockPaddingLeft()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
|
super.onCreateOptionsMenu(menu)
|
||||||
|
menuInflater.inflate(R.menu.key_generator, menu)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> {
|
||||||
|
onDatabaseBackPressed()
|
||||||
|
}
|
||||||
|
R.id.menu_generate -> {
|
||||||
|
keyGeneratorViewModel.requireKeyGeneration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseBackPressed() {
|
||||||
|
setResult(Activity.RESULT_CANCELED, Intent())
|
||||||
|
super.onDatabaseBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY_GENERATED = "KEY_GENERATED"
|
||||||
|
private const val KEY_GENERATED_FRAGMENT_TAG = "KEY_GENERATED_FRAGMENT_TAG"
|
||||||
|
|
||||||
|
fun registerForGeneratedKeyResult(activity: FragmentActivity,
|
||||||
|
keyGeneratedListener: (String?) -> Unit): ActivityResultLauncher<Intent> {
|
||||||
|
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
keyGeneratedListener.invoke(
|
||||||
|
result.data?.getStringExtra(KEY_GENERATED)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
keyGeneratedListener.invoke(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launch(context: FragmentActivity,
|
||||||
|
resultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
|
// Create an instance to return the picker icon
|
||||||
|
resultLauncher.launch(
|
||||||
|
Intent(context, KeyGeneratorActivity::class.java)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,473 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.activities
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuInflater
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.appcompat.view.ActionMode
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.adapters.NodeAdapter
|
|
||||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener {
|
|
||||||
|
|
||||||
private var nodeClickListener: NodeClickListener? = null
|
|
||||||
private var onScrollListener: OnScrollListener? = null
|
|
||||||
|
|
||||||
private var mNodesRecyclerView: RecyclerView? = null
|
|
||||||
var mainGroup: Group? = null
|
|
||||||
private set
|
|
||||||
private var mAdapter: NodeAdapter? = null
|
|
||||||
|
|
||||||
var nodeActionSelectionMode = false
|
|
||||||
private set
|
|
||||||
var nodeActionPasteMode: PasteMode = PasteMode.UNDEFINED
|
|
||||||
private set
|
|
||||||
private val listActionNodes = LinkedList<Node>()
|
|
||||||
private val listPasteNodes = LinkedList<Node>()
|
|
||||||
|
|
||||||
private var notFoundView: View? = null
|
|
||||||
private var isASearchResult: Boolean = false
|
|
||||||
|
|
||||||
|
|
||||||
private var readOnly: Boolean = false
|
|
||||||
get() {
|
|
||||||
return field || selectionMode
|
|
||||||
}
|
|
||||||
private var selectionMode: Boolean = false
|
|
||||||
|
|
||||||
val isEmpty: Boolean
|
|
||||||
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
try {
|
|
||||||
nodeClickListener = context as NodeClickListener
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
// The activity doesn't implement the interface, throw exception
|
|
||||||
throw ClassCastException(context.toString()
|
|
||||||
+ " must implement " + NodeAdapter.NodeClickCallback::class.java.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
onScrollListener = context as OnScrollListener
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
onScrollListener = null
|
|
||||||
// Context menu can be omit
|
|
||||||
Log.w(TAG, context.toString()
|
|
||||||
+ " must implement " + RecyclerView.OnScrollListener::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
setHasOptionsMenu(true)
|
|
||||||
|
|
||||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, arguments)
|
|
||||||
|
|
||||||
arguments?.let { args ->
|
|
||||||
// Contains all the group in element
|
|
||||||
if (args.containsKey(GROUP_KEY)) {
|
|
||||||
mainGroup = args.getParcelable(GROUP_KEY)
|
|
||||||
}
|
|
||||||
if (args.containsKey(IS_SEARCH)) {
|
|
||||||
isASearchResult = args.getBoolean(IS_SEARCH)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contextThemed?.let { context ->
|
|
||||||
mAdapter = NodeAdapter(context)
|
|
||||||
mAdapter?.apply {
|
|
||||||
setOnNodeClickListener(object : NodeAdapter.NodeClickCallback {
|
|
||||||
override fun onNodeClick(node: Node) {
|
|
||||||
if (nodeActionSelectionMode) {
|
|
||||||
if (listActionNodes.contains(node)) {
|
|
||||||
// Remove selected item if already selected
|
|
||||||
listActionNodes.remove(node)
|
|
||||||
} else {
|
|
||||||
// Add selected item if not already selected
|
|
||||||
listActionNodes.add(node)
|
|
||||||
}
|
|
||||||
nodeClickListener?.onNodeSelected(listActionNodes)
|
|
||||||
setActionNodes(listActionNodes)
|
|
||||||
notifyNodeChanged(node)
|
|
||||||
} else {
|
|
||||||
nodeClickListener?.onNodeClick(node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNodeLongClick(node: Node): Boolean {
|
|
||||||
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
|
|
||||||
// Select the first item after a long click
|
|
||||||
if (!listActionNodes.contains(node))
|
|
||||||
listActionNodes.add(node)
|
|
||||||
|
|
||||||
nodeClickListener?.onNodeSelected(listActionNodes)
|
|
||||||
|
|
||||||
setActionNodes(listActionNodes)
|
|
||||||
notifyNodeChanged(node)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
||||||
super.onCreateView(inflater, container, savedInstanceState)
|
|
||||||
|
|
||||||
// To apply theme
|
|
||||||
val rootView = inflater.cloneInContext(contextThemed)
|
|
||||||
.inflate(R.layout.fragment_list_nodes, container, false)
|
|
||||||
mNodesRecyclerView = rootView.findViewById(R.id.nodes_list)
|
|
||||||
notFoundView = rootView.findViewById(R.id.not_found_container)
|
|
||||||
|
|
||||||
mNodesRecyclerView?.apply {
|
|
||||||
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
|
||||||
layoutManager = LinearLayoutManager(context)
|
|
||||||
adapter = mAdapter
|
|
||||||
}
|
|
||||||
|
|
||||||
onScrollListener?.let { onScrollListener ->
|
|
||||||
mNodesRecyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
|
||||||
onScrollListener.onScrolled(dy)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return rootView
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
activity?.intent?.let {
|
|
||||||
selectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh data
|
|
||||||
rebuildList()
|
|
||||||
|
|
||||||
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
|
|
||||||
// To show the " no search entry found "
|
|
||||||
mNodesRecyclerView?.visibility = View.GONE
|
|
||||||
notFoundView?.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
mNodesRecyclerView?.visibility = View.VISIBLE
|
|
||||||
notFoundView?.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun rebuildList() {
|
|
||||||
// Add elements to the list
|
|
||||||
mainGroup?.let { mainGroup ->
|
|
||||||
mAdapter?.apply {
|
|
||||||
rebuildList(mainGroup)
|
|
||||||
// To visually change the elements
|
|
||||||
if (PreferencesUtil.APPEARANCE_CHANGED) {
|
|
||||||
notifyDataSetChanged()
|
|
||||||
PreferencesUtil.APPEARANCE_CHANGED = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSortSelected(sortNodeEnum: SortNodeEnum,
|
|
||||||
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
|
||||||
// Save setting
|
|
||||||
context?.let {
|
|
||||||
PreferencesUtil.saveNodeSort(it, sortNodeEnum, sortNodeParameters)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tell the adapter to refresh it's list
|
|
||||||
mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters)
|
|
||||||
rebuildList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
|
||||||
inflater.inflate(R.menu.tree, menu)
|
|
||||||
|
|
||||||
super.onCreateOptionsMenu(menu, inflater)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
|
||||||
|
|
||||||
R.id.menu_sort -> {
|
|
||||||
context?.let { context ->
|
|
||||||
val sortDialogFragment: SortDialogFragment =
|
|
||||||
if (Database.getInstance().isRecycleBinEnabled) {
|
|
||||||
SortDialogFragment.getInstance(
|
|
||||||
PreferencesUtil.getListSort(context),
|
|
||||||
PreferencesUtil.getAscendingSort(context),
|
|
||||||
PreferencesUtil.getGroupsBeforeSort(context),
|
|
||||||
PreferencesUtil.getRecycleBinBottomSort(context))
|
|
||||||
} else {
|
|
||||||
SortDialogFragment.getInstance(
|
|
||||||
PreferencesUtil.getListSort(context),
|
|
||||||
PreferencesUtil.getAscendingSort(context),
|
|
||||||
PreferencesUtil.getGroupsBeforeSort(context))
|
|
||||||
}
|
|
||||||
|
|
||||||
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun actionNodesCallback(nodes: List<Node>,
|
|
||||||
menuListener: NodesActionMenuListener?) : ActionMode.Callback {
|
|
||||||
|
|
||||||
return object : ActionMode.Callback {
|
|
||||||
|
|
||||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
|
||||||
nodeActionSelectionMode = false
|
|
||||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
|
||||||
menu?.clear()
|
|
||||||
|
|
||||||
if (nodeActionPasteMode != PasteMode.UNDEFINED) {
|
|
||||||
mode?.menuInflater?.inflate(R.menu.node_paste_menu, menu)
|
|
||||||
} else {
|
|
||||||
nodeActionSelectionMode = true
|
|
||||||
mode?.menuInflater?.inflate(R.menu.node_menu, menu)
|
|
||||||
|
|
||||||
val database = Database.getInstance()
|
|
||||||
|
|
||||||
// Open and Edit for a single item
|
|
||||||
if (nodes.size == 1) {
|
|
||||||
// Edition
|
|
||||||
if (readOnly
|
|
||||||
|| (database.isRecycleBinEnabled && nodes[0] == database.recycleBin)) {
|
|
||||||
menu?.removeItem(R.id.menu_edit)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
menu?.removeItem(R.id.menu_open)
|
|
||||||
menu?.removeItem(R.id.menu_edit)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy and Move (not for groups)
|
|
||||||
if (readOnly
|
|
||||||
|| isASearchResult
|
|
||||||
|| nodes.any { it.type == Type.GROUP }) {
|
|
||||||
// TODO Copy For Group
|
|
||||||
menu?.removeItem(R.id.menu_copy)
|
|
||||||
menu?.removeItem(R.id.menu_move)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deletion
|
|
||||||
if (readOnly
|
|
||||||
|| (database.isRecycleBinEnabled && nodes.any { it == database.recycleBin })) {
|
|
||||||
menu?.removeItem(R.id.menu_delete)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the number of items selected in title
|
|
||||||
mode?.title = nodes.size.toString()
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
|
||||||
if (menuListener == null)
|
|
||||||
return false
|
|
||||||
return when (item?.itemId) {
|
|
||||||
R.id.menu_open -> menuListener.onOpenMenuClick(nodes[0])
|
|
||||||
R.id.menu_edit -> menuListener.onEditMenuClick(nodes[0])
|
|
||||||
R.id.menu_copy -> {
|
|
||||||
nodeActionPasteMode = PasteMode.PASTE_FROM_COPY
|
|
||||||
mAdapter?.unselectActionNodes()
|
|
||||||
val returnValue = menuListener.onCopyMenuClick(nodes)
|
|
||||||
nodeActionSelectionMode = false
|
|
||||||
returnValue
|
|
||||||
}
|
|
||||||
R.id.menu_move -> {
|
|
||||||
nodeActionPasteMode = PasteMode.PASTE_FROM_MOVE
|
|
||||||
mAdapter?.unselectActionNodes()
|
|
||||||
val returnValue = menuListener.onMoveMenuClick(nodes)
|
|
||||||
nodeActionSelectionMode = false
|
|
||||||
returnValue
|
|
||||||
}
|
|
||||||
R.id.menu_delete -> menuListener.onDeleteMenuClick(nodes)
|
|
||||||
R.id.menu_paste -> {
|
|
||||||
val returnValue = menuListener.onPasteMenuClick(nodeActionPasteMode, nodes)
|
|
||||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
|
||||||
nodeActionSelectionMode = false
|
|
||||||
returnValue
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
|
||||||
listActionNodes.clear()
|
|
||||||
listPasteNodes.clear()
|
|
||||||
mAdapter?.unselectActionNodes()
|
|
||||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
|
||||||
nodeActionSelectionMode = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
|
|
||||||
when (requestCode) {
|
|
||||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
|
||||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE
|
|
||||||
|| resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
|
|
||||||
data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { changedNode ->
|
|
||||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
|
|
||||||
addNode(changedNode)
|
|
||||||
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE)
|
|
||||||
mAdapter?.notifyDataSetChanged()
|
|
||||||
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun contains(node: Node): Boolean {
|
|
||||||
return mAdapter?.contains(node) ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addNode(newNode: Node) {
|
|
||||||
mAdapter?.addNode(newNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addNodes(newNodes: List<Node>) {
|
|
||||||
mAdapter?.addNodes(newNodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateNode(oldNode: Node, newNode: Node? = null) {
|
|
||||||
mAdapter?.updateNode(oldNode, newNode ?: oldNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateNodes(oldNodes: List<Node>, newNodes: List<Node>) {
|
|
||||||
mAdapter?.updateNodes(oldNodes, newNodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeNode(pwNode: Node) {
|
|
||||||
mAdapter?.removeNode(pwNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeNodes(nodes: List<Node>) {
|
|
||||||
mAdapter?.removeNodes(nodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeNodeAt(position: Int) {
|
|
||||||
mAdapter?.removeNodeAt(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeNodesAt(positions: IntArray) {
|
|
||||||
mAdapter?.removeNodesAt(positions)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback listener to redefine to do an action when a node is click
|
|
||||||
*/
|
|
||||||
interface NodeClickListener {
|
|
||||||
fun onNodeClick(node: Node)
|
|
||||||
fun onNodeSelected(nodes: List<Node>): Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Menu listener to redefine to do an action in menu
|
|
||||||
*/
|
|
||||||
interface NodesActionMenuListener {
|
|
||||||
fun onOpenMenuClick(node: Node): Boolean
|
|
||||||
fun onEditMenuClick(node: Node): Boolean
|
|
||||||
fun onCopyMenuClick(nodes: List<Node>): Boolean
|
|
||||||
fun onMoveMenuClick(nodes: List<Node>): Boolean
|
|
||||||
fun onDeleteMenuClick(nodes: List<Node>): Boolean
|
|
||||||
fun onPasteMenuClick(pasteMode: PasteMode?, nodes: List<Node>): Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class PasteMode {
|
|
||||||
UNDEFINED, PASTE_FROM_COPY, PASTE_FROM_MOVE
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OnScrollListener {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback method to be invoked when the RecyclerView has been scrolled. This will be
|
|
||||||
* called after the scroll has completed.
|
|
||||||
*
|
|
||||||
* @param dy The amount of vertical scroll.
|
|
||||||
*/
|
|
||||||
fun onScrolled(dy: Int)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private val TAG = ListNodesFragment::class.java.name
|
|
||||||
|
|
||||||
private const val GROUP_KEY = "GROUP_KEY"
|
|
||||||
private const val IS_SEARCH = "IS_SEARCH"
|
|
||||||
|
|
||||||
fun newInstance(group: Group?, readOnly: Boolean, isASearch: Boolean): ListNodesFragment {
|
|
||||||
val bundle = Bundle()
|
|
||||||
if (group != null) {
|
|
||||||
bundle.putParcelable(GROUP_KEY, group)
|
|
||||||
}
|
|
||||||
bundle.putBoolean(IS_SEARCH, isASearch)
|
|
||||||
ReadOnlyHelper.putReadOnlyInBundle(bundle, readOnly)
|
|
||||||
val listNodesFragment = ListNodesFragment()
|
|
||||||
listNodesFragment.arguments = bundle
|
|
||||||
return listNodesFragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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?) {
|
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
|
||||||
Database.getInstance(),
|
|
||||||
null,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
GroupActivity.launchForEntrySelectionResult(this)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Pass extra to get entry
|
|
||||||
FileDatabaseSelectActivity.launchForEntrySelectionResult(this)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
finish()
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,924 @@
|
|||||||
|
/*
|
||||||
|
* 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.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.fragment.app.commit
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
|
||||||
|
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||||
|
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||||
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
|
import com.kunzisoft.keepass.model.*
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
||||||
|
import com.kunzisoft.keepass.settings.AdvancedUnlockSettingsActivity
|
||||||
|
import com.kunzisoft.keepass.settings.AppearanceSettingsActivity
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||||
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil.getUri
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
import com.kunzisoft.keepass.view.MainCredentialView
|
||||||
|
import com.kunzisoft.keepass.view.asError
|
||||||
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
|
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
|
|
||||||
|
class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
||||||
|
|
||||||
|
// Views
|
||||||
|
private var toolbar: Toolbar? = null
|
||||||
|
private var filenameView: TextView? = null
|
||||||
|
private var logotypeButton: View? = null
|
||||||
|
private var advancedUnlockButton: View? = null
|
||||||
|
private var mainCredentialView: MainCredentialView? = null
|
||||||
|
private var confirmButtonView: Button? = null
|
||||||
|
private var infoContainerView: ViewGroup? = null
|
||||||
|
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||||
|
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
|
||||||
|
|
||||||
|
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||||
|
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by viewModels()
|
||||||
|
|
||||||
|
private val mPasswordActivityEducation = PasswordActivityEducation(this)
|
||||||
|
|
||||||
|
private var mDefaultDatabase: Boolean = false
|
||||||
|
private var mDatabaseFileUri: Uri? = null
|
||||||
|
|
||||||
|
private var mRememberKeyFile: Boolean = false
|
||||||
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
|
private var mRememberHardwareKey: Boolean = false
|
||||||
|
|
||||||
|
private var mReadOnly: Boolean = false
|
||||||
|
private var mForceReadOnly: Boolean = false
|
||||||
|
|
||||||
|
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||||
|
AutofillHelper.buildActivityResultLauncher(this)
|
||||||
|
else null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_main_credential)
|
||||||
|
|
||||||
|
toolbar = findViewById(R.id.toolbar)
|
||||||
|
toolbar?.title = getString(R.string.app_name)
|
||||||
|
setSupportActionBar(toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
|
filenameView = findViewById(R.id.filename)
|
||||||
|
logotypeButton = findViewById(R.id.activity_password_logotype)
|
||||||
|
advancedUnlockButton = findViewById(R.id.fragment_advanced_unlock_container_view)
|
||||||
|
mainCredentialView = findViewById(R.id.activity_password_credentials)
|
||||||
|
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
||||||
|
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||||
|
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
|
||||||
|
|
||||||
|
mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) {
|
||||||
|
savedInstanceState.getBoolean(KEY_READ_ONLY)
|
||||||
|
} else {
|
||||||
|
PreferencesUtil.enableReadOnlyDatabase(this)
|
||||||
|
}
|
||||||
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||||
|
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this)
|
||||||
|
|
||||||
|
// Build elements to manage keyfile selection
|
||||||
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
|
if (uri != null) {
|
||||||
|
mainCredentialView?.populateKeyFileView(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
|
||||||
|
mainCredentialView?.onValidateListener = {
|
||||||
|
loadDatabase()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If is a view intent
|
||||||
|
getUriFromIntent(intent)
|
||||||
|
|
||||||
|
// Show appearance
|
||||||
|
logotypeButton?.setOnClickListener {
|
||||||
|
startActivity(Intent(this, AppearanceSettingsActivity::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init Biometric elements
|
||||||
|
advancedUnlockFragment = supportFragmentManager
|
||||||
|
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
|
||||||
|
if (advancedUnlockFragment == null) {
|
||||||
|
advancedUnlockFragment = AdvancedUnlockFragment()
|
||||||
|
supportFragmentManager.commit {
|
||||||
|
replace(R.id.fragment_advanced_unlock_container_view,
|
||||||
|
advancedUnlockFragment!!,
|
||||||
|
UNLOCK_FRAGMENT_TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen password checkbox to init advanced unlock and confirmation button
|
||||||
|
mainCredentialView?.onPasswordChecked =
|
||||||
|
CompoundButton.OnCheckedChangeListener { _, _ ->
|
||||||
|
mAdvancedUnlockViewModel.checkUnlockAvailability()
|
||||||
|
enableConfirmationButton()
|
||||||
|
}
|
||||||
|
mainCredentialView?.onKeyFileChecked =
|
||||||
|
CompoundButton.OnCheckedChangeListener { _, _ ->
|
||||||
|
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
|
||||||
|
enableConfirmationButton()
|
||||||
|
}
|
||||||
|
mainCredentialView?.onHardwareKeyChecked =
|
||||||
|
CompoundButton.OnCheckedChangeListener { _, _ ->
|
||||||
|
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
|
||||||
|
enableConfirmationButton()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observe if default database
|
||||||
|
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
||||||
|
mDefaultDatabase = isDefaultDatabase
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observe database file change
|
||||||
|
mDatabaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
|
||||||
|
|
||||||
|
// Force read only if the file does not exists
|
||||||
|
val databaseFileNotExists = databaseFile?.let {
|
||||||
|
!it.databaseFileExists
|
||||||
|
} ?: true
|
||||||
|
infoContainerView?.visibility = if (databaseFileNotExists) {
|
||||||
|
mReadOnly = true
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
mForceReadOnly = databaseFileNotExists
|
||||||
|
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
|
||||||
|
// Post init uri with KeyFile only if needed
|
||||||
|
val databaseKeyFileUri = mainCredentialView?.getMainCredential()?.keyFileUri
|
||||||
|
val keyFileUri =
|
||||||
|
if (mRememberKeyFile
|
||||||
|
&& (databaseKeyFileUri == null || databaseKeyFileUri.toString().isEmpty())) {
|
||||||
|
databaseFile?.keyFileUri
|
||||||
|
} else {
|
||||||
|
databaseKeyFileUri
|
||||||
|
}
|
||||||
|
|
||||||
|
val databaseHardwareKey = mainCredentialView?.getMainCredential()?.hardwareKey
|
||||||
|
val hardwareKey =
|
||||||
|
if (mRememberHardwareKey
|
||||||
|
&& databaseHardwareKey == null) {
|
||||||
|
databaseFile?.hardwareKey
|
||||||
|
} else {
|
||||||
|
databaseHardwareKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define title
|
||||||
|
filenameView?.text = databaseFile?.databaseAlias ?: ""
|
||||||
|
|
||||||
|
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri, hardwareKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity)
|
||||||
|
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this@MainCredentialActivity)
|
||||||
|
|
||||||
|
// Back to previous keyboard is setting activated
|
||||||
|
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@MainCredentialActivity)) {
|
||||||
|
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow auto open prompt if lock become when UI visible
|
||||||
|
if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true) {
|
||||||
|
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseFileUri?.let { databaseFileUri ->
|
||||||
|
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
launchGroupActivityIfLoaded(database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
if (database != null) {
|
||||||
|
// Trying to load another database
|
||||||
|
if (mDatabaseFileUri != null
|
||||||
|
&& database.fileUri != null
|
||||||
|
&& mDatabaseFileUri != database.fileUri) {
|
||||||
|
Toast.makeText(this,
|
||||||
|
R.string.warning_database_already_opened,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
launchGroupActivityIfLoaded(database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseActionFinished(
|
||||||
|
database: ContextualDatabase,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
|
when (actionTask) {
|
||||||
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
|
// Recheck advanced unlock if error
|
||||||
|
mAdvancedUnlockViewModel.initAdvancedUnlockMode()
|
||||||
|
|
||||||
|
if (result.isSuccess) {
|
||||||
|
launchGroupActivityIfLoaded(database)
|
||||||
|
} else {
|
||||||
|
mainCredentialView?.requestPasswordFocus()
|
||||||
|
// Manage special exceptions
|
||||||
|
when (result.exception) {
|
||||||
|
is DuplicateUuidDatabaseException -> {
|
||||||
|
// Relaunch loading if we need to fix UUID
|
||||||
|
showLoadDatabaseDuplicateUuidMessage {
|
||||||
|
|
||||||
|
var databaseUri: Uri? = null
|
||||||
|
var mainCredential = MainCredential()
|
||||||
|
var readOnly = true
|
||||||
|
var cipherEncryptDatabase: CipherEncryptDatabase? = null
|
||||||
|
|
||||||
|
result.data?.let { resultData ->
|
||||||
|
databaseUri = resultData.getParcelableCompat(DATABASE_URI_KEY)
|
||||||
|
mainCredential =
|
||||||
|
resultData.getParcelableCompat(MAIN_CREDENTIAL_KEY)
|
||||||
|
?: mainCredential
|
||||||
|
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
||||||
|
cipherEncryptDatabase =
|
||||||
|
resultData.getParcelableCompat(CIPHER_DATABASE_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
databaseUri?.let { databaseFileUri ->
|
||||||
|
showProgressDialogAndLoadDatabase(
|
||||||
|
databaseFileUri,
|
||||||
|
mainCredential,
|
||||||
|
readOnly,
|
||||||
|
cipherEncryptDatabase,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is FileNotFoundDatabaseException -> {
|
||||||
|
// Remove this default database inaccessible
|
||||||
|
if (mDefaultDatabase) {
|
||||||
|
mDatabaseFileViewModel.removeDefaultDatabase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
coordinatorLayout.showActionErrorIfNeeded(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUriFromIntent(intent: Intent?) {
|
||||||
|
// If is a view intent
|
||||||
|
val action = intent?.action
|
||||||
|
if (action == VIEW_INTENT) {
|
||||||
|
fillCredentials(
|
||||||
|
intent.data,
|
||||||
|
intent.getUri(KEY_KEYFILE),
|
||||||
|
HardwareKey.getHardwareKeyFromString(intent.getStringExtra(KEY_HARDWARE_KEY))
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
fillCredentials(
|
||||||
|
intent?.getParcelableExtraCompat(KEY_FILENAME),
|
||||||
|
intent?.getParcelableExtraCompat(KEY_KEYFILE),
|
||||||
|
HardwareKey.getHardwareKeyFromString(intent?.getStringExtra(KEY_HARDWARE_KEY))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
intent?.removeExtra(KEY_KEYFILE)
|
||||||
|
intent?.removeExtra(KEY_HARDWARE_KEY)
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
mDatabaseFileUri?.let {
|
||||||
|
mDatabaseFileViewModel.checkIfIsDefaultDatabase(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fillCredentials(databaseUri: Uri?,
|
||||||
|
keyFileUri: Uri?,
|
||||||
|
hardwareKey: HardwareKey?) {
|
||||||
|
mDatabaseFileUri = databaseUri
|
||||||
|
mainCredentialView?.populateKeyFileView(keyFileUri)
|
||||||
|
mainCredentialView?.populateHardwareKeyView(hardwareKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
getUriFromIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
||||||
|
// Check if database really loaded
|
||||||
|
if (database.loaded) {
|
||||||
|
clearCredentialsViews(clearKeyFile = true, clearHardwareKey = true)
|
||||||
|
GroupActivity.launch(this,
|
||||||
|
database,
|
||||||
|
{ onValidateSpecialMode() },
|
||||||
|
{ onCancelSpecialMode() },
|
||||||
|
{ onLaunchActivitySpecialMode() },
|
||||||
|
mAutofillActivityResultLauncher
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun retrieveCredentialForEncryption(): ByteArray {
|
||||||
|
return mainCredentialView?.retrieveCredentialForStorage(credentialStorageListener)
|
||||||
|
?: byteArrayOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun conditionToStoreCredential(): Boolean {
|
||||||
|
return mainCredentialView?.conditionToStoreCredential() == true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) {
|
||||||
|
// Load the database if password is registered with biometric
|
||||||
|
loadDatabase(mDatabaseFileUri,
|
||||||
|
mainCredentialView?.getMainCredential(),
|
||||||
|
cipherEncryptDatabase
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val credentialStorageListener = object: MainCredentialView.CredentialStorageListener {
|
||||||
|
override fun passwordToStore(password: String?): ByteArray? {
|
||||||
|
return password?.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun keyfileToStore(keyfile: Uri?): ByteArray? {
|
||||||
|
// TODO create byte array to store keyfile
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hardwareKeyToStore(): ByteArray? {
|
||||||
|
// TODO create byte array to store hardware key
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
|
||||||
|
// Load the database if password is retrieve from biometric
|
||||||
|
// Retrieve from biometric
|
||||||
|
val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
|
||||||
|
when (cipherDecryptDatabase.credentialStorage) {
|
||||||
|
CredentialStorage.PASSWORD -> {
|
||||||
|
mainCredential.password = String(cipherDecryptDatabase.decryptedValue)
|
||||||
|
}
|
||||||
|
CredentialStorage.KEY_FILE -> {
|
||||||
|
// TODO advanced unlock key file
|
||||||
|
}
|
||||||
|
CredentialStorage.HARDWARE_KEY -> {
|
||||||
|
// TODO advanced unlock hardware key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadDatabase(mDatabaseFileUri,
|
||||||
|
mainCredential,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDatabaseFileLoaded(databaseFileUri: Uri?,
|
||||||
|
keyFileUri: Uri?,
|
||||||
|
hardwareKey: HardwareKey?) {
|
||||||
|
// Define Key File text
|
||||||
|
if (mRememberKeyFile) {
|
||||||
|
mainCredentialView?.populateKeyFileView(keyFileUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define hardware key
|
||||||
|
if (mRememberHardwareKey) {
|
||||||
|
mainCredentialView?.populateHardwareKeyView(hardwareKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define listener for validate button
|
||||||
|
confirmButtonView?.setOnClickListener {
|
||||||
|
mainCredentialView?.validateCredential()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If Activity is launch with a password and want to open directly
|
||||||
|
val intent = intent
|
||||||
|
val password = intent.getStringExtra(KEY_PASSWORD)
|
||||||
|
// Consume the intent extra password
|
||||||
|
intent.removeExtra(KEY_PASSWORD)
|
||||||
|
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
|
||||||
|
if (password != null) {
|
||||||
|
mainCredentialView?.populatePasswordTextView(password)
|
||||||
|
}
|
||||||
|
if (launchImmediately) {
|
||||||
|
loadDatabase()
|
||||||
|
} else {
|
||||||
|
// Init Biometric elements
|
||||||
|
mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
enableConfirmationButton()
|
||||||
|
|
||||||
|
mainCredentialView?.focusPasswordFieldAndOpenKeyboard()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun enableConfirmationButton() {
|
||||||
|
// Enable or not the open button if setting is checked
|
||||||
|
if (!PreferencesUtil.emptyPasswordAllowed(this@MainCredentialActivity)) {
|
||||||
|
confirmButtonView?.isEnabled = mainCredentialView?.isFill() ?: false
|
||||||
|
} else {
|
||||||
|
confirmButtonView?.isEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile,
|
||||||
|
clearHardwareKey: Boolean = !mRememberHardwareKey) {
|
||||||
|
mainCredentialView?.populatePasswordTextView(null)
|
||||||
|
if (clearKeyFile) {
|
||||||
|
mainCredentialView?.populateKeyFileView(null)
|
||||||
|
}
|
||||||
|
if (clearHardwareKey) {
|
||||||
|
mainCredentialView?.populateHardwareKeyView(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
// Reinit locking activity UI variable
|
||||||
|
DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
||||||
|
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
outState.putBoolean(KEY_READ_ONLY, mReadOnly)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadDatabase() {
|
||||||
|
loadDatabase(mDatabaseFileUri,
|
||||||
|
mainCredentialView?.getMainCredential(),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadDatabase(databaseFileUri: Uri?,
|
||||||
|
mainCredential: MainCredential?,
|
||||||
|
cipherEncryptDatabase: CipherEncryptDatabase?) {
|
||||||
|
|
||||||
|
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
|
||||||
|
clearCredentialsViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mReadOnly && (
|
||||||
|
mSpecialMode == SpecialMode.SAVE
|
||||||
|
|| mSpecialMode == SpecialMode.REGISTRATION)
|
||||||
|
) {
|
||||||
|
Log.e(TAG, getString(R.string.autofill_read_only_save))
|
||||||
|
Snackbar.make(coordinatorLayout,
|
||||||
|
R.string.autofill_read_only_save,
|
||||||
|
Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
} else {
|
||||||
|
databaseFileUri?.let { databaseUri ->
|
||||||
|
// Show the progress dialog and load the database
|
||||||
|
showProgressDialogAndLoadDatabase(
|
||||||
|
databaseUri,
|
||||||
|
mainCredential ?: MainCredential(),
|
||||||
|
mReadOnly,
|
||||||
|
cipherEncryptDatabase,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
|
||||||
|
mainCredential: MainCredential,
|
||||||
|
readOnly: Boolean,
|
||||||
|
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||||
|
fixDuplicateUUID: Boolean) {
|
||||||
|
loadDatabase(
|
||||||
|
databaseUri,
|
||||||
|
mainCredential,
|
||||||
|
readOnly,
|
||||||
|
cipherEncryptDatabase,
|
||||||
|
fixDuplicateUUID
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLoadDatabaseDuplicateUuidMessage(loadDatabaseWithFix: (() -> Unit)? = null) {
|
||||||
|
DuplicateUuidDialog().apply {
|
||||||
|
positiveAction = loadDatabaseWithFix
|
||||||
|
}.show(supportFragmentManager, "duplicateUUIDDialog")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
val inflater = menuInflater
|
||||||
|
// Read menu
|
||||||
|
inflater.inflate(R.menu.open_file, menu)
|
||||||
|
if (mForceReadOnly) {
|
||||||
|
menu.removeItem(R.id.menu_open_file_read_mode_key)
|
||||||
|
} else {
|
||||||
|
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||||
|
MenuUtil.defaultMenuInflater(this, inflater, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onCreateOptionsMenu(menu)
|
||||||
|
|
||||||
|
launchEducation(menu)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// To fix multiple view education
|
||||||
|
private var performedEductionInProgress = false
|
||||||
|
private fun launchEducation(menu: Menu) {
|
||||||
|
if (!performedEductionInProgress) {
|
||||||
|
performedEductionInProgress = true
|
||||||
|
// Show education views
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
performedNextEducation(menu)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performedNextEducation(menu: Menu) {
|
||||||
|
val educationToolbar = toolbar
|
||||||
|
val unlockEducationPerformed = educationToolbar != null
|
||||||
|
&& mPasswordActivityEducation.checkAndPerformedUnlockEducation(
|
||||||
|
educationToolbar,
|
||||||
|
{
|
||||||
|
performedNextEducation(menu)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
performedNextEducation(menu)
|
||||||
|
})
|
||||||
|
if (!unlockEducationPerformed) {
|
||||||
|
val readOnlyEducationPerformed =
|
||||||
|
educationToolbar?.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
|
||||||
|
&& mPasswordActivityEducation.checkAndPerformedReadOnlyEducation(
|
||||||
|
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
menu.findItem(R.id.menu_open_file_read_mode_key)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to find read mode menu")
|
||||||
|
}
|
||||||
|
performedNextEducation(menu)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
performedNextEducation(menu)
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||||
|
&& !readOnlyEducationPerformed) {
|
||||||
|
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(this)
|
||||||
|
if ((biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
||||||
|
&& advancedUnlockButton != null) {
|
||||||
|
mPasswordActivityEducation.checkAndPerformedBiometricEducation(
|
||||||
|
advancedUnlockButton!!,
|
||||||
|
{
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
this,
|
||||||
|
AdvancedUnlockSettingsActivity::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ignored: Exception) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun changeOpenFileReadIcon(togglePassword: MenuItem) {
|
||||||
|
if (mReadOnly) {
|
||||||
|
togglePassword.setTitle(R.string.menu_file_selection_read_only)
|
||||||
|
togglePassword.setIcon(R.drawable.ic_read_only_white_24dp)
|
||||||
|
} else {
|
||||||
|
togglePassword.setTitle(R.string.menu_open_file_read_and_write)
|
||||||
|
togglePassword.setIcon(R.drawable.ic_read_write_white_24dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> finish()
|
||||||
|
R.id.menu_open_file_read_mode_key -> {
|
||||||
|
mReadOnly = !mReadOnly
|
||||||
|
changeOpenFileReadIcon(item)
|
||||||
|
}
|
||||||
|
else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val TAG = MainCredentialActivity::class.java.name
|
||||||
|
|
||||||
|
private const val UNLOCK_FRAGMENT_TAG = "UNLOCK_FRAGMENT_TAG"
|
||||||
|
|
||||||
|
private const val KEY_FILENAME = "fileName"
|
||||||
|
private const val KEY_KEYFILE = "keyFile"
|
||||||
|
private const val KEY_HARDWARE_KEY = "hardwareKey"
|
||||||
|
private const val VIEW_INTENT = "android.intent.action.VIEW"
|
||||||
|
|
||||||
|
private const val KEY_READ_ONLY = "KEY_READ_ONLY"
|
||||||
|
private const val KEY_PASSWORD = "password"
|
||||||
|
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||||
|
|
||||||
|
private fun buildAndLaunchIntent(activity: Activity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
hardwareKey: HardwareKey?,
|
||||||
|
intentBuildLauncher: (Intent) -> Unit) {
|
||||||
|
val intent = Intent(activity, MainCredentialActivity::class.java)
|
||||||
|
intent.putExtra(KEY_FILENAME, databaseFile)
|
||||||
|
if (keyFile != null)
|
||||||
|
intent.putExtra(KEY_KEYFILE, keyFile)
|
||||||
|
if (hardwareKey != null)
|
||||||
|
intent.putExtra(KEY_HARDWARE_KEY, hardwareKey.toString())
|
||||||
|
intentBuildLauncher.invoke(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Standard Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launch(activity: Activity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
hardwareKey: HardwareKey?) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
|
activity.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Share Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launchForSearchResult(activity: Activity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
hardwareKey: HardwareKey?,
|
||||||
|
searchInfo: SearchInfo) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
|
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Save Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launchForSaveResult(activity: Activity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
hardwareKey: HardwareKey?,
|
||||||
|
searchInfo: SearchInfo) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
|
EntrySelectionHelper.startActivityForSaveModeResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Keyboard Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launchForKeyboardResult(activity: Activity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
hardwareKey: HardwareKey?,
|
||||||
|
searchInfo: SearchInfo?) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
|
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Autofill Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
hardwareKey: HardwareKey?,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
autofillComponent: AutofillComponent,
|
||||||
|
searchInfo: SearchInfo?) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
|
AutofillHelper.startActivityForAutofillResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
activityResultLauncher,
|
||||||
|
autofillComponent,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Registration Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
fun launchForRegistration(activity: Activity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
hardwareKey: HardwareKey?,
|
||||||
|
registerInfo: RegisterInfo?) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
registerInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Global Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
fun launch(activity: AppCompatActivity,
|
||||||
|
databaseUri: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
hardwareKey: HardwareKey?,
|
||||||
|
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
||||||
|
onCancelSpecialMode: () -> Unit,
|
||||||
|
onLaunchActivitySpecialMode: () -> Unit,
|
||||||
|
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||||
|
{
|
||||||
|
launch(
|
||||||
|
activity,
|
||||||
|
databaseUri,
|
||||||
|
keyFile,
|
||||||
|
hardwareKey
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ searchInfo -> // Search Action
|
||||||
|
launchForSearchResult(
|
||||||
|
activity,
|
||||||
|
databaseUri,
|
||||||
|
keyFile,
|
||||||
|
hardwareKey,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
{ searchInfo -> // Save Action
|
||||||
|
launchForSaveResult(
|
||||||
|
activity,
|
||||||
|
databaseUri,
|
||||||
|
keyFile,
|
||||||
|
hardwareKey,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
{ searchInfo -> // Keyboard Selection Action
|
||||||
|
launchForKeyboardResult(
|
||||||
|
activity,
|
||||||
|
databaseUri,
|
||||||
|
keyFile,
|
||||||
|
hardwareKey,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
{ searchInfo, autofillComponent -> // Autofill Selection Action
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
launchForAutofillResult(
|
||||||
|
activity,
|
||||||
|
databaseUri,
|
||||||
|
keyFile,
|
||||||
|
hardwareKey,
|
||||||
|
autofillActivityResultLauncher,
|
||||||
|
autofillComponent,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
} else {
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ registerInfo -> // Registration Action
|
||||||
|
launchForRegistration(
|
||||||
|
activity,
|
||||||
|
databaseUri,
|
||||||
|
keyFile,
|
||||||
|
hardwareKey,
|
||||||
|
registerInfo
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
fileNoFoundAction(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,886 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.activities
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.assist.AssistStructure
|
|
||||||
import android.app.backup.BackupManager
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.Handler
|
|
||||||
import android.text.Editable
|
|
||||||
import android.text.TextWatcher
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.*
|
|
||||||
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
|
||||||
import android.widget.*
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
|
||||||
import androidx.biometric.BiometricManager
|
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
|
||||||
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
|
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
|
||||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
|
|
||||||
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.MASTER_PASSWORD_KEY
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.utils.FileDatabaseInfo
|
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
|
||||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
|
||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
|
||||||
import com.kunzisoft.keepass.view.asError
|
|
||||||
import kotlinx.android.synthetic.main.activity_password.*
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
|
|
||||||
open class PasswordActivity : SpecialModeActivity() {
|
|
||||||
|
|
||||||
// Views
|
|
||||||
private var toolbar: Toolbar? = null
|
|
||||||
private var filenameView: TextView? = null
|
|
||||||
private var passwordView: EditText? = null
|
|
||||||
private var keyFileSelectionView: KeyFileSelectionView? = null
|
|
||||||
private var confirmButtonView: Button? = null
|
|
||||||
private var checkboxPasswordView: CompoundButton? = null
|
|
||||||
private var checkboxKeyFileView: CompoundButton? = null
|
|
||||||
private var checkboxDefaultDatabaseView: CompoundButton? = null
|
|
||||||
private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null
|
|
||||||
private var infoContainerView: ViewGroup? = null
|
|
||||||
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
|
|
||||||
|
|
||||||
private var mDatabaseFileUri: Uri? = null
|
|
||||||
private var mDatabaseKeyFileUri: Uri? = null
|
|
||||||
|
|
||||||
private var mRememberKeyFile: Boolean = false
|
|
||||||
private var mOpenFileHelper: OpenFileHelper? = null
|
|
||||||
|
|
||||||
private var mPermissionAsked = false
|
|
||||||
private var readOnly: Boolean = false
|
|
||||||
private var mForceReadOnly: Boolean = false
|
|
||||||
set(value) {
|
|
||||||
infoContainerView?.visibility = if (value) {
|
|
||||||
readOnly = true
|
|
||||||
View.VISIBLE
|
|
||||||
} else {
|
|
||||||
View.GONE
|
|
||||||
}
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
private var mProgressDialogThread: ProgressDialogThread? = null
|
|
||||||
|
|
||||||
private var advancedUnlockedManager: AdvancedUnlockedManager? = null
|
|
||||||
private var mAllowAutoOpenBiometricPrompt: Boolean = true
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_password)
|
|
||||||
|
|
||||||
toolbar = findViewById(R.id.toolbar)
|
|
||||||
toolbar?.title = getString(R.string.app_name)
|
|
||||||
setSupportActionBar(toolbar)
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
|
||||||
|
|
||||||
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
|
||||||
filenameView = findViewById(R.id.filename)
|
|
||||||
passwordView = findViewById(R.id.password)
|
|
||||||
keyFileSelectionView = findViewById(R.id.keyfile_selection)
|
|
||||||
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
|
||||||
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
|
||||||
checkboxDefaultDatabaseView = findViewById(R.id.default_database)
|
|
||||||
advancedUnlockInfoView = findViewById(R.id.biometric_info)
|
|
||||||
infoContainerView = findViewById(R.id.activity_password_info_container)
|
|
||||||
|
|
||||||
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
|
||||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
|
||||||
|
|
||||||
mOpenFileHelper = OpenFileHelper(this@PasswordActivity)
|
|
||||||
keyFileSelectionView?.apply {
|
|
||||||
mOpenFileHelper?.openFileOnClickViewListener?.let {
|
|
||||||
setOnClickListener(it)
|
|
||||||
setOnLongClickListener(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
|
||||||
passwordView?.addTextChangedListener(object : TextWatcher {
|
|
||||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
|
||||||
|
|
||||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
|
||||||
|
|
||||||
override fun afterTextChanged(editable: Editable) {
|
|
||||||
if (editable.toString().isNotEmpty() && checkboxPasswordView?.isChecked != true)
|
|
||||||
checkboxPasswordView?.isChecked = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ ->
|
|
||||||
enableOrNotTheConfirmationButton()
|
|
||||||
}
|
|
||||||
|
|
||||||
// If is a view intent
|
|
||||||
getUriFromIntent(intent)
|
|
||||||
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
|
|
||||||
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 {
|
|
||||||
onActionFinish = { actionTask, result ->
|
|
||||||
when (actionTask) {
|
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
|
||||||
// Recheck biometric if error
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
if (PreferencesUtil.isBiometricUnlockEnable(this@PasswordActivity)) {
|
|
||||||
// Stay with the same mode and init it
|
|
||||||
advancedUnlockedManager?.initBiometricMode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.isSuccess) {
|
|
||||||
mDatabaseKeyFileUri = null
|
|
||||||
clearCredentialsViews(true)
|
|
||||||
launchGroupActivity()
|
|
||||||
} else {
|
|
||||||
var resultError = ""
|
|
||||||
val resultException = result.exception
|
|
||||||
val resultMessage = result.message
|
|
||||||
|
|
||||||
if (resultException != null) {
|
|
||||||
resultError = resultException.getLocalizedMessage(resources)
|
|
||||||
|
|
||||||
// Relaunch loading if we need to fix UUID
|
|
||||||
if (resultException is DuplicateUuidDatabaseException) {
|
|
||||||
showLoadDatabaseDuplicateUuidMessage {
|
|
||||||
|
|
||||||
var databaseUri: Uri? = null
|
|
||||||
var masterPassword: String? = null
|
|
||||||
var keyFileUri: Uri? = null
|
|
||||||
var readOnly = true
|
|
||||||
var cipherEntity: CipherDatabaseEntity? = null
|
|
||||||
|
|
||||||
result.data?.let { resultData ->
|
|
||||||
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
|
||||||
masterPassword = resultData.getString(MASTER_PASSWORD_KEY)
|
|
||||||
keyFileUri = resultData.getParcelable(KEY_FILE_KEY)
|
|
||||||
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
|
||||||
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
|
|
||||||
}
|
|
||||||
|
|
||||||
databaseUri?.let { databaseFileUri ->
|
|
||||||
showProgressDialogAndLoadDatabase(
|
|
||||||
databaseFileUri,
|
|
||||||
masterPassword,
|
|
||||||
keyFileUri,
|
|
||||||
readOnly,
|
|
||||||
cipherEntity,
|
|
||||||
true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show error message
|
|
||||||
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
|
||||||
resultError = "$resultError $resultMessage"
|
|
||||||
}
|
|
||||||
Log.e(TAG, resultError, resultException)
|
|
||||||
Snackbar.make(activity_password_coordinator_layout,
|
|
||||||
resultError,
|
|
||||||
Snackbar.LENGTH_LONG).asError().show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getUriFromIntent(intent: Intent?) {
|
|
||||||
// If is a view intent
|
|
||||||
val action = intent?.action
|
|
||||||
if (action != null
|
|
||||||
&& action == VIEW_INTENT) {
|
|
||||||
mDatabaseFileUri = intent.data
|
|
||||||
mDatabaseKeyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
|
|
||||||
} else {
|
|
||||||
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
|
|
||||||
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
|
||||||
super.onNewIntent(intent)
|
|
||||||
getUriFromIntent(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchGroupActivity() {
|
|
||||||
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
|
|
||||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
|
||||||
{
|
|
||||||
GroupActivity.launch(this@PasswordActivity,
|
|
||||||
true,
|
|
||||||
searchInfo,
|
|
||||||
readOnly)
|
|
||||||
// Finish activity if no search info
|
|
||||||
if (searchInfo != null) {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
|
||||||
Database.getInstance(),
|
|
||||||
searchInfo,
|
|
||||||
{ items ->
|
|
||||||
// Response is build
|
|
||||||
if (items.size == 1) {
|
|
||||||
populateKeyboardAndMoveAppToBackground(this@PasswordActivity,
|
|
||||||
items[0],
|
|
||||||
intent)
|
|
||||||
} else {
|
|
||||||
// Select the one we want
|
|
||||||
GroupActivity.launchForEntrySelectionResult(this,
|
|
||||||
true,
|
|
||||||
searchInfo)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Here no search info found, disable auto search
|
|
||||||
GroupActivity.launchForEntrySelectionResult(this@PasswordActivity,
|
|
||||||
false,
|
|
||||||
searchInfo,
|
|
||||||
readOnly)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Simply close if database not opened, normally not happened
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// Do not keep history
|
|
||||||
finish()
|
|
||||||
},
|
|
||||||
{ assistStructure ->
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
|
||||||
Database.getInstance(),
|
|
||||||
searchInfo,
|
|
||||||
{ items ->
|
|
||||||
// Response is build
|
|
||||||
AutofillHelper.buildResponse(this, items)
|
|
||||||
finish()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Here no search info found, disable auto search
|
|
||||||
GroupActivity.launchForAutofillResult(this@PasswordActivity,
|
|
||||||
assistStructure,
|
|
||||||
false,
|
|
||||||
searchInfo,
|
|
||||||
readOnly)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Simply close if database not opened, normally not happened
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private val onEditorActionListener = object : TextView.OnEditorActionListener {
|
|
||||||
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
|
|
||||||
if (actionId == IME_ACTION_DONE) {
|
|
||||||
verifyCheckboxesAndLoadDatabase()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
if (Database.getInstance().loaded) {
|
|
||||||
launchGroupActivity()
|
|
||||||
} else {
|
|
||||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
|
||||||
|
|
||||||
// If the database isn't accessible make sure to clear the password field, if it
|
|
||||||
// was saved in the instance state
|
|
||||||
if (Database.getInstance().loaded) {
|
|
||||||
clearCredentialsViews()
|
|
||||||
}
|
|
||||||
|
|
||||||
mProgressDialogThread?.registerProgressTask()
|
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
initUriFromIntent()
|
|
||||||
|
|
||||||
checkPermission()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
onPostInitUri(mDatabaseFileUri, mDatabaseKeyFileUri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
|
||||||
// Define title
|
|
||||||
databaseFileUri?.let {
|
|
||||||
FileDatabaseInfo(this, it).retrieveDatabaseTitle { title ->
|
|
||||||
filenameView?.text = title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define Key File text
|
|
||||||
if (mRememberKeyFile) {
|
|
||||||
populateKeyFileTextView(keyFileUri)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define listeners for default database checkbox and 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() }
|
|
||||||
|
|
||||||
// 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
|
|
||||||
val intent = intent
|
|
||||||
val password = intent.getStringExtra(KEY_PASSWORD)
|
|
||||||
// Consume the intent extra password
|
|
||||||
intent.removeExtra(KEY_PASSWORD)
|
|
||||||
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
|
|
||||||
if (password != null) {
|
|
||||||
populatePasswordTextView(password)
|
|
||||||
}
|
|
||||||
if (launchImmediately) {
|
|
||||||
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
|
|
||||||
} else {
|
|
||||||
// Init Biometric elements
|
|
||||||
var biometricInitialize = false
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
if (PreferencesUtil.isBiometricUnlockEnable(this)) {
|
|
||||||
|
|
||||||
if (advancedUnlockedManager == null && databaseFileUri != null) {
|
|
||||||
advancedUnlockedManager = AdvancedUnlockedManager(this,
|
|
||||||
databaseFileUri,
|
|
||||||
advancedUnlockInfoView,
|
|
||||||
checkboxPasswordView,
|
|
||||||
enableButtonOnCheckedChangeListener,
|
|
||||||
passwordView,
|
|
||||||
{ passwordEncrypted, ivSpec ->
|
|
||||||
// Load the database if password is registered with biometric
|
|
||||||
if (passwordEncrypted != null && ivSpec != null) {
|
|
||||||
verifyCheckboxesAndLoadDatabase(
|
|
||||||
CipherDatabaseEntity(
|
|
||||||
databaseFileUri.toString(),
|
|
||||||
passwordEncrypted,
|
|
||||||
ivSpec)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ passwordDecrypted ->
|
|
||||||
// Load the database if password is retrieve from biometric
|
|
||||||
passwordDecrypted?.let {
|
|
||||||
// Retrieve from biometric
|
|
||||||
verifyKeyFileCheckboxAndLoadDatabase(it)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
advancedUnlockedManager?.isBiometricPromptAutoOpenEnable = mAllowAutoOpenBiometricPrompt
|
|
||||||
advancedUnlockedManager?.checkBiometricAvailability()
|
|
||||||
biometricInitialize = true
|
|
||||||
} else {
|
|
||||||
advancedUnlockedManager?.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!biometricInitialize) {
|
|
||||||
checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
|
||||||
}
|
|
||||||
checkboxKeyFileView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
enableOrNotTheConfirmationButton()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun enableOrNotTheConfirmationButton() {
|
|
||||||
// Enable or not the open button if setting is checked
|
|
||||||
if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) {
|
|
||||||
checkboxPasswordView?.let {
|
|
||||||
confirmButtonView?.isEnabled = (checkboxPasswordView?.isChecked == true
|
|
||||||
|| checkboxKeyFileView?.isChecked == true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
confirmButtonView?.isEnabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
|
|
||||||
populatePasswordTextView(null)
|
|
||||||
if (clearKeyFile) {
|
|
||||||
populateKeyFileTextView(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun populatePasswordTextView(text: String?) {
|
|
||||||
if (text == null || text.isEmpty()) {
|
|
||||||
passwordView?.setText("")
|
|
||||||
if (checkboxPasswordView?.isChecked == true)
|
|
||||||
checkboxPasswordView?.isChecked = false
|
|
||||||
} else {
|
|
||||||
passwordView?.setText(text)
|
|
||||||
if (checkboxPasswordView?.isChecked != true)
|
|
||||||
checkboxPasswordView?.isChecked = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun populateKeyFileTextView(uri: Uri?) {
|
|
||||||
if (uri == null || uri.toString().isEmpty()) {
|
|
||||||
keyFileSelectionView?.uri = null
|
|
||||||
if (checkboxKeyFileView?.isChecked == true)
|
|
||||||
checkboxKeyFileView?.isChecked = false
|
|
||||||
} else {
|
|
||||||
keyFileSelectionView?.uri = uri
|
|
||||||
if (checkboxKeyFileView?.isChecked != true)
|
|
||||||
checkboxKeyFileView?.isChecked = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
mProgressDialogThread?.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()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
|
|
||||||
mDatabaseKeyFileUri?.let {
|
|
||||||
outState.putString(KEY_KEYFILE, it.toString())
|
|
||||||
}
|
|
||||||
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
|
||||||
outState.putBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT, false)
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
|
||||||
val password: String? = passwordView?.text?.toString()
|
|
||||||
val keyFile: Uri? = keyFileSelectionView?.uri
|
|
||||||
verifyCheckboxesAndLoadDatabase(password, keyFile, cipherDatabaseEntity)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun verifyCheckboxesAndLoadDatabase(password: String?,
|
|
||||||
keyFile: Uri?,
|
|
||||||
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
|
||||||
val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password
|
|
||||||
verifyKeyFileCheckbox(keyFile)
|
|
||||||
loadDatabase(mDatabaseFileUri, keyPassword, mDatabaseKeyFileUri, cipherDatabaseEntity)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) {
|
|
||||||
val keyFile: Uri? = keyFileSelectionView?.uri
|
|
||||||
verifyKeyFileCheckbox(keyFile)
|
|
||||||
loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun verifyKeyFileCheckbox(keyFile: Uri?) {
|
|
||||||
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadDatabase(databaseFileUri: Uri?,
|
|
||||||
password: String?,
|
|
||||||
keyFileUri: Uri?,
|
|
||||||
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
|
||||||
|
|
||||||
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
|
|
||||||
clearCredentialsViews()
|
|
||||||
}
|
|
||||||
|
|
||||||
databaseFileUri?.let { databaseUri ->
|
|
||||||
// Show the progress dialog and load the database
|
|
||||||
showProgressDialogAndLoadDatabase(
|
|
||||||
databaseUri,
|
|
||||||
password,
|
|
||||||
keyFileUri,
|
|
||||||
readOnly,
|
|
||||||
cipherDatabaseEntity,
|
|
||||||
false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
|
|
||||||
password: String?,
|
|
||||||
keyFile: Uri?,
|
|
||||||
readOnly: Boolean,
|
|
||||||
cipherDatabaseEntity: CipherDatabaseEntity?,
|
|
||||||
fixDuplicateUUID: Boolean) {
|
|
||||||
mProgressDialogThread?.startDatabaseLoad(
|
|
||||||
databaseUri,
|
|
||||||
password,
|
|
||||||
keyFile,
|
|
||||||
readOnly,
|
|
||||||
cipherDatabaseEntity,
|
|
||||||
fixDuplicateUUID
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showLoadDatabaseDuplicateUuidMessage(loadDatabaseWithFix: (() -> Unit)? = null) {
|
|
||||||
DuplicateUuidDialog().apply {
|
|
||||||
positiveAction = loadDatabaseWithFix
|
|
||||||
}.show(supportFragmentManager, "duplicateUUIDDialog")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
||||||
val inflater = menuInflater
|
|
||||||
// Read menu
|
|
||||||
inflater.inflate(R.menu.open_file, menu)
|
|
||||||
if (mSelectionMode || mForceReadOnly) {
|
|
||||||
menu.removeItem(R.id.menu_open_file_read_mode_key)
|
|
||||||
} else {
|
|
||||||
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mSelectionMode) {
|
|
||||||
MenuUtil.defaultMenuInflater(inflater, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
// biometric menu
|
|
||||||
advancedUnlockedManager?.inflateOptionsMenu(inflater, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onCreateOptionsMenu(menu)
|
|
||||||
|
|
||||||
launchEducation(menu)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check permission
|
|
||||||
private fun checkPermission() {
|
|
||||||
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
|
||||||
val permissions = arrayOf(writePermission)
|
|
||||||
if (Build.VERSION.SDK_INT >= 23
|
|
||||||
&& !readOnly
|
|
||||||
&& !mPermissionAsked) {
|
|
||||||
mPermissionAsked = true
|
|
||||||
// Check self permission to show or not the dialog
|
|
||||||
if (toolbar != null
|
|
||||||
&& ActivityCompat.checkSelfPermission(this, writePermission) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
ActivityCompat.requestPermissions(this, permissions, WRITE_EXTERNAL_STORAGE_REQUEST)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
|
||||||
|
|
||||||
when (requestCode) {
|
|
||||||
WRITE_EXTERNAL_STORAGE_REQUEST -> {
|
|
||||||
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
if (ActivityCompat.shouldShowRequestPermissionRationale(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE))
|
|
||||||
Toast.makeText(this, R.string.read_only_warning, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// To fix multiple view education
|
|
||||||
private var performedEductionInProgress = false
|
|
||||||
private fun launchEducation(menu: Menu) {
|
|
||||||
if (!performedEductionInProgress) {
|
|
||||||
performedEductionInProgress = true
|
|
||||||
// Show education views
|
|
||||||
Handler().post { performedNextEducation(PasswordActivityEducation(this), menu) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
|
|
||||||
menu: Menu) {
|
|
||||||
val educationToolbar = toolbar
|
|
||||||
val unlockEducationPerformed = educationToolbar != null
|
|
||||||
&& passwordActivityEducation.checkAndPerformedUnlockEducation(
|
|
||||||
educationToolbar,
|
|
||||||
{
|
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
|
||||||
})
|
|
||||||
if (!unlockEducationPerformed) {
|
|
||||||
val readOnlyEducationPerformed =
|
|
||||||
educationToolbar?.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
|
|
||||||
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
|
|
||||||
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
|
|
||||||
{
|
|
||||||
onOptionsItemSelected(menu.findItem(R.id.menu_open_file_read_mode_key))
|
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!readOnlyEducationPerformed) {
|
|
||||||
val biometricCanAuthenticate = BiometricManager.from(this).canAuthenticate()
|
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
|
||||||
&& PreferencesUtil.isBiometricUnlockEnable(applicationContext)
|
|
||||||
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
|
||||||
&& advancedUnlockInfoView != null && advancedUnlockInfoView?.unlockIconImageView != null
|
|
||||||
&& passwordActivityEducation.checkAndPerformedBiometricEducation(advancedUnlockInfoView?.unlockIconImageView!!,
|
|
||||||
{
|
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun changeOpenFileReadIcon(togglePassword: MenuItem) {
|
|
||||||
if (readOnly) {
|
|
||||||
togglePassword.setTitle(R.string.menu_file_selection_read_only)
|
|
||||||
togglePassword.setIcon(R.drawable.ic_read_only_white_24dp)
|
|
||||||
} else {
|
|
||||||
togglePassword.setTitle(R.string.menu_open_file_read_and_write)
|
|
||||||
togglePassword.setIcon(R.drawable.ic_read_write_white_24dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
|
|
||||||
when (item.itemId) {
|
|
||||||
android.R.id.home -> finish()
|
|
||||||
R.id.menu_open_file_read_mode_key -> {
|
|
||||||
readOnly = !readOnly
|
|
||||||
changeOpenFileReadIcon(item)
|
|
||||||
}
|
|
||||||
R.id.menu_biometric_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
advancedUnlockedManager?.deleteEntryKey()
|
|
||||||
}
|
|
||||||
else -> return MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(
|
|
||||||
requestCode: Int,
|
|
||||||
resultCode: Int,
|
|
||||||
data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
|
|
||||||
mAllowAutoOpenBiometricPrompt = false
|
|
||||||
|
|
||||||
// To get entry in result
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
var keyFileResult = false
|
|
||||||
mOpenFileHelper?.let {
|
|
||||||
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
|
||||||
) { uri ->
|
|
||||||
if (uri != null) {
|
|
||||||
mDatabaseKeyFileUri = uri
|
|
||||||
populateKeyFileTextView(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!keyFileResult) {
|
|
||||||
// this block if not a key file response
|
|
||||||
when (resultCode) {
|
|
||||||
LockingActivity.RESULT_EXIT_LOCK -> {
|
|
||||||
clearCredentialsViews()
|
|
||||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
|
||||||
}
|
|
||||||
Activity.RESULT_CANCELED -> {
|
|
||||||
clearCredentialsViews()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private val TAG = PasswordActivity::class.java.name
|
|
||||||
|
|
||||||
private const val KEY_FILENAME = "fileName"
|
|
||||||
private const val KEY_KEYFILE = "keyFile"
|
|
||||||
private const val VIEW_INTENT = "android.intent.action.VIEW"
|
|
||||||
|
|
||||||
private const val KEY_PASSWORD = "password"
|
|
||||||
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
|
||||||
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"
|
|
||||||
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?,
|
|
||||||
intentBuildLauncher: (Intent) -> Unit) {
|
|
||||||
val intent = Intent(activity, PasswordActivity::class.java)
|
|
||||||
intent.putExtra(KEY_FILENAME, databaseFile)
|
|
||||||
if (keyFile != null)
|
|
||||||
intent.putExtra(KEY_KEYFILE, keyFile)
|
|
||||||
intentBuildLauncher.invoke(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Standard Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
fun launch(
|
|
||||||
activity: Activity,
|
|
||||||
databaseFile: Uri,
|
|
||||||
keyFile: Uri?,
|
|
||||||
searchInfo: SearchInfo?) {
|
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
|
||||||
searchInfo?.let {
|
|
||||||
intent.putExtra(KEY_SEARCH_INFO, it)
|
|
||||||
}
|
|
||||||
activity.startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Keyboard Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
fun launchForKeyboardResult(
|
|
||||||
activity: Activity,
|
|
||||||
databaseFile: Uri,
|
|
||||||
keyFile: Uri?,
|
|
||||||
searchInfo: SearchInfo?) {
|
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
|
||||||
EntrySelectionHelper.startActivityForEntrySelectionResult(
|
|
||||||
activity,
|
|
||||||
intent,
|
|
||||||
searchInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Autofill Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
fun launchForAutofillResult(
|
|
||||||
activity: Activity,
|
|
||||||
databaseFile: Uri,
|
|
||||||
keyFile: Uri?,
|
|
||||||
assistStructure: AssistStructure?,
|
|
||||||
searchInfo: SearchInfo?) {
|
|
||||||
if (assistStructure != null) {
|
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
|
||||||
AutofillHelper.startActivityForAutofillResult(
|
|
||||||
activity,
|
|
||||||
intent,
|
|
||||||
assistStructure,
|
|
||||||
searchInfo)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
launch(activity, databaseFile, keyFile, searchInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import android.text.Editable
|
|
||||||
import android.text.TextWatcher
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.CompoundButton
|
|
||||||
import android.widget.TextView
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
|
||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
|
||||||
|
|
||||||
class AssignMasterKeyDialogFragment : DialogFragment() {
|
|
||||||
|
|
||||||
private var mMasterPassword: String? = null
|
|
||||||
private var mKeyFile: Uri? = null
|
|
||||||
|
|
||||||
private var rootView: View? = null
|
|
||||||
|
|
||||||
private var passwordCheckBox: CompoundButton? = null
|
|
||||||
|
|
||||||
private var passwordTextInputLayout: TextInputLayout? = null
|
|
||||||
private var passwordView: TextView? = null
|
|
||||||
private var passwordRepeatTextInputLayout: TextInputLayout? = null
|
|
||||||
private var passwordRepeatView: TextView? = null
|
|
||||||
|
|
||||||
private var keyFileCheckBox: CompoundButton? = null
|
|
||||||
private var keyFileSelectionView: KeyFileSelectionView? = null
|
|
||||||
|
|
||||||
private var mListener: AssignPasswordDialogListener? = null
|
|
||||||
|
|
||||||
private var mOpenFileHelper: OpenFileHelper? = null
|
|
||||||
|
|
||||||
private val passwordTextWatcher = object : TextWatcher {
|
|
||||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
|
||||||
|
|
||||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
|
||||||
|
|
||||||
override fun afterTextChanged(editable: Editable) {
|
|
||||||
passwordCheckBox?.isChecked = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AssignPasswordDialogListener {
|
|
||||||
fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?,
|
|
||||||
keyFileChecked: Boolean, keyFile: Uri?)
|
|
||||||
fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean, masterPassword: String?,
|
|
||||||
keyFileChecked: Boolean, keyFile: Uri?)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttach(activity: Context) {
|
|
||||||
super.onAttach(activity)
|
|
||||||
try {
|
|
||||||
mListener = activity as AssignPasswordDialogListener
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
throw ClassCastException(activity.toString()
|
|
||||||
+ " must implement " + AssignPasswordDialogListener::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
activity?.let { activity ->
|
|
||||||
|
|
||||||
var allowNoMasterKey = false
|
|
||||||
arguments?.apply {
|
|
||||||
if (containsKey(ALLOW_NO_MASTER_KEY_ARG))
|
|
||||||
allowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
|
||||||
val inflater = activity.layoutInflater
|
|
||||||
|
|
||||||
rootView = inflater.inflate(R.layout.fragment_set_password, null)
|
|
||||||
builder.setView(rootView)
|
|
||||||
.setTitle(R.string.assign_master_key)
|
|
||||||
// Add action buttons
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
|
||||||
|
|
||||||
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
|
||||||
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
|
|
||||||
passwordView = rootView?.findViewById(R.id.pass_password)
|
|
||||||
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
|
|
||||||
passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password)
|
|
||||||
|
|
||||||
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
|
||||||
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
|
||||||
|
|
||||||
mOpenFileHelper = OpenFileHelper(this)
|
|
||||||
keyFileSelectionView?.apply {
|
|
||||||
setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener)
|
|
||||||
setOnLongClickListener(mOpenFileHelper?.openFileOnClickViewListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
val dialog = builder.create()
|
|
||||||
|
|
||||||
if (passwordCheckBox != null && keyFileCheckBox!= null) {
|
|
||||||
dialog.setOnShowListener { dialog1 ->
|
|
||||||
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
|
|
||||||
positiveButton.setOnClickListener {
|
|
||||||
|
|
||||||
mMasterPassword = ""
|
|
||||||
mKeyFile = null
|
|
||||||
|
|
||||||
var error = verifyPassword() || verifyFile()
|
|
||||||
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
|
|
||||||
error = true
|
|
||||||
if (allowNoMasterKey)
|
|
||||||
showNoKeyConfirmationDialog()
|
|
||||||
else {
|
|
||||||
passwordTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!error) {
|
|
||||||
mListener?.onAssignKeyDialogPositiveClick(
|
|
||||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
|
||||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
|
|
||||||
negativeButton.setOnClickListener {
|
|
||||||
mListener?.onAssignKeyDialogNegativeClick(
|
|
||||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
|
||||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dialog
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.onCreateDialog(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
// To check checkboxes if a text is present
|
|
||||||
passwordView?.addTextChangedListener(passwordTextWatcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
|
|
||||||
passwordView?.removeTextChangedListener(passwordTextWatcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun verifyPassword(): Boolean {
|
|
||||||
var error = false
|
|
||||||
if (passwordCheckBox != null
|
|
||||||
&& passwordCheckBox!!.isChecked
|
|
||||||
&& passwordView != null
|
|
||||||
&& passwordRepeatView != null) {
|
|
||||||
mMasterPassword = passwordView!!.text.toString()
|
|
||||||
val confPassword = passwordRepeatView!!.text.toString()
|
|
||||||
|
|
||||||
// Verify that passwords match
|
|
||||||
if (mMasterPassword != confPassword) {
|
|
||||||
error = true
|
|
||||||
// Passwords do not match
|
|
||||||
passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mMasterPassword == null || mMasterPassword!!.isEmpty()) {
|
|
||||||
error = true
|
|
||||||
showEmptyPasswordConfirmationDialog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return error
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun verifyFile(): Boolean {
|
|
||||||
var error = false
|
|
||||||
if (keyFileCheckBox != null
|
|
||||||
&& keyFileCheckBox!!.isChecked) {
|
|
||||||
|
|
||||||
keyFileSelectionView?.uri?.let { uri ->
|
|
||||||
mKeyFile = uri
|
|
||||||
} ?: run {
|
|
||||||
error = true
|
|
||||||
keyFileSelectionView?.error = getString(R.string.error_nokeyfile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return error
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showEmptyPasswordConfirmationDialog() {
|
|
||||||
activity?.let {
|
|
||||||
val builder = AlertDialog.Builder(it)
|
|
||||||
builder.setMessage(R.string.warning_empty_password)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
if (!verifyFile()) {
|
|
||||||
mListener?.onAssignKeyDialogPositiveClick(
|
|
||||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
|
||||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
|
||||||
this@AssignMasterKeyDialogFragment.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
|
||||||
builder.create().show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showNoKeyConfirmationDialog() {
|
|
||||||
activity?.let {
|
|
||||||
val builder = AlertDialog.Builder(it)
|
|
||||||
builder.setMessage(R.string.warning_no_encryption_key)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
mListener?.onAssignKeyDialogPositiveClick(
|
|
||||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
|
||||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
|
||||||
this@AssignMasterKeyDialogFragment.dismiss()
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
|
||||||
builder.create().show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
|
|
||||||
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
|
||||||
) { uri ->
|
|
||||||
uri?.let { pathUri ->
|
|
||||||
keyFileCheckBox?.isChecked = true
|
|
||||||
keyFileSelectionView?.uri = pathUri
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
|
|
||||||
|
|
||||||
fun getInstance(allowNoMasterKey: Boolean): AssignMasterKeyDialogFragment {
|
|
||||||
val fragment = AssignMasterKeyDialogFragment()
|
|
||||||
val args = Bundle()
|
|
||||||
args.putBoolean(ALLOW_NO_MASTER_KEY_ARG, allowNoMasterKey)
|
|
||||||
fragment.arguments = args
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import com.kunzisoft.androidclearchroma.view.ChromaColorView
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
|
||||||
|
|
||||||
|
class ColorPickerDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
|
private val mColorPickerViewModel: ColorPickerViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private lateinit var enableSwitchView: CompoundButton
|
||||||
|
private lateinit var chromaColorView: ChromaColorView
|
||||||
|
|
||||||
|
private var mDefaultColor = Color.WHITE
|
||||||
|
private var mActivated = false
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|
||||||
|
activity?.let { activity ->
|
||||||
|
val root = activity.layoutInflater.inflate(R.layout.fragment_color_picker, null)
|
||||||
|
enableSwitchView = root.findViewById(R.id.switch_element)
|
||||||
|
chromaColorView = root.findViewById(R.id.chroma_color_view)
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
if (savedInstanceState.containsKey(ARG_INITIAL_COLOR)) {
|
||||||
|
mDefaultColor = savedInstanceState.getInt(ARG_INITIAL_COLOR)
|
||||||
|
}
|
||||||
|
if (savedInstanceState.containsKey(ARG_ACTIVATED)) {
|
||||||
|
mActivated = savedInstanceState.getBoolean(ARG_ACTIVATED)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(ARG_INITIAL_COLOR)) {
|
||||||
|
mDefaultColor = getInt(ARG_INITIAL_COLOR)
|
||||||
|
}
|
||||||
|
if (containsKey(ARG_ACTIVATED)) {
|
||||||
|
mActivated = getBoolean(ARG_ACTIVATED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enableSwitchView.isChecked = mActivated
|
||||||
|
chromaColorView.currentColor = mDefaultColor
|
||||||
|
|
||||||
|
chromaColorView.setOnColorChangedListener {
|
||||||
|
if (!enableSwitchView.isChecked)
|
||||||
|
enableSwitchView.isChecked = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
builder.setView(root)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
val color: Int? = if (enableSwitchView.isChecked)
|
||||||
|
chromaColorView.currentColor
|
||||||
|
else
|
||||||
|
null
|
||||||
|
mColorPickerViewModel.pickColor(color)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putInt(ARG_INITIAL_COLOR, chromaColorView.currentColor)
|
||||||
|
outState.putBoolean(ARG_ACTIVATED, mActivated)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ARG_INITIAL_COLOR = "ARG_INITIAL_COLOR"
|
||||||
|
private const val ARG_ACTIVATED = "ARG_ACTIVATED"
|
||||||
|
|
||||||
|
fun newInstance(
|
||||||
|
@ColorInt initialColor: Int?,
|
||||||
|
): ColorPickerDialogFragment {
|
||||||
|
return ColorPickerDialogFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putInt(ARG_INITIAL_COLOR, initialColor ?: Color.WHITE)
|
||||||
|
putBoolean(ARG_ACTIVATED, initialColor != null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* 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 android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
|
var actionDatabaseListener: ActionDatabaseChangedListener? = null
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
actionDatabaseListener = null
|
||||||
|
this.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
|
||||||
|
val oldSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelableCompat(OLD_FILE_DATABASE_INFO)
|
||||||
|
val newSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelableCompat(NEW_FILE_DATABASE_INFO)
|
||||||
|
|
||||||
|
if (oldSnapFileDatabaseInfo != null && newSnapFileDatabaseInfo != null) {
|
||||||
|
// Use the Builder class for convenient dialog construction
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|
||||||
|
val stringBuilder = SpannableStringBuilder()
|
||||||
|
if (newSnapFileDatabaseInfo.exists) {
|
||||||
|
stringBuilder.append(getString(R.string.warning_database_info_changed))
|
||||||
|
stringBuilder.append("\n\n" +oldSnapFileDatabaseInfo.toString(activity)
|
||||||
|
+ "\n→\n" +
|
||||||
|
newSnapFileDatabaseInfo.toString(activity) + "\n\n")
|
||||||
|
stringBuilder.append(getString(R.string.warning_database_info_changed_options))
|
||||||
|
} else {
|
||||||
|
stringBuilder.append(getString(R.string.warning_database_revoked))
|
||||||
|
}
|
||||||
|
builder.setMessage(stringBuilder)
|
||||||
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
actionDatabaseListener?.validateDatabaseChanged()
|
||||||
|
}
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionDatabaseChangedListener {
|
||||||
|
fun validateDatabaseChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val DATABASE_CHANGED_DIALOG_TAG = "databaseChangedDialogFragment"
|
||||||
|
private const val OLD_FILE_DATABASE_INFO = "OLD_FILE_DATABASE_INFO"
|
||||||
|
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
|
||||||
|
|
||||||
|
fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
newSnapFileDatabaseInfo: SnapFileDatabaseInfo
|
||||||
|
)
|
||||||
|
: DatabaseChangedDialogFragment {
|
||||||
|
val fragment = DatabaseChangedDialogFragment()
|
||||||
|
fragment.arguments = Bundle().apply {
|
||||||
|
putParcelable(OLD_FILE_DATABASE_INFO, oldSnapFileDatabaseInfo)
|
||||||
|
putParcelable(NEW_FILE_DATABASE_INFO, newSnapFileDatabaseInfo)
|
||||||
|
}
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
|
||||||
|
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
||||||
|
|
||||||
|
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||||
|
private var mDatabase: ContextualDatabase? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
mDatabaseViewModel.database.observe(this) { database ->
|
||||||
|
this.mDatabase = database
|
||||||
|
resetAppTimeoutOnTouchOrFocus()
|
||||||
|
onDatabaseRetrieved(database)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.actionFinished.observe(this) { result ->
|
||||||
|
onDatabaseActionFinished(result.database, result.actionTask, result.result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
|
||||||
|
resetAppTimeoutOnTouchOrFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
|
// Can be overridden by a subclass
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseActionFinished(
|
||||||
|
database: ContextualDatabase,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
// Can be overridden by a subclass
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetAppTimeout() {
|
||||||
|
context?.let {
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(it,
|
||||||
|
mDatabase?.loaded ?: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun overrideTimeoutTouchAndFocusEvents(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetAppTimeoutOnTouchOrFocus() {
|
||||||
|
if (!overrideTimeoutTouchAndFocusEvents()) {
|
||||||
|
context?.let {
|
||||||
|
dialog?.window?.decorView?.resetAppTimeoutWhenViewTouchedOrFocused(
|
||||||
|
it,
|
||||||
|
mDatabase?.loaded
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.activities.dialogs
|
|
||||||
|
|
||||||
import android.app.DatePickerDialog
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
|
|
||||||
class DatePickerFragment : DialogFragment() {
|
|
||||||
|
|
||||||
private var mDefaultYear: Int = 2000
|
|
||||||
private var mDefaultMonth: Int = 1
|
|
||||||
private var mDefaultDay: Int = 1
|
|
||||||
|
|
||||||
private var mListener: DatePickerDialog.OnDateSetListener? = null
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
try {
|
|
||||||
mListener = context as DatePickerDialog.OnDateSetListener
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
throw ClassCastException(context.toString()
|
|
||||||
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
// Create a new instance of DatePickerDialog and return it
|
|
||||||
return context?.let {
|
|
||||||
arguments?.apply {
|
|
||||||
if (containsKey(DEFAULT_YEAR_BUNDLE_KEY))
|
|
||||||
mDefaultYear = getInt(DEFAULT_YEAR_BUNDLE_KEY)
|
|
||||||
if (containsKey(DEFAULT_MONTH_BUNDLE_KEY))
|
|
||||||
mDefaultMonth = getInt(DEFAULT_MONTH_BUNDLE_KEY)
|
|
||||||
if (containsKey(DEFAULT_DAY_BUNDLE_KEY))
|
|
||||||
mDefaultDay = getInt(DEFAULT_DAY_BUNDLE_KEY)
|
|
||||||
}
|
|
||||||
|
|
||||||
DatePickerDialog(it, mListener, mDefaultYear, mDefaultMonth, mDefaultDay)
|
|
||||||
} ?: super.onCreateDialog(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val DEFAULT_YEAR_BUNDLE_KEY = "DEFAULT_YEAR_BUNDLE_KEY"
|
|
||||||
private const val DEFAULT_MONTH_BUNDLE_KEY = "DEFAULT_MONTH_BUNDLE_KEY"
|
|
||||||
private const val DEFAULT_DAY_BUNDLE_KEY = "DEFAULT_DAY_BUNDLE_KEY"
|
|
||||||
|
|
||||||
fun getInstance(defaultYear: Int,
|
|
||||||
defaultMonth: Int,
|
|
||||||
defaultDay: Int): DatePickerFragment {
|
|
||||||
return DatePickerFragment().apply {
|
|
||||||
arguments = Bundle().apply {
|
|
||||||
putInt(DEFAULT_YEAR_BUNDLE_KEY, defaultYear)
|
|
||||||
putInt(DEFAULT_MONTH_BUNDLE_KEY, defaultMonth)
|
|
||||||
putInt(DEFAULT_DAY_BUNDLE_KEY, defaultDay)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,73 +20,54 @@
|
|||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.activityViewModels
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.viewmodels.NodesViewModel
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
|
||||||
|
|
||||||
class DeleteNodesDialogFragment : DialogFragment() {
|
class DeleteNodesDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
private var mNodesToDelete: List<Node> = ArrayList()
|
private var mNodesToDelete: List<Node> = listOf()
|
||||||
private var mListener: DeleteNodeListener? = null
|
private val mNodesViewModel: NodesViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
try {
|
|
||||||
mListener = context as DeleteNodeListener
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
throw ClassCastException(context.toString()
|
|
||||||
+ " must implement " + DeleteNodeListener::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
mNodesViewModel.nodesToDelete.observe(this) { nodes ->
|
||||||
|
this.mNodesToDelete = nodes
|
||||||
|
}
|
||||||
|
var recycleBin = false
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY)
|
if (containsKey(RECYCLE_BIN_TAG)) {
|
||||||
&& containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) {
|
recycleBin = this.getBoolean(RECYCLE_BIN_TAG)
|
||||||
mNodesToDelete = getListNodesFromBundle(Database.getInstance(), this)
|
|
||||||
}
|
|
||||||
} ?: savedInstanceState?.apply {
|
|
||||||
if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY)
|
|
||||||
&& containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) {
|
|
||||||
mNodesToDelete = getListNodesFromBundle(Database.getInstance(), savedInstanceState)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
// Use the Builder class for convenient dialog construction
|
// Use the Builder class for convenient dialog construction
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|
||||||
builder.setMessage(getString(R.string.warning_permanently_delete_nodes))
|
builder.setMessage(if (recycleBin)
|
||||||
builder.setPositiveButton(android.R.string.yes) { _, _ ->
|
getString(R.string.warning_empty_recycle_bin)
|
||||||
mListener?.permanentlyDeleteNodes(mNodesToDelete)
|
else
|
||||||
|
getString(R.string.warning_permanently_delete_nodes))
|
||||||
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
mNodesViewModel.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()
|
||||||
}
|
}
|
||||||
return super.onCreateDialog(savedInstanceState)
|
return super.onCreateDialog(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
outState.putAll(getBundleFromListNodes(mNodesToDelete))
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeleteNodeListener {
|
|
||||||
fun permanentlyDeleteNodes(nodes: List<Node>)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getInstance(nodesToDelete: List<Node>): DeleteNodesDialogFragment {
|
private const val RECYCLE_BIN_TAG = "RECYCLE_BIN_TAG"
|
||||||
|
|
||||||
|
fun getInstance(recycleBin: Boolean): DeleteNodesDialogFragment {
|
||||||
return DeleteNodesDialogFragment().apply {
|
return DeleteNodesDialogFragment().apply {
|
||||||
arguments = getBundleFromListNodes(nodesToDelete)
|
arguments = Bundle().apply {
|
||||||
|
putBoolean(RECYCLE_BIN_TAG, recycleBin)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ class DuplicateUuidDialog : DialogFragment() {
|
|||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
// Use the Builder class for convenient dialog construction
|
// Use the Builder class for convenient dialog construction
|
||||||
val builder = androidx.appcompat.app.AlertDialog.Builder(activity).apply {
|
val builder = AlertDialog.Builder(activity).apply {
|
||||||
val message = getString(R.string.contains_duplicate_uuid) +
|
val message = getString(R.string.contains_duplicate_uuid) +
|
||||||
"\n\n" + getString(R.string.contains_duplicate_uuid_procedure)
|
"\n\n" + getString(R.string.contains_duplicate_uuid_procedure)
|
||||||
setMessage(message)
|
setMessage(message)
|
||||||
|
|||||||
@@ -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 com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Field
|
||||||
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
|
||||||
|
|
||||||
|
class EntryCustomFieldDialogFragment: DatabaseDialogFragment() {
|
||||||
|
|
||||||
|
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?.getParcelableCompat(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,12 +21,12 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
|
|
||||||
class FileManagerDialogFragment : DialogFragment() {
|
class FileManagerDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ class FileManagerDialogFragment : DialogFragment() {
|
|||||||
textDescription.text = getString(R.string.file_manager_install_description)
|
textDescription.text = getString(R.string.file_manager_install_description)
|
||||||
|
|
||||||
root.findViewById<Button>(R.id.file_manager_button).setOnClickListener {
|
root.findViewById<Button>(R.id.file_manager_button).setOnClickListener {
|
||||||
UriUtil.gotoUrl(requireContext(), R.string.file_manager_explanation_url)
|
context?.openUrl(R.string.file_manager_explanation_url)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* 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.utils.getParcelableCompat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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?.getParcelableCompat(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.CompoundButton
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.SeekBar
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.password.PasswordGenerator
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
|
||||||
|
|
||||||
class GeneratePasswordDialogFragment : DialogFragment() {
|
|
||||||
|
|
||||||
private var mListener: GeneratePasswordListener? = null
|
|
||||||
|
|
||||||
private var root: View? = null
|
|
||||||
private var lengthTextView: EditText? = null
|
|
||||||
private var passwordInputLayoutView: TextInputLayout? = null
|
|
||||||
private var passwordView: EditText? = null
|
|
||||||
|
|
||||||
private var uppercaseBox: CompoundButton? = null
|
|
||||||
private var lowercaseBox: CompoundButton? = null
|
|
||||||
private var digitsBox: CompoundButton? = null
|
|
||||||
private var minusBox: CompoundButton? = null
|
|
||||||
private var underlineBox: CompoundButton? = null
|
|
||||||
private var spaceBox: CompoundButton? = null
|
|
||||||
private var specialsBox: CompoundButton? = null
|
|
||||||
private var bracketsBox: CompoundButton? = null
|
|
||||||
private var extendedBox: CompoundButton? = null
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
try {
|
|
||||||
mListener = context as GeneratePasswordListener
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
throw ClassCastException(context.toString()
|
|
||||||
+ " must implement " + GeneratePasswordListener::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
activity?.let { activity ->
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
|
||||||
val inflater = activity.layoutInflater
|
|
||||||
root = inflater.inflate(R.layout.fragment_generate_password, null)
|
|
||||||
|
|
||||||
passwordInputLayoutView = root?.findViewById(R.id.password_input_layout)
|
|
||||||
passwordView = root?.findViewById(R.id.password)
|
|
||||||
passwordView?.applyFontVisibility()
|
|
||||||
|
|
||||||
lengthTextView = root?.findViewById(R.id.length)
|
|
||||||
|
|
||||||
uppercaseBox = root?.findViewById(R.id.cb_uppercase)
|
|
||||||
lowercaseBox = root?.findViewById(R.id.cb_lowercase)
|
|
||||||
digitsBox = root?.findViewById(R.id.cb_digits)
|
|
||||||
minusBox = root?.findViewById(R.id.cb_minus)
|
|
||||||
underlineBox = root?.findViewById(R.id.cb_underline)
|
|
||||||
spaceBox = root?.findViewById(R.id.cb_space)
|
|
||||||
specialsBox = root?.findViewById(R.id.cb_specials)
|
|
||||||
bracketsBox = root?.findViewById(R.id.cb_brackets)
|
|
||||||
extendedBox = root?.findViewById(R.id.cb_extended)
|
|
||||||
|
|
||||||
assignDefaultCharacters()
|
|
||||||
|
|
||||||
val seekBar = root?.findViewById<SeekBar>(R.id.seekbar_length)
|
|
||||||
seekBar?.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
||||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
|
||||||
lengthTextView?.setText(progress.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
|
||||||
|
|
||||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
|
||||||
})
|
|
||||||
|
|
||||||
context?.let { context ->
|
|
||||||
seekBar?.progress = PreferencesUtil.getDefaultPasswordLength(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
root?.findViewById<Button>(R.id.generate_password_button)
|
|
||||||
?.setOnClickListener { fillPassword() }
|
|
||||||
|
|
||||||
builder.setView(root)
|
|
||||||
.setPositiveButton(R.string.accept) { _, _ ->
|
|
||||||
val bundle = Bundle()
|
|
||||||
bundle.putString(KEY_PASSWORD_ID, passwordView!!.text.toString())
|
|
||||||
mListener?.acceptPassword(bundle)
|
|
||||||
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
val bundle = Bundle()
|
|
||||||
mListener?.cancelPassword(bundle)
|
|
||||||
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pre-populate a password to possibly save the user a few clicks
|
|
||||||
fillPassword()
|
|
||||||
|
|
||||||
return builder.create()
|
|
||||||
}
|
|
||||||
return super.onCreateDialog(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun assignDefaultCharacters() {
|
|
||||||
uppercaseBox?.isChecked = false
|
|
||||||
lowercaseBox?.isChecked = false
|
|
||||||
digitsBox?.isChecked = false
|
|
||||||
minusBox?.isChecked = false
|
|
||||||
underlineBox?.isChecked = false
|
|
||||||
spaceBox?.isChecked = false
|
|
||||||
specialsBox?.isChecked = false
|
|
||||||
bracketsBox?.isChecked = false
|
|
||||||
extendedBox?.isChecked = false
|
|
||||||
|
|
||||||
context?.let { context ->
|
|
||||||
PreferencesUtil.getDefaultPasswordCharacters(context)?.let { charSet ->
|
|
||||||
for (passwordChar in charSet) {
|
|
||||||
when (passwordChar) {
|
|
||||||
getString(R.string.value_password_uppercase) -> uppercaseBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_lowercase) -> lowercaseBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_digits) -> digitsBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_minus) -> minusBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_underline) -> underlineBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_space) -> spaceBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_special) -> specialsBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_brackets) -> bracketsBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_extended) -> extendedBox?.isChecked = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fillPassword() {
|
|
||||||
root?.findViewById<EditText>(R.id.password)?.setText(generatePassword())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generatePassword(): String {
|
|
||||||
var password = ""
|
|
||||||
try {
|
|
||||||
val length = Integer.valueOf(root?.findViewById<EditText>(R.id.length)?.text.toString())
|
|
||||||
password = PasswordGenerator(resources).generatePassword(length,
|
|
||||||
uppercaseBox?.isChecked == true,
|
|
||||||
lowercaseBox?.isChecked == true,
|
|
||||||
digitsBox?.isChecked == true,
|
|
||||||
minusBox?.isChecked == true,
|
|
||||||
underlineBox?.isChecked == true,
|
|
||||||
spaceBox?.isChecked == true,
|
|
||||||
specialsBox?.isChecked == true,
|
|
||||||
bracketsBox?.isChecked == true,
|
|
||||||
extendedBox?.isChecked == true)
|
|
||||||
passwordInputLayoutView?.error = null
|
|
||||||
} catch (e: NumberFormatException) {
|
|
||||||
passwordInputLayoutView?.error = getString(R.string.error_wrong_length)
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
passwordInputLayoutView?.error = e.message
|
|
||||||
}
|
|
||||||
|
|
||||||
return password
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GeneratePasswordListener {
|
|
||||||
fun acceptPassword(bundle: Bundle)
|
|
||||||
fun cancelPassword(bundle: Bundle)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val KEY_PASSWORD_ID = "KEY_PASSWORD_ID"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 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.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.adapters.TagsAdapter
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
||||||
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
import com.kunzisoft.keepass.view.DateTimeFieldView
|
||||||
|
|
||||||
|
class GroupDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
|
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
|
||||||
|
private var mGroupInfo = GroupInfo()
|
||||||
|
|
||||||
|
private lateinit var iconView: ImageView
|
||||||
|
private var mIconColor: Int = 0
|
||||||
|
private lateinit var nameTextView: TextView
|
||||||
|
private lateinit var tagsListView: RecyclerView
|
||||||
|
private var tagsAdapter: TagsAdapter? = null
|
||||||
|
private lateinit var notesTextLabelView: TextView
|
||||||
|
private lateinit var notesTextView: TextView
|
||||||
|
private lateinit var expirationView: DateTimeFieldView
|
||||||
|
private lateinit var creationView: TextView
|
||||||
|
private lateinit var modificationView: TextView
|
||||||
|
private lateinit var searchableLabelView: TextView
|
||||||
|
private lateinit var searchableView: TextView
|
||||||
|
private lateinit var autoTypeLabelView: TextView
|
||||||
|
private lateinit var autoTypeView: TextView
|
||||||
|
private lateinit var uuidContainerView: ViewGroup
|
||||||
|
private lateinit var uuidReferenceView: TextView
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
mPopulateIconMethod = { imageView, icon ->
|
||||||
|
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||||
|
}
|
||||||
|
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
|
||||||
|
|
||||||
|
if (database?.allowCustomSearchableGroup() == true) {
|
||||||
|
searchableLabelView.visibility = View.VISIBLE
|
||||||
|
searchableView.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
searchableLabelView.visibility = View.GONE
|
||||||
|
searchableView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Auto-Type
|
||||||
|
/*
|
||||||
|
if (database?.allowAutoType() == true) {
|
||||||
|
autoTypeLabelView.visibility = View.VISIBLE
|
||||||
|
autoTypeView.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
autoTypeLabelView.visibility = View.GONE
|
||||||
|
autoTypeView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
val root = activity.layoutInflater.inflate(R.layout.fragment_group, null)
|
||||||
|
iconView = root.findViewById(R.id.group_icon)
|
||||||
|
nameTextView = root.findViewById(R.id.group_name)
|
||||||
|
tagsListView = root.findViewById(R.id.group_tags_list_view)
|
||||||
|
notesTextLabelView = root.findViewById(R.id.group_note_label)
|
||||||
|
notesTextView = root.findViewById(R.id.group_note)
|
||||||
|
expirationView = root.findViewById(R.id.group_expiration)
|
||||||
|
creationView = root.findViewById(R.id.group_created)
|
||||||
|
modificationView = root.findViewById(R.id.group_modified)
|
||||||
|
searchableLabelView = root.findViewById(R.id.group_searchable_label)
|
||||||
|
searchableView = root.findViewById(R.id.group_searchable)
|
||||||
|
autoTypeLabelView = root.findViewById(R.id.group_auto_type_label)
|
||||||
|
autoTypeView = root.findViewById(R.id.group_auto_type)
|
||||||
|
uuidContainerView = root.findViewById(R.id.group_UUID_container)
|
||||||
|
uuidReferenceView = root.findViewById(R.id.group_UUID_reference)
|
||||||
|
|
||||||
|
// Retrieve the textColor to tint the icon
|
||||||
|
val ta = activity.theme.obtainStyledAttributes(intArrayOf(R.attr.colorSecondary))
|
||||||
|
mIconColor = ta.getColor(0, Color.WHITE)
|
||||||
|
ta.recycle()
|
||||||
|
|
||||||
|
if (savedInstanceState != null
|
||||||
|
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
|
||||||
|
mGroupInfo = savedInstanceState.getParcelableCompat(KEY_GROUP_INFO) ?: mGroupInfo
|
||||||
|
} else {
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(KEY_GROUP_INFO)) {
|
||||||
|
mGroupInfo = getParcelableCompat(KEY_GROUP_INFO) ?: mGroupInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// populate info in views
|
||||||
|
val title = mGroupInfo.title
|
||||||
|
if (title.isEmpty()) {
|
||||||
|
nameTextView.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
nameTextView.text = title
|
||||||
|
nameTextView.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
tagsAdapter = TagsAdapter(activity)
|
||||||
|
tagsListView.apply {
|
||||||
|
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
adapter = tagsAdapter
|
||||||
|
}
|
||||||
|
val tags = mGroupInfo.tags
|
||||||
|
tagsListView.visibility = if (tags.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
tagsAdapter?.setTags(tags)
|
||||||
|
val notes = mGroupInfo.notes
|
||||||
|
if (notes == null || notes.isEmpty()) {
|
||||||
|
notesTextLabelView.visibility = View.GONE
|
||||||
|
notesTextView.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
notesTextView.text = notes
|
||||||
|
notesTextLabelView.visibility = View.VISIBLE
|
||||||
|
notesTextView.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
expirationView.activation = mGroupInfo.expires
|
||||||
|
expirationView.dateTime = mGroupInfo.expiryTime
|
||||||
|
creationView.text = mGroupInfo.creationTime.getDateTimeString(resources)
|
||||||
|
modificationView.text = mGroupInfo.lastModificationTime.getDateTimeString(resources)
|
||||||
|
searchableView.text = stringFromInheritableBoolean(mGroupInfo.searchable)
|
||||||
|
autoTypeView.text = stringFromInheritableBoolean(mGroupInfo.enableAutoType,
|
||||||
|
mGroupInfo.defaultAutoTypeSequence)
|
||||||
|
val uuid = UuidUtil.toHexString(mGroupInfo.id)
|
||||||
|
if (uuid == null || uuid.isEmpty()) {
|
||||||
|
uuidContainerView.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
uuidReferenceView.text = uuid
|
||||||
|
uuidContainerView.apply {
|
||||||
|
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
builder.setView(root)
|
||||||
|
.setPositiveButton(android.R.string.ok){ _, _ ->
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stringFromInheritableBoolean(enable: Boolean?, value: String? = null): String {
|
||||||
|
val valueString = if (value != null && value.isNotEmpty()) " [$value]" else ""
|
||||||
|
return when {
|
||||||
|
enable == null -> getString(R.string.inherited) + valueString
|
||||||
|
enable -> getString(R.string.enable) + valueString
|
||||||
|
else -> getString(R.string.disable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
outState.putParcelable(KEY_GROUP_INFO, mGroupInfo)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Error(val isError: Boolean, val messageId: Int?)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG_SHOW_GROUP = "TAG_SHOW_GROUP"
|
||||||
|
private const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
||||||
|
|
||||||
|
fun launch(groupInfo: GroupInfo): GroupDialogFragment {
|
||||||
|
val bundle = Bundle()
|
||||||
|
bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
|
||||||
|
val fragment = GroupDialogFragment()
|
||||||
|
fragment.arguments = bundle
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,37 +20,57 @@
|
|||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import android.view.View
|
||||||
import androidx.fragment.app.DialogFragment
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION
|
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.NONE
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
|
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.adapters.TagsProposalAdapter
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
import com.kunzisoft.keepass.view.DateTimeEditFieldView
|
||||||
|
import com.kunzisoft.keepass.view.InheritedCompletionView
|
||||||
|
import com.kunzisoft.keepass.view.TagsCompletionView
|
||||||
|
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
|
||||||
|
import com.tokenautocomplete.FilteredArrayAdapter
|
||||||
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconPickerListener {
|
class GroupEditDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
private var mDatabase: Database? = null
|
private val mGroupEditViewModel: GroupEditViewModel by activityViewModels()
|
||||||
|
|
||||||
private var editGroupListener: EditGroupListener? = null
|
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
|
||||||
|
private var mEditGroupDialogAction = NONE
|
||||||
|
private var mGroupInfo = GroupInfo()
|
||||||
|
private var mGroupNamesNotAllowed: List<String>? = null
|
||||||
|
|
||||||
private var editGroupDialogAction: EditGroupDialogAction? = null
|
private lateinit var iconButtonView: ImageView
|
||||||
private var nameGroup: String? = null
|
private var mIconColor: Int = 0
|
||||||
private var iconGroup: IconImage? = null
|
private lateinit var nameTextLayoutView: TextInputLayout
|
||||||
|
private lateinit var nameTextView: TextView
|
||||||
private var nameTextLayoutView: TextInputLayout? = null
|
private lateinit var notesTextLayoutView: TextInputLayout
|
||||||
private var nameTextView: TextView? = null
|
private lateinit var notesTextView: TextView
|
||||||
private var iconButtonView: ImageView? = null
|
private lateinit var expirationView: DateTimeEditFieldView
|
||||||
private var iconColor: Int = 0
|
private lateinit var searchableContainerView: TextInputLayout
|
||||||
|
private lateinit var searchableView: InheritedCompletionView
|
||||||
|
private lateinit var autoTypeContainerView: ViewGroup
|
||||||
|
private lateinit var autoTypeInheritedView: InheritedCompletionView
|
||||||
|
private lateinit var autoTypeSequenceView: TextView
|
||||||
|
private lateinit var tagsContainerView: TextInputLayout
|
||||||
|
private lateinit var tagsCompletionView: TagsCompletionView
|
||||||
|
private var tagsAdapter: FilteredArrayAdapter<String>? = null
|
||||||
|
|
||||||
enum class EditGroupDialogAction {
|
enum class EditGroupDialogAction {
|
||||||
CREATION, UPDATE, NONE;
|
CREATION, UPDATE, NONE;
|
||||||
@@ -62,77 +82,125 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onAttach(context)
|
super.onCreate(savedInstanceState)
|
||||||
// Verify that the host activity implements the callback interface
|
|
||||||
try {
|
mGroupEditViewModel.onIconSelected.observe(this) { iconImage ->
|
||||||
// Instantiate the NoticeDialogListener so we can send events to the host
|
mGroupInfo.icon = iconImage
|
||||||
editGroupListener = context as EditGroupListener
|
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
// The activity doesn't implement the interface, throw exception
|
|
||||||
throw ClassCastException(context.toString()
|
|
||||||
+ " must implement " + GroupEditDialogFragment::class.java.name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mGroupEditViewModel.onDateSelected.observe(this) { dateMilliseconds ->
|
||||||
|
// Save the date
|
||||||
|
mGroupInfo.expiryTime = DateInstant(
|
||||||
|
DateTime(mGroupInfo.expiryTime.date)
|
||||||
|
.withMillis(dateMilliseconds)
|
||||||
|
.toDate())
|
||||||
|
expirationView.dateTime = mGroupInfo.expiryTime
|
||||||
|
if (expirationView.dateTime.type == DateInstant.Type.DATE_TIME) {
|
||||||
|
val instantTime = DateInstant(mGroupInfo.expiryTime.date, DateInstant.Type.TIME)
|
||||||
|
// Trick to recall selection with time
|
||||||
|
mGroupEditViewModel.requestDateTimeSelection(instantTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mGroupEditViewModel.onTimeSelected.observe(this) { viewModelTime ->
|
||||||
|
// Save the time
|
||||||
|
mGroupInfo.expiryTime = DateInstant(
|
||||||
|
DateTime(mGroupInfo.expiryTime.date)
|
||||||
|
.withHourOfDay(viewModelTime.hours)
|
||||||
|
.withMinuteOfHour(viewModelTime.minutes)
|
||||||
|
.toDate(), mGroupInfo.expiryTime.type)
|
||||||
|
expirationView.dateTime = mGroupInfo.expiryTime
|
||||||
|
}
|
||||||
|
|
||||||
|
mGroupEditViewModel.groupNamesNotAllowed.observe(this) { namesNotAllowed ->
|
||||||
|
this.mGroupNamesNotAllowed = namesNotAllowed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
|
mPopulateIconMethod = { imageView, icon ->
|
||||||
|
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||||
|
}
|
||||||
|
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
||||||
|
|
||||||
|
searchableContainerView.visibility = if (database?.allowCustomSearchableGroup() == true) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
if (database?.allowAutoType() == true) {
|
||||||
|
autoTypeContainerView.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
autoTypeContainerView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
|
||||||
|
tagsCompletionView.apply {
|
||||||
|
threshold = 1
|
||||||
|
setAdapter(tagsAdapter)
|
||||||
|
}
|
||||||
|
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val root = activity.layoutInflater.inflate(R.layout.fragment_group_edit, null)
|
val root = activity.layoutInflater.inflate(R.layout.fragment_group_edit, null)
|
||||||
nameTextLayoutView = root?.findViewById(R.id.group_edit_name_container)
|
iconButtonView = root.findViewById(R.id.group_edit_icon_button)
|
||||||
nameTextView = root?.findViewById(R.id.group_edit_name)
|
nameTextLayoutView = root.findViewById(R.id.group_edit_name_container)
|
||||||
iconButtonView = root?.findViewById(R.id.group_edit_icon_button)
|
nameTextView = root.findViewById(R.id.group_edit_name)
|
||||||
|
notesTextLayoutView = root.findViewById(R.id.group_edit_note_container)
|
||||||
|
notesTextView = root.findViewById(R.id.group_edit_note)
|
||||||
|
expirationView = root.findViewById(R.id.group_edit_expiration)
|
||||||
|
searchableContainerView = root.findViewById(R.id.group_edit_searchable_container)
|
||||||
|
searchableView = root.findViewById(R.id.group_edit_searchable)
|
||||||
|
autoTypeContainerView = root.findViewById(R.id.group_edit_auto_type_container)
|
||||||
|
autoTypeInheritedView = root.findViewById(R.id.group_edit_auto_type_inherited)
|
||||||
|
autoTypeSequenceView = root.findViewById(R.id.group_edit_auto_type_sequence)
|
||||||
|
tagsContainerView = root.findViewById(R.id.group_tags_label)
|
||||||
|
tagsCompletionView = root.findViewById(R.id.group_tags_completion_view)
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
// Retrieve the textColor to tint the icon
|
||||||
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
iconColor = ta.getColor(0, Color.WHITE)
|
mIconColor = ta.getColor(0, Color.WHITE)
|
||||||
ta.recycle()
|
ta.recycle()
|
||||||
|
|
||||||
// Init elements
|
|
||||||
mDatabase = Database.getInstance()
|
|
||||||
editGroupDialogAction = EditGroupDialogAction.NONE
|
|
||||||
nameGroup = ""
|
|
||||||
iconGroup = mDatabase?.iconFactory?.folderIcon
|
|
||||||
|
|
||||||
if (savedInstanceState != null
|
if (savedInstanceState != null
|
||||||
&& savedInstanceState.containsKey(KEY_ACTION_ID)
|
&& savedInstanceState.containsKey(KEY_ACTION_ID)
|
||||||
&& savedInstanceState.containsKey(KEY_NAME)
|
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
|
||||||
&& savedInstanceState.containsKey(KEY_ICON)) {
|
mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID))
|
||||||
editGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID))
|
mGroupInfo = savedInstanceState.getParcelableCompat(KEY_GROUP_INFO) ?: mGroupInfo
|
||||||
nameGroup = savedInstanceState.getString(KEY_NAME)
|
|
||||||
iconGroup = savedInstanceState.getParcelable(KEY_ICON)
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
if (containsKey(KEY_ACTION_ID))
|
if (containsKey(KEY_ACTION_ID))
|
||||||
editGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID))
|
mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID))
|
||||||
|
if (containsKey(KEY_GROUP_INFO)) {
|
||||||
if (containsKey(KEY_NAME) && containsKey(KEY_ICON)) {
|
mGroupInfo = getParcelableCompat(KEY_GROUP_INFO) ?: mGroupInfo
|
||||||
nameGroup = getString(KEY_NAME)
|
|
||||||
iconGroup = getParcelable(KEY_ICON)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// populate the name
|
// populate info in views
|
||||||
nameTextView?.text = nameGroup
|
populateInfoToViews(mGroupInfo)
|
||||||
// populate the icon
|
|
||||||
assignIconView()
|
iconButtonView.setOnClickListener { _ ->
|
||||||
|
mGroupEditViewModel.requestIconSelection(mGroupInfo.icon)
|
||||||
|
}
|
||||||
|
expirationView.setOnDateClickListener = { dateInstant ->
|
||||||
|
mGroupEditViewModel.requestDateTimeSelection(dateInstant)
|
||||||
|
}
|
||||||
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
builder.setView(root)
|
builder.setView(root)
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
editGroupListener?.cancelEditGroup(
|
// Do nothing
|
||||||
editGroupDialogAction,
|
|
||||||
nameTextView?.text?.toString(),
|
|
||||||
iconGroup)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
iconButtonView?.setOnClickListener { _ ->
|
|
||||||
IconPickerDialogFragment().show(parentFragmentManager, "IconPickerDialogFragment")
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.create()
|
return builder.create()
|
||||||
}
|
}
|
||||||
return super.onCreateDialog(savedInstanceState)
|
return super.onCreateDialog(savedInstanceState)
|
||||||
@@ -142,73 +210,116 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
// To prevent auto dismiss
|
// To prevent auto dismiss
|
||||||
val d = dialog as AlertDialog?
|
val alertDialog = dialog as AlertDialog?
|
||||||
if (d != null) {
|
if (alertDialog != null) {
|
||||||
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
|
val positiveButton = alertDialog.getButton(Dialog.BUTTON_POSITIVE) as Button
|
||||||
positiveButton.setOnClickListener {
|
positiveButton.setOnClickListener {
|
||||||
|
retrieveGroupInfoFromViews()
|
||||||
if (isValid()) {
|
if (isValid()) {
|
||||||
editGroupListener?.approveEditGroup(
|
when (mEditGroupDialogAction) {
|
||||||
editGroupDialogAction,
|
CREATION ->
|
||||||
nameTextView?.text?.toString(),
|
mGroupEditViewModel.approveGroupCreation(mGroupInfo)
|
||||||
iconGroup)
|
UPDATE ->
|
||||||
d.dismiss()
|
mGroupEditViewModel.approveGroupUpdate(mGroupInfo)
|
||||||
|
NONE -> {}
|
||||||
|
}
|
||||||
|
alertDialog.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assignIconView() {
|
private fun populateInfoToViews(groupInfo: GroupInfo) {
|
||||||
if (mDatabase?.drawFactory != null && iconGroup != null) {
|
mGroupEditViewModel.selectIcon(groupInfo.icon)
|
||||||
iconButtonView?.assignDatabaseIcon(mDatabase?.drawFactory!!, iconGroup!!, iconColor)
|
nameTextView.text = groupInfo.title
|
||||||
|
notesTextLayoutView.visibility = if (groupInfo.notes == null) View.GONE else View.VISIBLE
|
||||||
|
groupInfo.notes?.let {
|
||||||
|
notesTextView.text = it
|
||||||
|
}
|
||||||
|
expirationView.activation = groupInfo.expires
|
||||||
|
expirationView.dateTime = groupInfo.expiryTime
|
||||||
|
|
||||||
|
// Set searchable
|
||||||
|
searchableView.setValue(groupInfo.searchable)
|
||||||
|
// Set auto-type
|
||||||
|
autoTypeInheritedView.setValue(groupInfo.enableAutoType)
|
||||||
|
autoTypeSequenceView.text = groupInfo.defaultAutoTypeSequence
|
||||||
|
// Set Tags
|
||||||
|
groupInfo.tags.let { tags ->
|
||||||
|
tagsCompletionView.setText("")
|
||||||
|
for (i in 0 until tags.size()) {
|
||||||
|
tagsCompletionView.addObjectSync(tags.get(i))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun iconPicked(bundle: Bundle) {
|
private fun retrieveGroupInfoFromViews() {
|
||||||
iconGroup = IconPickerDialogFragment.getIconStandardFromBundle(bundle)
|
mGroupInfo.title = nameTextView.text.toString()
|
||||||
assignIconView()
|
// Only if there
|
||||||
|
val newNotes = notesTextView.text.toString()
|
||||||
|
if (newNotes.isNotEmpty()) {
|
||||||
|
mGroupInfo.notes = newNotes
|
||||||
|
}
|
||||||
|
mGroupInfo.expires = expirationView.activation
|
||||||
|
mGroupInfo.expiryTime = expirationView.dateTime
|
||||||
|
mGroupInfo.searchable = searchableView.getValue()
|
||||||
|
mGroupInfo.enableAutoType = autoTypeInheritedView.getValue()
|
||||||
|
mGroupInfo.defaultAutoTypeSequence = autoTypeSequenceView.text.toString()
|
||||||
|
mGroupInfo.tags = tagsCompletionView.getTags()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
outState.putInt(KEY_ACTION_ID, editGroupDialogAction!!.ordinal)
|
retrieveGroupInfoFromViews()
|
||||||
outState.putString(KEY_NAME, nameGroup)
|
outState.putInt(KEY_ACTION_ID, mEditGroupDialogAction.ordinal)
|
||||||
outState.putParcelable(KEY_ICON, iconGroup)
|
outState.putParcelable(KEY_GROUP_INFO, mGroupInfo)
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isValid(): Boolean {
|
private fun isValid(): Boolean {
|
||||||
if (nameTextView?.text?.toString()?.isNotEmpty() != true) {
|
val name = nameTextView.text.toString()
|
||||||
nameTextLayoutView?.error = getString(R.string.error_no_name)
|
val error = when {
|
||||||
return false
|
name.isEmpty() -> {
|
||||||
|
Error(true, R.string.error_no_name)
|
||||||
|
}
|
||||||
|
mGroupNamesNotAllowed == null -> {
|
||||||
|
Error(true, R.string.error_word_reserved)
|
||||||
|
}
|
||||||
|
mGroupNamesNotAllowed?.find { it.equals(name, ignoreCase = true) } != null -> {
|
||||||
|
Error(true, R.string.error_word_reserved)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Error(false, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true
|
error.messageId?.let { messageId ->
|
||||||
|
nameTextLayoutView.error = getString(messageId)
|
||||||
|
} ?: kotlin.run {
|
||||||
|
nameTextLayoutView.error = null
|
||||||
|
}
|
||||||
|
return !error.isError
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EditGroupListener {
|
data class Error(val isError: Boolean, val messageId: Int?)
|
||||||
fun approveEditGroup(action: EditGroupDialogAction?, name: String?, icon: IconImage?)
|
|
||||||
fun cancelEditGroup(action: EditGroupDialogAction?, name: String?, icon: IconImage?)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
|
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
|
||||||
|
private const val KEY_ACTION_ID = "KEY_ACTION_ID"
|
||||||
|
private const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
||||||
|
|
||||||
const val KEY_NAME = "KEY_NAME"
|
fun create(groupInfo: GroupInfo): GroupEditDialogFragment {
|
||||||
const val KEY_ICON = "KEY_ICON"
|
|
||||||
const val KEY_ACTION_ID = "KEY_ACTION_ID"
|
|
||||||
|
|
||||||
fun build(): GroupEditDialogFragment {
|
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
bundle.putInt(KEY_ACTION_ID, CREATION.ordinal)
|
bundle.putInt(KEY_ACTION_ID, CREATION.ordinal)
|
||||||
|
bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
|
||||||
val fragment = GroupEditDialogFragment()
|
val fragment = GroupEditDialogFragment()
|
||||||
fragment.arguments = bundle
|
fragment.arguments = bundle
|
||||||
return fragment
|
return fragment
|
||||||
}
|
}
|
||||||
|
|
||||||
fun build(group: Group): GroupEditDialogFragment {
|
fun update(groupInfo: GroupInfo): GroupEditDialogFragment {
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
bundle.putString(KEY_NAME, group.title)
|
|
||||||
bundle.putParcelable(KEY_ICON, group.icon)
|
|
||||||
bundle.putInt(KEY_ACTION_ID, UPDATE.ordinal)
|
bundle.putInt(KEY_ACTION_ID, UPDATE.ordinal)
|
||||||
|
bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
|
||||||
val fragment = GroupEditDialogFragment()
|
val fragment = GroupEditDialogFragment()
|
||||||
fragment.arguments = bundle
|
fragment.arguments = bundle
|
||||||
return fragment
|
return fragment
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 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.os.Bundle
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||||
|
|
||||||
|
class IconEditDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
|
private val mIconPickerViewModel: IconPickerViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
|
||||||
|
private lateinit var iconView: ImageView
|
||||||
|
private lateinit var nameTextLayoutView: TextInputLayout
|
||||||
|
private lateinit var nameTextView: TextView
|
||||||
|
|
||||||
|
private var mCustomIcon: IconImageCustom? = null
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
mPopulateIconMethod = { imageView, icon ->
|
||||||
|
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon)
|
||||||
|
}
|
||||||
|
mCustomIcon?.let { customIcon ->
|
||||||
|
populateViewsWithCustomIcon(customIcon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
val root = activity.layoutInflater.inflate(R.layout.fragment_icon_edit, null)
|
||||||
|
iconView = root.findViewById(R.id.icon_edit_image)
|
||||||
|
nameTextLayoutView = root.findViewById(R.id.icon_edit_name_container)
|
||||||
|
nameTextView = root.findViewById(R.id.icon_edit_name)
|
||||||
|
|
||||||
|
if (savedInstanceState != null
|
||||||
|
&& savedInstanceState.containsKey(KEY_CUSTOM_ICON_ID)) {
|
||||||
|
mCustomIcon = savedInstanceState.getParcelableCompat(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
|
||||||
|
} else {
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(KEY_CUSTOM_ICON_ID)) {
|
||||||
|
mCustomIcon = getParcelableCompat(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
builder.setView(root)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
retrieveIconInfoFromViews()
|
||||||
|
mCustomIcon?.let { customIcon ->
|
||||||
|
mIconPickerViewModel.updateCustomIcon(
|
||||||
|
IconPickerViewModel.IconCustomState(customIcon, false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
// Do nothing
|
||||||
|
mIconPickerViewModel.updateCustomIcon(
|
||||||
|
IconPickerViewModel.IconCustomState(null, false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateViewsWithCustomIcon(customIcon: IconImageCustom) {
|
||||||
|
mPopulateIconMethod?.invoke(iconView, customIcon.getIconImageToDraw())
|
||||||
|
nameTextView.text = customIcon.name
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun retrieveIconInfoFromViews() {
|
||||||
|
mCustomIcon?.name = nameTextView.text.toString()
|
||||||
|
mCustomIcon?.lastModificationTime = DateInstant()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
retrieveIconInfoFromViews()
|
||||||
|
outState.putParcelable(KEY_CUSTOM_ICON_ID, mCustomIcon)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val TAG_UPDATE_ICON = "TAG_UPDATE_ICON"
|
||||||
|
const val KEY_CUSTOM_ICON_ID = "KEY_CUSTOM_ICON_ID"
|
||||||
|
|
||||||
|
fun update(customIcon: IconImageCustom): IconEditDialogFragment {
|
||||||
|
val bundle = Bundle()
|
||||||
|
bundle.putParcelable(KEY_CUSTOM_ICON_ID, IconImageCustom(customIcon))
|
||||||
|
val fragment = IconEditDialogFragment()
|
||||||
|
fragment.arguments = bundle
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.res.ColorStateList
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.BaseAdapter
|
|
||||||
import android.widget.GridView
|
|
||||||
import android.widget.ImageView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.widget.ImageViewCompat
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
|
||||||
import com.kunzisoft.keepass.icons.IconPack
|
|
||||||
import com.kunzisoft.keepass.icons.IconPackChooser
|
|
||||||
|
|
||||||
|
|
||||||
class IconPickerDialogFragment : DialogFragment() {
|
|
||||||
|
|
||||||
private var iconPickerListener: IconPickerListener? = null
|
|
||||||
private var iconPack: IconPack? = null
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
try {
|
|
||||||
iconPickerListener = context as IconPickerListener
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
// The activity doesn't implement the interface, throw exception
|
|
||||||
throw ClassCastException(context.toString()
|
|
||||||
+ " must implement " + IconPickerListener::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
activity?.let { activity ->
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
|
||||||
|
|
||||||
iconPack = IconPackChooser.getSelectedIconPack(requireContext())
|
|
||||||
|
|
||||||
// Inflate and set the layout for the dialog
|
|
||||||
// Pass null as the parent view because its going in the dialog layout
|
|
||||||
val root = activity.layoutInflater.inflate(R.layout.fragment_icon_picker, null)
|
|
||||||
builder.setView(root)
|
|
||||||
|
|
||||||
val currIconGridView = root.findViewById<GridView>(R.id.IconGridView)
|
|
||||||
currIconGridView.adapter = ImageAdapter(activity)
|
|
||||||
|
|
||||||
currIconGridView.setOnItemClickListener { _, _, position, _ ->
|
|
||||||
val bundle = Bundle()
|
|
||||||
bundle.putParcelable(KEY_ICON_STANDARD, IconImageStandard(position))
|
|
||||||
iconPickerListener?.iconPicked(bundle)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() }
|
|
||||||
|
|
||||||
return builder.create()
|
|
||||||
}
|
|
||||||
return super.onCreateDialog(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ImageAdapter internal constructor(private val context: Context) : BaseAdapter() {
|
|
||||||
|
|
||||||
override fun getCount(): Int {
|
|
||||||
return iconPack?.numberOfIcons() ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItem(position: Int): Any? {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
|
||||||
val currentView: View = convertView
|
|
||||||
?: (context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)
|
|
||||||
.inflate(R.layout.item_icon, parent, false)
|
|
||||||
|
|
||||||
iconPack?.let { iconPack ->
|
|
||||||
val iconImageView = currentView.findViewById<ImageView>(R.id.icon_image)
|
|
||||||
iconImageView.setImageResource(iconPack.iconToResId(position))
|
|
||||||
|
|
||||||
// Assign color if icons are tintable
|
|
||||||
if (iconPack.tintable()) {
|
|
||||||
// Retrieve the textColor to tint the icon
|
|
||||||
val ta = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
|
||||||
ImageViewCompat.setImageTintList(iconImageView, ColorStateList.valueOf(ta.getColor(0, Color.BLACK)))
|
|
||||||
ta.recycle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentView
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IconPickerListener {
|
|
||||||
fun iconPicked(bundle: Bundle)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val KEY_ICON_STANDARD = "KEY_ICON_STANDARD"
|
|
||||||
|
|
||||||
fun getIconStandardFromBundle(bundle: Bundle): IconImageStandard? {
|
|
||||||
return bundle.getParcelable(KEY_ICON_STANDARD)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun launch(activity: AppCompatActivity) {
|
|
||||||
// Create an instance of the dialog fragment and show it
|
|
||||||
val dialog = IconPickerDialogFragment()
|
|
||||||
dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 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.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
import com.kunzisoft.keepass.view.MainCredentialView
|
||||||
|
|
||||||
|
class MainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
|
private var mainCredentialView: MainCredentialView? = null
|
||||||
|
|
||||||
|
private var mListener: AskMainCredentialDialogListener? = null
|
||||||
|
|
||||||
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
|
interface AskMainCredentialDialogListener {
|
||||||
|
fun onAskMainCredentialDialogPositiveClick(databaseUri: Uri?, mainCredential: MainCredential)
|
||||||
|
fun onAskMainCredentialDialogNegativeClick(databaseUri: Uri?, mainCredential: MainCredential)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttach(activity: Context) {
|
||||||
|
super.onAttach(activity)
|
||||||
|
try {
|
||||||
|
mListener = activity as AskMainCredentialDialogListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(activity.toString()
|
||||||
|
+ " must implement " + AskMainCredentialDialogListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
|
||||||
|
var databaseUri: Uri? = null
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(KEY_ASK_CREDENTIAL_URI))
|
||||||
|
databaseUri = getParcelableCompat(KEY_ASK_CREDENTIAL_URI)
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|
||||||
|
val root = activity.layoutInflater.inflate(R.layout.fragment_main_credential, null)
|
||||||
|
mainCredentialView = root.findViewById(R.id.main_credential_view)
|
||||||
|
databaseUri?.let {
|
||||||
|
root.findViewById<TextView>(R.id.title_database)?.text =
|
||||||
|
it.getDocumentFile(requireContext())?.name
|
||||||
|
}
|
||||||
|
builder.setView(root)
|
||||||
|
// Add action buttons
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
mListener?.onAskMainCredentialDialogPositiveClick(
|
||||||
|
databaseUri,
|
||||||
|
retrieveMainCredential()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
mListener?.onAskMainCredentialDialogNegativeClick(
|
||||||
|
databaseUri,
|
||||||
|
retrieveMainCredential()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
|
if (uri != null) {
|
||||||
|
mainCredentialView?.populateKeyFileView(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
|
||||||
|
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun retrieveMainCredential(): MainCredential {
|
||||||
|
return mainCredentialView?.getMainCredential() ?: MainCredential()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val KEY_ASK_CREDENTIAL_URI = "KEY_ASK_CREDENTIAL_URI"
|
||||||
|
const val TAG_ASK_MAIN_CREDENTIAL = "TAG_ASK_MAIN_CREDENTIAL"
|
||||||
|
|
||||||
|
fun getInstance(uri: Uri?): MainCredentialDialogFragment {
|
||||||
|
val fragment = MainCredentialDialogFragment()
|
||||||
|
val args = Bundle()
|
||||||
|
args.putParcelable(KEY_ASK_CREDENTIAL_URI, uri)
|
||||||
|
fragment.arguments = args
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,22 +19,49 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
import android.app.AlertDialog
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.DialogInterface
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
|
||||||
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?.getParcelableCompat(DATABASE_URI_KEY)
|
||||||
|
val mainCredential: MainCredential = savedInstanceState?.getParcelableCompat(MAIN_CREDENTIAL) ?: MainCredential()
|
||||||
|
|
||||||
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,
|
||||||
|
mainCredential
|
||||||
|
)
|
||||||
|
}
|
||||||
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 +69,26 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
|||||||
return super.onCreateDialog(savedInstanceState)
|
return super.onCreateDialog(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun onPasswordEncodingValidateListener(databaseUri: Uri?,
|
||||||
|
mainCredential: MainCredential)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
|
||||||
|
private const val MAIN_CREDENTIAL = "MAIN_CREDENTIAL"
|
||||||
|
|
||||||
|
fun getInstance(
|
||||||
|
databaseUri: Uri,
|
||||||
|
mainCredential: MainCredential
|
||||||
|
): SortDialogFragment {
|
||||||
|
val fragment = SortDialogFragment()
|
||||||
|
fragment.arguments = Bundle().apply {
|
||||||
|
putParcelable(DATABASE_URI_KEY, databaseUri)
|
||||||
|
putParcelable(MAIN_CREDENTIAL, mainCredential)
|
||||||
|
}
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
|
|||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.BuildConfig
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Dialog that asks the user to download the pro version or make a donation.
|
* Custom Dialog that asks the user to download the pro version or make a donation.
|
||||||
@@ -45,13 +45,16 @@ 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(requireContext(), R.string.app_pro_url)
|
activity.openUrl(
|
||||||
|
activity.getString(R.string.play_store_url,
|
||||||
|
activity.getString(R.string.keepro_app_id))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} 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(requireContext(), R.string.contribution_url)
|
activity.openUrl(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 com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Dialog to confirm big file to upload
|
||||||
|
*/
|
||||||
|
class ReplaceFileDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
|
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?.getParcelableCompat(KEY_FILE_URI),
|
||||||
|
arguments?.getParcelableCompat(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,374 @@
|
|||||||
|
/*
|
||||||
|
* 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.content.DialogInterface
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
|
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
|
||||||
|
import com.kunzisoft.keepass.password.PasswordEntropy
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
|
import com.kunzisoft.keepass.view.HardwareKeySelectionView
|
||||||
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
|
import com.kunzisoft.keepass.view.PassKeyView
|
||||||
|
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||||
|
|
||||||
|
class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
|
private var mMasterPassword: String? = null
|
||||||
|
private var mKeyFileUri: Uri? = null
|
||||||
|
private var mHardwareKey: HardwareKey? = null
|
||||||
|
|
||||||
|
private lateinit var rootView: View
|
||||||
|
|
||||||
|
private lateinit var passwordCheckBox: CompoundButton
|
||||||
|
private lateinit var passwordView: PassKeyView
|
||||||
|
private lateinit var passwordRepeatTextInputLayout: TextInputLayout
|
||||||
|
private lateinit var passwordRepeatView: TextView
|
||||||
|
|
||||||
|
private lateinit var keyFileCheckBox: CompoundButton
|
||||||
|
private lateinit var keyFileSelectionView: KeyFileSelectionView
|
||||||
|
|
||||||
|
private lateinit var hardwareKeyCheckBox: CompoundButton
|
||||||
|
private lateinit var hardwareKeySelectionView: HardwareKeySelectionView
|
||||||
|
|
||||||
|
private var mListener: AssignMainCredentialDialogListener? = null
|
||||||
|
|
||||||
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
private var mPasswordEntropyCalculator: PasswordEntropy? = null
|
||||||
|
|
||||||
|
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
|
||||||
|
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
||||||
|
private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null
|
||||||
|
|
||||||
|
private var mAllowNoMasterKey: Boolean = false
|
||||||
|
|
||||||
|
private val passwordTextWatcher = object : TextWatcher {
|
||||||
|
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||||
|
|
||||||
|
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||||
|
|
||||||
|
override fun afterTextChanged(editable: Editable) {
|
||||||
|
passwordCheckBox.isChecked = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AssignMainCredentialDialogListener {
|
||||||
|
fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential)
|
||||||
|
fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttach(activity: Context) {
|
||||||
|
super.onAttach(activity)
|
||||||
|
try {
|
||||||
|
mListener = activity as AssignMainCredentialDialogListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(activity.toString()
|
||||||
|
+ " must implement " + AssignMainCredentialDialogListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
mEmptyPasswordConfirmationDialog?.dismiss()
|
||||||
|
mEmptyPasswordConfirmationDialog = null
|
||||||
|
mNoKeyConfirmationDialog?.dismiss()
|
||||||
|
mNoKeyConfirmationDialog = null
|
||||||
|
mEmptyKeyFileConfirmationDialog?.dismiss()
|
||||||
|
mEmptyKeyFileConfirmationDialog = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Create the password entropy object
|
||||||
|
mPasswordEntropyCalculator = PasswordEntropy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(ALLOW_NO_MASTER_KEY_ARG))
|
||||||
|
mAllowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
val inflater = activity.layoutInflater
|
||||||
|
|
||||||
|
rootView = inflater.inflate(R.layout.fragment_set_main_credential, null)
|
||||||
|
builder.setView(rootView)
|
||||||
|
// Add action buttons
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
|
||||||
|
rootView.findViewById<View>(R.id.credentials_information)?.setOnClickListener {
|
||||||
|
activity.openUrl(R.string.credentials_explanation_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordCheckBox = rootView.findViewById(R.id.password_checkbox)
|
||||||
|
passwordView = rootView.findViewById(R.id.password_view)
|
||||||
|
passwordRepeatTextInputLayout = rootView.findViewById(R.id.password_repeat_input_layout)
|
||||||
|
passwordRepeatView = rootView.findViewById(R.id.password_confirmation)
|
||||||
|
passwordRepeatView.applyFontVisibility()
|
||||||
|
|
||||||
|
keyFileCheckBox = rootView.findViewById(R.id.keyfile_checkbox)
|
||||||
|
keyFileSelectionView = rootView.findViewById(R.id.keyfile_selection)
|
||||||
|
|
||||||
|
hardwareKeyCheckBox = rootView.findViewById(R.id.hardware_key_checkbox)
|
||||||
|
hardwareKeySelectionView = rootView.findViewById(R.id.hardware_key_selection)
|
||||||
|
|
||||||
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
|
uri?.let { pathUri ->
|
||||||
|
pathUri.getDocumentFile(requireContext())?.length()?.let { lengthFile ->
|
||||||
|
keyFileSelectionView.error = null
|
||||||
|
keyFileCheckBox.isChecked = true
|
||||||
|
keyFileSelectionView.uri = pathUri
|
||||||
|
if (lengthFile <= 0L) {
|
||||||
|
showEmptyKeyFileConfirmationDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keyFileSelectionView.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
|
|
||||||
|
hardwareKeySelectionView.selectionListener = { hardwareKey ->
|
||||||
|
hardwareKeyCheckBox.isChecked = true
|
||||||
|
hardwareKeySelectionView.error =
|
||||||
|
if (!HardwareKeyActivity.isHardwareKeyAvailable(requireActivity(), hardwareKey)) {
|
||||||
|
// show hardware driver dialog if required
|
||||||
|
getString(R.string.error_driver_required, hardwareKey.toString())
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val dialog = builder.create()
|
||||||
|
dialog.setOnShowListener { dialog1 ->
|
||||||
|
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
|
||||||
|
positiveButton.setOnClickListener {
|
||||||
|
|
||||||
|
mMasterPassword = ""
|
||||||
|
mKeyFileUri = null
|
||||||
|
mHardwareKey = null
|
||||||
|
|
||||||
|
approveMainCredential()
|
||||||
|
}
|
||||||
|
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
|
||||||
|
negativeButton.setOnClickListener {
|
||||||
|
mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun approveMainCredential() {
|
||||||
|
val errorPassword = verifyPassword()
|
||||||
|
val errorKeyFile = verifyKeyFile()
|
||||||
|
val errorHardwareKey = verifyHardwareKey()
|
||||||
|
// Check all to fill error
|
||||||
|
var error = errorPassword || errorKeyFile || errorHardwareKey
|
||||||
|
val hardwareKey = hardwareKeySelectionView.hardwareKey
|
||||||
|
if (!error
|
||||||
|
&& (!passwordCheckBox.isChecked)
|
||||||
|
&& (!keyFileCheckBox.isChecked)
|
||||||
|
&& (!hardwareKeyCheckBox.isChecked)
|
||||||
|
) {
|
||||||
|
error = true
|
||||||
|
if (mAllowNoMasterKey) {
|
||||||
|
// show no key dialog if required
|
||||||
|
showNoKeyConfirmationDialog()
|
||||||
|
} else {
|
||||||
|
passwordRepeatTextInputLayout.error =
|
||||||
|
getString(R.string.error_disallow_no_credentials)
|
||||||
|
}
|
||||||
|
} else if (!error
|
||||||
|
&& mMasterPassword.isNullOrEmpty()
|
||||||
|
&& !keyFileCheckBox.isChecked
|
||||||
|
&& !hardwareKeyCheckBox.isChecked
|
||||||
|
) {
|
||||||
|
// show empty password dialog if required
|
||||||
|
error = true
|
||||||
|
showEmptyPasswordConfirmationDialog()
|
||||||
|
} else if (!error
|
||||||
|
&& hardwareKey != null
|
||||||
|
&& !HardwareKeyActivity.isHardwareKeyAvailable(
|
||||||
|
requireActivity(), hardwareKey, false)
|
||||||
|
) {
|
||||||
|
// show hardware driver dialog if required
|
||||||
|
error = true
|
||||||
|
hardwareKeySelectionView.error =
|
||||||
|
getString(R.string.error_driver_required, hardwareKey.toString())
|
||||||
|
}
|
||||||
|
if (!error) {
|
||||||
|
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyPassword(): Boolean {
|
||||||
|
var error = false
|
||||||
|
passwordRepeatTextInputLayout.error = null
|
||||||
|
if (passwordCheckBox.isChecked) {
|
||||||
|
mMasterPassword = passwordView.passwordString
|
||||||
|
val confPassword = passwordRepeatView.text.toString()
|
||||||
|
|
||||||
|
// Verify that passwords match
|
||||||
|
if (mMasterPassword != confPassword) {
|
||||||
|
error = true
|
||||||
|
// Passwords do not match
|
||||||
|
passwordRepeatTextInputLayout.error = getString(R.string.error_pass_match)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyKeyFile(): Boolean {
|
||||||
|
var error = false
|
||||||
|
keyFileSelectionView.error = null
|
||||||
|
if (keyFileCheckBox.isChecked) {
|
||||||
|
keyFileSelectionView.uri?.let { uri ->
|
||||||
|
mKeyFileUri = uri
|
||||||
|
} ?: run {
|
||||||
|
error = true
|
||||||
|
keyFileSelectionView.error = getString(R.string.error_nokeyfile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyHardwareKey(): Boolean {
|
||||||
|
var error = false
|
||||||
|
hardwareKeySelectionView.error = null
|
||||||
|
if (hardwareKeyCheckBox.isChecked) {
|
||||||
|
hardwareKeySelectionView.hardwareKey?.let { hardwareKey ->
|
||||||
|
mHardwareKey = hardwareKey
|
||||||
|
} ?: run {
|
||||||
|
error = true
|
||||||
|
hardwareKeySelectionView.error = getString(R.string.error_no_hardware_key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun retrieveMainCredential(): MainCredential {
|
||||||
|
val masterPassword = if (passwordCheckBox.isChecked) mMasterPassword else null
|
||||||
|
val keyFileUri = if (keyFileCheckBox.isChecked) mKeyFileUri else null
|
||||||
|
val hardwareKey = if (hardwareKeyCheckBox.isChecked) mHardwareKey else null
|
||||||
|
return MainCredential(masterPassword, keyFileUri, hardwareKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// To check checkboxes if a text is present
|
||||||
|
passwordView.addTextChangedListener(passwordTextWatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
|
||||||
|
passwordView.removeTextChangedListener(passwordTextWatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showEmptyPasswordConfirmationDialog() {
|
||||||
|
activity?.let {
|
||||||
|
val builder = AlertDialog.Builder(it)
|
||||||
|
builder.setMessage(R.string.warning_empty_password)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||||
|
this@SetMainCredentialDialogFragment.dismiss()
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
mEmptyPasswordConfirmationDialog = builder.create()
|
||||||
|
mEmptyPasswordConfirmationDialog?.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showNoKeyConfirmationDialog() {
|
||||||
|
activity?.let {
|
||||||
|
val builder = AlertDialog.Builder(it)
|
||||||
|
builder.setMessage(R.string.warning_no_encryption_key)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||||
|
this@SetMainCredentialDialogFragment.dismiss()
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
|
||||||
|
|
||||||
|
fun getInstance(allowNoMasterKey: Boolean): SetMainCredentialDialogFragment {
|
||||||
|
val fragment = SetMainCredentialDialogFragment()
|
||||||
|
val args = Bundle()
|
||||||
|
args.putBoolean(ALLOW_NO_MASTER_KEY_ARG, allowNoMasterKey)
|
||||||
|
fragment.arguments = args
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,14 +28,10 @@ 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.widget.AdapterView
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.*
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.Spinner
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.BuildConfig
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.model.OtpModel
|
import com.kunzisoft.keepass.model.OtpModel
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
@@ -48,14 +44,18 @@ import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
|
|||||||
import com.kunzisoft.keepass.otp.OtpTokenType
|
import com.kunzisoft.keepass.otp.OtpTokenType
|
||||||
import com.kunzisoft.keepass.otp.OtpType
|
import com.kunzisoft.keepass.otp.OtpType
|
||||||
import com.kunzisoft.keepass.otp.TokenCalculator
|
import com.kunzisoft.keepass.otp.TokenCalculator
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class SetOTPDialogFragment : DialogFragment() {
|
class SetOTPDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
private var mCreateOTPElementListener: CreateOtpListener? = null
|
private var mCreateOTPElementListener: CreateOtpListener? = null
|
||||||
|
|
||||||
private var mOtpElement: OtpElement = OtpElement()
|
private var mOtpElement: OtpElement = OtpElement()
|
||||||
|
|
||||||
|
private var otpTypeMessage: TextView? = null
|
||||||
private var otpTypeSpinner: Spinner? = null
|
private var otpTypeSpinner: Spinner? = null
|
||||||
private var otpTokenTypeSpinner: Spinner? = null
|
private var otpTokenTypeSpinner: Spinner? = null
|
||||||
private var otpSecretContainer: TextInputLayout? = null
|
private var otpSecretContainer: TextInputLayout? = null
|
||||||
@@ -73,25 +73,35 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
private var totpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
private var totpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
||||||
private var hotpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
private var hotpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
||||||
private var otpAlgorithmAdapter: ArrayAdapter<TokenCalculator.HashAlgorithm>? = null
|
private var otpAlgorithmAdapter: ArrayAdapter<TokenCalculator.HashAlgorithm>? = null
|
||||||
|
private var mHotpTokenTypeArray: Array<OtpTokenType>? = null
|
||||||
|
private var mTotpTokenTypeArray: Array<OtpTokenType>? = null
|
||||||
|
|
||||||
private var mManualEvent = false
|
private var mManualEvent = false
|
||||||
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
|
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
|
||||||
if (!isFocus)
|
if (!isFocus)
|
||||||
mManualEvent = true
|
mManualEvent = true
|
||||||
|
else
|
||||||
|
resetAppTimeout()
|
||||||
}
|
}
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
private var mOnTouchListener = View.OnTouchListener { _, event ->
|
private var mOnTouchListener = View.OnTouchListener { _, event ->
|
||||||
when (event.action) {
|
when (event.action) {
|
||||||
MotionEvent.ACTION_DOWN -> {
|
MotionEvent.ACTION_DOWN -> {
|
||||||
mManualEvent = true
|
mManualEvent = true
|
||||||
|
resetAppTimeout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
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 overrideTimeoutTouchAndFocusEvents(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
@@ -106,20 +116,25 @@ 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 {
|
||||||
|
|
||||||
// Retrieve OTP model from instance state
|
// Retrieve OTP model from instance state
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
if (savedInstanceState.containsKey(KEY_OTP)) {
|
if (savedInstanceState.containsKey(KEY_OTP)) {
|
||||||
savedInstanceState.getParcelable<OtpModel>(KEY_OTP)?.let { otpModel ->
|
savedInstanceState.getParcelableCompat<OtpModel>(KEY_OTP)?.let { otpModel ->
|
||||||
mOtpElement = OtpElement(otpModel)
|
mOtpElement = OtpElement(otpModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
if (containsKey(KEY_OTP)) {
|
if (containsKey(KEY_OTP)) {
|
||||||
getParcelable<OtpModel?>(KEY_OTP)?.let { otpModel ->
|
getParcelableCompat<OtpModel>(KEY_OTP)?.let { otpModel ->
|
||||||
mOtpElement = OtpElement(otpModel)
|
mOtpElement = OtpElement(otpModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,6 +143,7 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val root = activity.layoutInflater.inflate(R.layout.fragment_set_otp, null) as ViewGroup?
|
val root = activity.layoutInflater.inflate(R.layout.fragment_set_otp, null) as ViewGroup?
|
||||||
|
otpTypeMessage = root?.findViewById(R.id.setup_otp_type_message)
|
||||||
otpTypeSpinner = root?.findViewById(R.id.setup_otp_type)
|
otpTypeSpinner = root?.findViewById(R.id.setup_otp_type)
|
||||||
otpTokenTypeSpinner = root?.findViewById(R.id.setup_otp_token_type)
|
otpTokenTypeSpinner = root?.findViewById(R.id.setup_otp_token_type)
|
||||||
otpSecretContainer = root?.findViewById(R.id.setup_otp_secret_label)
|
otpSecretContainer = root?.findViewById(R.id.setup_otp_secret_label)
|
||||||
@@ -152,26 +168,49 @@ 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()
|
||||||
otpTypeAdapter = ArrayAdapter<OtpType>(activity,
|
otpTypeAdapter = ArrayAdapter(activity,
|
||||||
android.R.layout.simple_spinner_item, otpTypeArray).apply {
|
android.R.layout.simple_spinner_item, otpTypeArray).apply {
|
||||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
}
|
}
|
||||||
otpTypeSpinner?.adapter = otpTypeAdapter
|
otpTypeSpinner?.adapter = otpTypeAdapter
|
||||||
|
|
||||||
// Otp Token type selection
|
// Otp Token type selection
|
||||||
val hotpTokenTypeArray = OtpTokenType.getHotpTokenTypeValues()
|
mHotpTokenTypeArray = OtpTokenType.getHotpTokenTypeValues()
|
||||||
hotpTokenTypeAdapter = ArrayAdapter(activity,
|
hotpTokenTypeAdapter = ArrayAdapter(activity,
|
||||||
android.R.layout.simple_spinner_item, hotpTokenTypeArray).apply {
|
android.R.layout.simple_spinner_item, mHotpTokenTypeArray!!).apply {
|
||||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
}
|
}
|
||||||
// Proprietary only on closed and full version
|
// Proprietary only on full version
|
||||||
val totpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
|
mTotpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
|
||||||
BuildConfig.CLOSED_STORE && BuildConfig.FULL_VERSION)
|
activity.isContributingUser()
|
||||||
|
)
|
||||||
totpTokenTypeAdapter = ArrayAdapter(activity,
|
totpTokenTypeAdapter = ArrayAdapter(activity,
|
||||||
android.R.layout.simple_spinner_item, totpTokenTypeArray).apply {
|
android.R.layout.simple_spinner_item, mTotpTokenTypeArray!!).apply {
|
||||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
}
|
}
|
||||||
otpTokenTypeAdapter = hotpTokenTypeAdapter
|
otpTokenTypeAdapter = hotpTokenTypeAdapter
|
||||||
@@ -179,7 +218,7 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
// OTP Algorithm
|
// OTP Algorithm
|
||||||
val otpAlgorithmArray = TokenCalculator.HashAlgorithm.values()
|
val otpAlgorithmArray = TokenCalculator.HashAlgorithm.values()
|
||||||
otpAlgorithmAdapter = ArrayAdapter<TokenCalculator.HashAlgorithm>(activity,
|
otpAlgorithmAdapter = ArrayAdapter(activity,
|
||||||
android.R.layout.simple_spinner_item, otpAlgorithmArray).apply {
|
android.R.layout.simple_spinner_item, otpAlgorithmArray).apply {
|
||||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
}
|
}
|
||||||
@@ -194,11 +233,17 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
builder.apply {
|
builder.apply {
|
||||||
setTitle(R.string.entry_setup_otp)
|
|
||||||
setView(root)
|
setView(root)
|
||||||
.setPositiveButton(android.R.string.ok) {_, _ -> }
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
resetAppTimeout()
|
||||||
}
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
resetAppTimeout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root?.findViewById<View>(R.id.otp_information)?.setOnClickListener {
|
||||||
|
activity.openUrl(R.string.otp_explanation_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.create()
|
return builder.create()
|
||||||
@@ -266,7 +311,7 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
override fun afterTextChanged(s: Editable?) {
|
override fun afterTextChanged(s: Editable?) {
|
||||||
s?.toString()?.let { userString ->
|
s?.toString()?.let { userString ->
|
||||||
try {
|
try {
|
||||||
mOtpElement.setBase32Secret(userString.toUpperCase(Locale.ENGLISH))
|
mOtpElement.setBase32Secret(userString.uppercase(Locale.ENGLISH))
|
||||||
otpSecretContainer?.error = null
|
otpSecretContainer?.error = null
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
|
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
|
||||||
@@ -344,35 +389,63 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun upgradeTokenType() {
|
private fun upgradeTokenType() {
|
||||||
|
val tokenType = mOtpElement.tokenType
|
||||||
when (mOtpElement.type) {
|
when (mOtpElement.type) {
|
||||||
OtpType.HOTP -> {
|
OtpType.HOTP -> {
|
||||||
otpPeriodContainer?.visibility = View.GONE
|
otpPeriodContainer?.visibility = View.GONE
|
||||||
otpCounterContainer?.visibility = View.VISIBLE
|
otpCounterContainer?.visibility = View.VISIBLE
|
||||||
otpTokenTypeSpinner?.adapter = hotpTokenTypeAdapter
|
otpTokenTypeSpinner?.adapter = hotpTokenTypeAdapter
|
||||||
otpTokenTypeSpinner?.setSelection(OtpTokenType
|
mHotpTokenTypeArray?.let { otpTokenTypeArray ->
|
||||||
.getHotpTokenTypeValues().indexOf(mOtpElement.tokenType))
|
defineOtpTokenTypeSpinner(otpTokenTypeArray, tokenType, OtpTokenType.RFC4226)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
OtpType.TOTP -> {
|
OtpType.TOTP -> {
|
||||||
otpPeriodContainer?.visibility = View.VISIBLE
|
otpPeriodContainer?.visibility = View.VISIBLE
|
||||||
otpCounterContainer?.visibility = View.GONE
|
otpCounterContainer?.visibility = View.GONE
|
||||||
otpTokenTypeSpinner?.adapter = totpTokenTypeAdapter
|
otpTokenTypeSpinner?.adapter = totpTokenTypeAdapter
|
||||||
otpTokenTypeSpinner?.setSelection(OtpTokenType
|
mTotpTokenTypeArray?.let { otpTokenTypeArray ->
|
||||||
.getTotpTokenTypeValues().indexOf(mOtpElement.tokenType))
|
defineOtpTokenTypeSpinner(otpTokenTypeArray, tokenType, OtpTokenType.RFC6238)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun defineOtpTokenTypeSpinner(otpTokenTypeArray: Array<OtpTokenType>,
|
||||||
|
tokenType: OtpTokenType,
|
||||||
|
defaultTokenType: OtpTokenType) {
|
||||||
|
val formTokenType = if (otpTokenTypeArray.contains(tokenType)) {
|
||||||
|
otpTypeMessage?.visibility = View.GONE
|
||||||
|
tokenType
|
||||||
|
} else {
|
||||||
|
otpTypeMessage?.visibility = View.VISIBLE
|
||||||
|
defaultTokenType
|
||||||
|
}
|
||||||
|
otpTokenTypeSpinner?.setSelection(otpTokenTypeArray.indexOf(formTokenType))
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -398,4 +471,4 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,16 +22,15 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.annotation.IdRes
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import android.widget.RadioGroup
|
import android.widget.RadioGroup
|
||||||
|
import androidx.annotation.IdRes
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||||
|
|
||||||
class SortDialogFragment : DialogFragment() {
|
class SortDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
private var mListener: SortSelectionListener? = null
|
private var mListener: SortSelectionListener? = null
|
||||||
|
|
||||||
@@ -54,6 +53,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)
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.activities.dialogs
|
|
||||||
|
|
||||||
import android.app.DatePickerDialog
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.app.TimePickerDialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.format.DateFormat
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
|
|
||||||
class TimePickerFragment : DialogFragment() {
|
|
||||||
|
|
||||||
private var defaultHour: Int = 0
|
|
||||||
private var defaultMinute: Int = 0
|
|
||||||
|
|
||||||
private var mListener: TimePickerDialog.OnTimeSetListener? = null
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
try {
|
|
||||||
mListener = context as TimePickerDialog.OnTimeSetListener
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
throw ClassCastException(context.toString()
|
|
||||||
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
// Create a new instance of DatePickerDialog and return it
|
|
||||||
return context?.let {
|
|
||||||
arguments?.apply {
|
|
||||||
if (containsKey(DEFAULT_HOUR_BUNDLE_KEY))
|
|
||||||
defaultHour = getInt(DEFAULT_HOUR_BUNDLE_KEY)
|
|
||||||
if (containsKey(DEFAULT_MINUTE_BUNDLE_KEY))
|
|
||||||
defaultMinute = getInt(DEFAULT_MINUTE_BUNDLE_KEY)
|
|
||||||
}
|
|
||||||
|
|
||||||
TimePickerDialog(it, mListener, defaultHour, defaultMinute, DateFormat.is24HourFormat(activity))
|
|
||||||
} ?: super.onCreateDialog(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val DEFAULT_HOUR_BUNDLE_KEY = "DEFAULT_HOUR_BUNDLE_KEY"
|
|
||||||
private const val DEFAULT_MINUTE_BUNDLE_KEY = "DEFAULT_MINUTE_BUNDLE_KEY"
|
|
||||||
|
|
||||||
fun getInstance(defaultHour: Int,
|
|
||||||
defaultMinute: Int): TimePickerFragment {
|
|
||||||
return TimePickerFragment().apply {
|
|
||||||
arguments = Bundle().apply {
|
|
||||||
putInt(DEFAULT_HOUR_BUNDLE_KEY, defaultHour)
|
|
||||||
putInt(DEFAULT_MINUTE_BUNDLE_KEY, defaultMinute)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,12 +22,12 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
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
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
class UnavailableFeatureDialogFragment : DialogFragment() {
|
class UnavailableFeatureDialogFragment : DialogFragment() {
|
||||||
@@ -90,12 +90,12 @@ class UnavailableFeatureDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (apiName.isEmpty()) {
|
if (apiName.isEmpty()) {
|
||||||
val mapper = arrayOf("ANDROID BASE", "ANDROID BASE 1.1", "CUPCAKE", "DONUT", "ECLAIR", "ECLAIR_0_1", "ECLAIR_MR1", "FROYO", "GINGERBREAD", "GINGERBREAD_MR1", "HONEYCOMB", "HONEYCOMB_MR1", "HONEYCOMB_MR2", "ICE_CREAM_SANDWICH", "ICE_CREAM_SANDWICH_MR1", "JELLY_BEAN", "JELLY_BEAN", "JELLY_BEAN", "KITKAT", "KITKAT", "LOLLIPOOP", "LOLLIPOOP_MR1", "MARSHMALLOW", "NOUGAT", "NOUGAT", "OREO", "OREO")
|
val mapper = arrayOf("ANDROID BASE", "ANDROID BASE 1.1", "CUPCAKE", "DONUT", "ECLAIR", "ECLAIR_0_1", "ECLAIR_MR1", "FROYO", "GINGERBREAD", "GINGERBREAD_MR1", "HONEYCOMB", "HONEYCOMB_MR1", "HONEYCOMB_MR2", "ICE_CREAM_SANDWICH", "ICE_CREAM_SANDWICH_MR1", "JELLY_BEAN", "JELLY_BEAN", "JELLY_BEAN", "KITKAT", "KITKAT", "LOLLIPOOP", "LOLLIPOOP_MR1", "MARSHMALLOW", "NOUGAT", "NOUGAT", "OREO", "OREO", "PIE", "", "")
|
||||||
val index = apiNumber - 1
|
val index = apiNumber - 1
|
||||||
apiName = if (index < mapper.size) mapper[index] else "UNKNOWN_VERSION"
|
apiName = if (index < mapper.size) mapper[index] else "UNKNOWN_VERSION"
|
||||||
}
|
}
|
||||||
if (version.isEmpty()) {
|
if (version.isEmpty()) {
|
||||||
val versions = arrayOf("1.0", "1.1", "1.5", "1.6", "2.0", "2.0.1", "2.1", "2.2.X", "2.3", "2.3.3", "3.0", "3.1", "3.2.0", "4.0.1", "4.0.3", "4.1.0", "4.2.0", "4.3.0", "4.4", "4.4", "5.0", "5.1", "6.0", "7.0", "7.1", "8.0.0", "8.1.0")
|
val versions = arrayOf("1.0", "1.1", "1.5", "1.6", "2.0", "2.0.1", "2.1", "2.2.X", "2.3", "2.3.3", "3.0", "3.1", "3.2.0", "4.0.1", "4.0.3", "4.1.0", "4.2.0", "4.3.0", "4.4", "4.4", "5.0", "5.1", "6.0", "7.0", "7.1", "8.0.0", "8.1.0", "9", "10", "11")
|
||||||
val index = apiNumber - 1
|
val index = apiNumber - 1
|
||||||
version = if (index < versions.size) versions[index] else "UNKNOWN_VERSION"
|
version = if (index < versions.size) versions[index] else "UNKNOWN_VERSION"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,8 @@ import android.text.SpannableStringBuilder
|
|||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.BuildConfig
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Dialog that asks the user to download the pro version or make a donation.
|
* Custom Dialog that asks the user to download the pro version or make a donation.
|
||||||
@@ -40,31 +39,22 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
|
|||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|
||||||
val stringBuilder = SpannableStringBuilder()
|
val stringBuilder = SpannableStringBuilder()
|
||||||
if (BuildConfig.CLOSED_STORE) {
|
/*
|
||||||
if (BuildConfig.FULL_VERSION) {
|
if (activity.isContributingUser()) {
|
||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_thanks), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_thanks), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_rose), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
.append(HtmlCompat.fromHtml(getString(R.string.html_rose), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_work_hard), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
|
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_work_hard), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
|
||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_upgrade), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_upgrade), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
||||||
builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() }
|
builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() }
|
||||||
} else {
|
|
||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\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))
|
|
||||||
builder.setPositiveButton(R.string.download) { _, _ ->
|
|
||||||
UriUtil.gotoUrl(requireContext(), R.string.app_pro_url)
|
|
||||||
}
|
|
||||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
|
*/
|
||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||||
.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(requireContext(), R.string.contribution_url)
|
context?.openUrl(R.string.contribution_url)
|
||||||
}
|
}
|
||||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
//}
|
||||||
}
|
|
||||||
builder.setMessage(stringBuilder)
|
builder.setMessage(stringBuilder)
|
||||||
// Create the AlertDialog object and return it
|
// Create the AlertDialog object and return it
|
||||||
return builder.create()
|
return builder.create()
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
|
||||||
|
abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
|
||||||
|
|
||||||
|
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||||
|
protected var mDatabase: ContextualDatabase? = null
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
|
||||||
|
if (mDatabase == null || mDatabase != database) {
|
||||||
|
this.mDatabase = database
|
||||||
|
onDatabaseRetrieved(database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) { result ->
|
||||||
|
onDatabaseActionFinished(result.database, result.actionTask, result.result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) {
|
||||||
|
context?.let {
|
||||||
|
view?.resetAppTimeoutWhenViewTouchedOrFocused(it, mDatabase?.loaded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseActionFinished(
|
||||||
|
database: ContextualDatabase,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
// Can be overridden by a subclass
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun buildNewBinaryAttachment(): BinaryData? {
|
||||||
|
return mDatabase?.buildNewBinaryAttachment()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,409 @@
|
|||||||
|
/*
|
||||||
|
* 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.fragments
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
||||||
|
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||||
|
import com.kunzisoft.keepass.adapters.TagsProposalAdapter
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.template.Template
|
||||||
|
import com.kunzisoft.keepass.model.AttachmentState
|
||||||
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableList
|
||||||
|
import com.kunzisoft.keepass.utils.putParcelableList
|
||||||
|
import com.kunzisoft.keepass.view.TagsCompletionView
|
||||||
|
import com.kunzisoft.keepass.view.TemplateEditView
|
||||||
|
import com.kunzisoft.keepass.view.collapse
|
||||||
|
import com.kunzisoft.keepass.view.expand
|
||||||
|
import com.kunzisoft.keepass.view.showByFading
|
||||||
|
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
||||||
|
import com.tokenautocomplete.FilteredArrayAdapter
|
||||||
|
|
||||||
|
|
||||||
|
class EntryEditFragment: DatabaseFragment() {
|
||||||
|
|
||||||
|
private val mEntryEditViewModel: EntryEditViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private lateinit var rootView: View
|
||||||
|
private lateinit var templateView: TemplateEditView
|
||||||
|
private lateinit var attachmentsContainerView: ViewGroup
|
||||||
|
private lateinit var attachmentsListView: RecyclerView
|
||||||
|
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
|
||||||
|
private lateinit var tagsContainerView: TextInputLayout
|
||||||
|
private lateinit var tagsCompletionView: TagsCompletionView
|
||||||
|
private var tagsAdapter: FilteredArrayAdapter<String>? = null
|
||||||
|
|
||||||
|
private var mTemplate: Template? = null
|
||||||
|
private var mAllowMultipleAttachments: Boolean = false
|
||||||
|
|
||||||
|
private var mIconColor: Int = 0
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?): View? {
|
||||||
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
|
// Retrieve the textColor to tint the icon
|
||||||
|
val taIconColor = context?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
|
mIconColor = taIconColor?.getColor(0, Color.BLACK) ?: Color.BLACK
|
||||||
|
taIconColor?.recycle()
|
||||||
|
|
||||||
|
return inflater.inflate(R.layout.fragment_entry_edit, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View,
|
||||||
|
savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
rootView = view
|
||||||
|
// Hide only the first time
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
view.isVisible = false
|
||||||
|
}
|
||||||
|
templateView = view.findViewById(R.id.template_view)
|
||||||
|
attachmentsContainerView = view.findViewById(R.id.entry_attachments_container)
|
||||||
|
attachmentsListView = view.findViewById(R.id.entry_attachments_list)
|
||||||
|
tagsContainerView = view.findViewById(R.id.entry_tags_label)
|
||||||
|
tagsCompletionView = view.findViewById(R.id.entry_tags_completion_view)
|
||||||
|
|
||||||
|
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
|
||||||
|
attachmentsListView.apply {
|
||||||
|
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||||
|
adapter = attachmentsAdapter
|
||||||
|
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
|
}
|
||||||
|
|
||||||
|
templateView.apply {
|
||||||
|
setOnIconClickListener {
|
||||||
|
mEntryEditViewModel.requestIconSelection(templateView.getIcon())
|
||||||
|
}
|
||||||
|
setOnBackgroundColorClickListener {
|
||||||
|
mEntryEditViewModel.requestBackgroundColorSelection(templateView.getBackgroundColor())
|
||||||
|
}
|
||||||
|
setOnForegroundColorClickListener {
|
||||||
|
mEntryEditViewModel.requestForegroundColorSelection(templateView.getForegroundColor())
|
||||||
|
}
|
||||||
|
setOnCustomEditionActionClickListener { field ->
|
||||||
|
mEntryEditViewModel.requestCustomFieldEdition(field)
|
||||||
|
}
|
||||||
|
setOnPasswordGenerationActionClickListener { field ->
|
||||||
|
mEntryEditViewModel.requestPasswordSelection(field)
|
||||||
|
}
|
||||||
|
setOnDateInstantClickListener { dateInstant ->
|
||||||
|
mEntryEditViewModel.requestDateTimeSelection(dateInstant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
val attachments: List<Attachment> =
|
||||||
|
savedInstanceState.getParcelableList(ATTACHMENTS_TAG) ?: listOf()
|
||||||
|
setAttachments(attachments)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onTemplateChanged.observe(viewLifecycleOwner) { template ->
|
||||||
|
this.mTemplate = template
|
||||||
|
templateView.setTemplate(template)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.templatesEntry.observe(viewLifecycleOwner) { templateEntry ->
|
||||||
|
if (templateEntry != null) {
|
||||||
|
val selectedTemplate = if (mTemplate != null)
|
||||||
|
mTemplate
|
||||||
|
else
|
||||||
|
templateEntry.defaultTemplate
|
||||||
|
templateView.setTemplate(selectedTemplate)
|
||||||
|
// Load entry info only the first time to keep change locally
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
assignEntryInfo(templateEntry.entryInfo)
|
||||||
|
}
|
||||||
|
// To prevent flickering
|
||||||
|
rootView.showByFading()
|
||||||
|
// Apply timeout reset
|
||||||
|
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.requestEntryInfoUpdate.observe(viewLifecycleOwner) {
|
||||||
|
val entryInfo = retrieveEntryInfo()
|
||||||
|
mEntryEditViewModel.saveEntryInfo(it.database, it.entry, it.parent, entryInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onIconSelected.observe(viewLifecycleOwner) { iconImage ->
|
||||||
|
templateView.setIcon(iconImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onBackgroundColorSelected.observe(viewLifecycleOwner) { color ->
|
||||||
|
templateView.setBackgroundColor(color)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onForegroundColorSelected.observe(viewLifecycleOwner) { color ->
|
||||||
|
templateView.setForegroundColor(color)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onPasswordSelected.observe(viewLifecycleOwner) { passwordField ->
|
||||||
|
templateView.setPasswordField(passwordField)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onDateSelected.observe(viewLifecycleOwner) { viewModelDate ->
|
||||||
|
// Save the date
|
||||||
|
templateView.setCurrentDateTimeValue(viewModelDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onTimeSelected.observe(viewLifecycleOwner) { viewModelTime ->
|
||||||
|
// Save the time
|
||||||
|
templateView.setCurrentTimeValue(viewModelTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onCustomFieldEdited.observe(viewLifecycleOwner) { fieldAction ->
|
||||||
|
val oldField = fieldAction.oldField
|
||||||
|
val newField = fieldAction.newField
|
||||||
|
// Field to add
|
||||||
|
if (oldField == null) {
|
||||||
|
newField?.let {
|
||||||
|
if (!templateView.putCustomField(it)) {
|
||||||
|
mEntryEditViewModel.showCustomFieldEditionError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Field to replace
|
||||||
|
oldField?.let {
|
||||||
|
newField?.let {
|
||||||
|
if (!templateView.replaceCustomField(oldField, newField)) {
|
||||||
|
mEntryEditViewModel.showCustomFieldEditionError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Field to remove
|
||||||
|
if (newField == null) {
|
||||||
|
oldField?.let {
|
||||||
|
templateView.removeCustomField(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.requestSetupOtp.observe(viewLifecycleOwner) {
|
||||||
|
// Retrieve the current otpElement if exists
|
||||||
|
// and open the dialog to set up the OTP
|
||||||
|
SetOTPDialogFragment.build(templateView.getEntryInfo().otpModel)
|
||||||
|
.show(parentFragmentManager, "addOTPDialog")
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onOtpCreated.observe(viewLifecycleOwner) {
|
||||||
|
// Update the otp field with otpauth:// url
|
||||||
|
templateView.putOtpElement(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onBuildNewAttachment.observe(viewLifecycleOwner) {
|
||||||
|
val attachmentToUploadUri = it.attachmentToUploadUri
|
||||||
|
val fileName = it.fileName
|
||||||
|
|
||||||
|
buildNewBinaryAttachment()?.let { binaryAttachment ->
|
||||||
|
val entryAttachment = Attachment(fileName, binaryAttachment)
|
||||||
|
// Ask to replace the current attachment
|
||||||
|
if ((!mAllowMultipleAttachments
|
||||||
|
&& containsAttachment()) ||
|
||||||
|
containsAttachment(EntryAttachmentState(entryAttachment, StreamDirection.UPLOAD))) {
|
||||||
|
ReplaceFileDialogFragment.build(attachmentToUploadUri, entryAttachment)
|
||||||
|
.show(parentFragmentManager, "replacementFileFragment")
|
||||||
|
} else {
|
||||||
|
mEntryEditViewModel.startUploadAttachment(attachmentToUploadUri, entryAttachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onAttachmentAction.observe(viewLifecycleOwner) { entryAttachmentState ->
|
||||||
|
when (entryAttachmentState?.downloadState) {
|
||||||
|
AttachmentState.START -> {
|
||||||
|
putAttachment(entryAttachmentState)
|
||||||
|
getAttachmentViewPosition(entryAttachmentState) { attachment, position ->
|
||||||
|
mEntryEditViewModel.binaryPreviewLoaded(attachment, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AttachmentState.IN_PROGRESS -> {
|
||||||
|
putAttachment(entryAttachmentState)
|
||||||
|
}
|
||||||
|
AttachmentState.COMPLETE -> {
|
||||||
|
putAttachment(entryAttachmentState) { entryAttachment ->
|
||||||
|
getAttachmentViewPosition(entryAttachment) { attachment, position ->
|
||||||
|
mEntryEditViewModel.binaryPreviewLoaded(attachment, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mEntryEditViewModel.onAttachmentAction(null)
|
||||||
|
}
|
||||||
|
AttachmentState.CANCELED,
|
||||||
|
AttachmentState.ERROR -> {
|
||||||
|
removeAttachment(entryAttachmentState)
|
||||||
|
mEntryEditViewModel.onAttachmentAction(null)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
|
|
||||||
|
templateView.populateIconMethod = { imageView, icon ->
|
||||||
|
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
mAllowMultipleAttachments = database?.allowMultipleAttachments == true
|
||||||
|
|
||||||
|
attachmentsAdapter?.database = database
|
||||||
|
attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize ->
|
||||||
|
if (previousSize > 0 && newSize == 0) {
|
||||||
|
attachmentsContainerView.collapse(true)
|
||||||
|
} else if (previousSize == 0 && newSize == 1) {
|
||||||
|
attachmentsContainerView.expand(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
|
||||||
|
tagsCompletionView.apply {
|
||||||
|
threshold = 1
|
||||||
|
setAdapter(tagsAdapter)
|
||||||
|
}
|
||||||
|
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assignEntryInfo(entryInfo: EntryInfo?) {
|
||||||
|
// Populate entry views
|
||||||
|
templateView.setEntryInfo(entryInfo)
|
||||||
|
|
||||||
|
// Set Tags
|
||||||
|
entryInfo?.tags?.let { tags ->
|
||||||
|
tagsCompletionView.setText("")
|
||||||
|
for (i in 0 until tags.size()) {
|
||||||
|
tagsCompletionView.addObjectSync(tags.get(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manage attachments
|
||||||
|
setAttachments(entryInfo?.attachments ?: listOf())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun retrieveEntryInfo(): EntryInfo {
|
||||||
|
val entryInfo = templateView.getEntryInfo()
|
||||||
|
entryInfo.tags = tagsCompletionView.getTags()
|
||||||
|
entryInfo.attachments = getAttachments().toMutableList()
|
||||||
|
return entryInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------
|
||||||
|
* Attachments
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
private fun getAttachments(): List<Attachment> {
|
||||||
|
return attachmentsAdapter?.itemsList?.map { it.attachment } ?: listOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAttachments(attachments: List<Attachment>) {
|
||||||
|
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
attachmentsAdapter?.assignItems(attachments.map {
|
||||||
|
EntryAttachmentState(it, StreamDirection.UPLOAD)
|
||||||
|
})
|
||||||
|
attachmentsAdapter?.onDeleteButtonClickListener = { item ->
|
||||||
|
val attachment = item.attachment
|
||||||
|
removeAttachment(EntryAttachmentState(attachment, StreamDirection.DOWNLOAD))
|
||||||
|
mEntryEditViewModel.deleteAttachment(attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun containsAttachment(): Boolean {
|
||||||
|
return attachmentsAdapter?.isEmpty() != true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun containsAttachment(attachment: EntryAttachmentState): Boolean {
|
||||||
|
return attachmentsAdapter?.contains(attachment) ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun putAttachment(attachment: EntryAttachmentState,
|
||||||
|
onPreviewLoaded: ((attachment: EntryAttachmentState) -> Unit)? = null) {
|
||||||
|
// When only one attachment is allowed
|
||||||
|
if (!mAllowMultipleAttachments
|
||||||
|
&& attachment.downloadState == AttachmentState.START) {
|
||||||
|
attachmentsAdapter?.clear()
|
||||||
|
}
|
||||||
|
attachmentsContainerView.visibility = View.VISIBLE
|
||||||
|
attachmentsAdapter?.putItem(attachment)
|
||||||
|
attachmentsAdapter?.onBinaryPreviewLoaded = {
|
||||||
|
onPreviewLoaded?.invoke(attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeAttachment(attachment: EntryAttachmentState) {
|
||||||
|
attachmentsAdapter?.removeItem(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAttachmentViewPosition(attachment: EntryAttachmentState,
|
||||||
|
position: (attachment: EntryAttachmentState, Float) -> Unit) {
|
||||||
|
attachmentsListView.postDelayed({
|
||||||
|
attachmentsAdapter?.indexOf(attachment)?.let { index ->
|
||||||
|
position.invoke(attachment,
|
||||||
|
attachmentsContainerView.y
|
||||||
|
+ attachmentsListView.y
|
||||||
|
+ (attachmentsListView.getChildAt(index)?.y
|
||||||
|
?: 0F)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putParcelableList(ATTACHMENTS_TAG, getAttachments())
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------
|
||||||
|
* Education
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun getActionImageView(): View? {
|
||||||
|
return templateView.getActionImageView()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchGeneratePasswordEductionAction() {
|
||||||
|
mEntryEditViewModel.requestPasswordSelection(templateView.getPasswordField())
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = EntryEditFragment::class.java.name
|
||||||
|
|
||||||
|
private const val ATTACHMENTS_TAG = "ATTACHMENTS_TAG"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
|
import com.kunzisoft.keepass.database.helper.getLocalizedName
|
||||||
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
|
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
||||||
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
|
import com.kunzisoft.keepass.view.TemplateView
|
||||||
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
|
import com.kunzisoft.keepass.view.showByFading
|
||||||
|
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||||
|
|
||||||
|
class EntryFragment: DatabaseFragment() {
|
||||||
|
|
||||||
|
private lateinit var rootView: View
|
||||||
|
private lateinit var mainSection: View
|
||||||
|
private lateinit var advancedSection: View
|
||||||
|
|
||||||
|
private lateinit var templateView: TemplateView
|
||||||
|
|
||||||
|
private lateinit var creationDateView: TextView
|
||||||
|
private lateinit var modificationDateView: TextView
|
||||||
|
|
||||||
|
private lateinit var attachmentsContainerView: View
|
||||||
|
private lateinit var attachmentsListView: RecyclerView
|
||||||
|
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
|
||||||
|
|
||||||
|
private lateinit var customDataView: TextView
|
||||||
|
|
||||||
|
private lateinit var uuidContainerView: View
|
||||||
|
private lateinit var uuidReferenceView: TextView
|
||||||
|
|
||||||
|
private var mClipboardHelper: ClipboardHelper? = null
|
||||||
|
|
||||||
|
private val mEntryViewModel: EntryViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?): View? {
|
||||||
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
|
return inflater.inflate(R.layout.fragment_entry, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View,
|
||||||
|
savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
context?.let { context ->
|
||||||
|
mClipboardHelper = ClipboardHelper(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootView = view
|
||||||
|
// Hide only the first time
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
view.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
mainSection = view.findViewById(R.id.entry_section_main)
|
||||||
|
advancedSection = view.findViewById(R.id.entry_section_advanced)
|
||||||
|
|
||||||
|
templateView = view.findViewById(R.id.entry_template)
|
||||||
|
loadTemplateSettings()
|
||||||
|
|
||||||
|
attachmentsContainerView = view.findViewById(R.id.entry_attachments_container)
|
||||||
|
attachmentsListView = view.findViewById(R.id.entry_attachments_list)
|
||||||
|
attachmentsListView.apply {
|
||||||
|
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||||
|
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
|
}
|
||||||
|
|
||||||
|
creationDateView = view.findViewById(R.id.entry_created)
|
||||||
|
modificationDateView = view.findViewById(R.id.entry_modified)
|
||||||
|
|
||||||
|
// TODO Custom data
|
||||||
|
// customDataView = view.findViewById(R.id.entry_custom_data)
|
||||||
|
|
||||||
|
uuidContainerView = view.findViewById(R.id.entry_UUID_container)
|
||||||
|
uuidContainerView.apply {
|
||||||
|
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
uuidReferenceView = view.findViewById(R.id.entry_UUID_reference)
|
||||||
|
|
||||||
|
mEntryViewModel.entryInfoHistory.observe(viewLifecycleOwner) { entryInfoHistory ->
|
||||||
|
if (entryInfoHistory != null) {
|
||||||
|
templateView.setTemplate(entryInfoHistory.template)
|
||||||
|
assignEntryInfo(entryInfoHistory.entryInfo)
|
||||||
|
// Smooth appearing
|
||||||
|
rootView.showByFading()
|
||||||
|
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryViewModel.onAttachmentAction.observe(viewLifecycleOwner) { entryAttachmentState ->
|
||||||
|
entryAttachmentState?.let {
|
||||||
|
if (it.streamDirection != StreamDirection.UPLOAD) {
|
||||||
|
putAttachment(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryViewModel.sectionSelected.observe(viewLifecycleOwner) { entrySection ->
|
||||||
|
when (entrySection ?: EntryViewModel.EntrySection.MAIN) {
|
||||||
|
EntryViewModel.EntrySection.MAIN -> {
|
||||||
|
mainSection.showByFading()
|
||||||
|
advancedSection.hideByFading()
|
||||||
|
}
|
||||||
|
EntryViewModel.EntrySection.ADVANCED -> {
|
||||||
|
mainSection.hideByFading()
|
||||||
|
advancedSection.showByFading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
|
context?.let { context ->
|
||||||
|
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
|
||||||
|
attachmentsAdapter?.database = database
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentsListView.adapter = attachmentsAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadTemplateSettings() {
|
||||||
|
context?.let { context ->
|
||||||
|
templateView.setFirstTimeAskAllowCopyProtectedFields(PreferencesUtil.isFirstTimeAskAllowCopyProtectedFields(context))
|
||||||
|
templateView.setAllowCopyProtectedFields(PreferencesUtil.allowCopyProtectedFields(context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assignEntryInfo(entryInfo: EntryInfo?) {
|
||||||
|
// Set copy buttons
|
||||||
|
templateView.apply {
|
||||||
|
setOnAskCopySafeClickListener {
|
||||||
|
showClipboardDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnCopyActionClickListener { field ->
|
||||||
|
mClipboardHelper?.timeoutCopyToClipboard(
|
||||||
|
TemplateField.getLocalizedName(context, field.name),
|
||||||
|
field.protectedValue.stringValue,
|
||||||
|
field.protectedValue.isProtected
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate entry views
|
||||||
|
templateView.setEntryInfo(entryInfo)
|
||||||
|
|
||||||
|
// OTP timer updated
|
||||||
|
templateView.setOnOtpElementUpdated { otpElementUpdated ->
|
||||||
|
mEntryViewModel.onOtpElementUpdated(otpElementUpdated)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manage attachments
|
||||||
|
assignAttachments(entryInfo?.attachments ?: listOf())
|
||||||
|
|
||||||
|
// Assign dates
|
||||||
|
creationDateView.text = entryInfo?.creationTime?.getDateTimeString(resources)
|
||||||
|
modificationDateView.text = entryInfo?.lastModificationTime?.getDateTimeString(resources)
|
||||||
|
|
||||||
|
// TODO Custom data
|
||||||
|
// customDataView.text = entryInfo?.customData?.toString()
|
||||||
|
|
||||||
|
// Assign special data
|
||||||
|
uuidReferenceView.text = UuidUtil.toHexString(entryInfo?.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showClipboardDialog() {
|
||||||
|
context?.let {
|
||||||
|
AlertDialog.Builder(it)
|
||||||
|
.setMessage(
|
||||||
|
getString(R.string.allow_copy_password_warning) +
|
||||||
|
"\n\n" +
|
||||||
|
getString(R.string.clipboard_warning)
|
||||||
|
)
|
||||||
|
.create().apply {
|
||||||
|
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) { dialog, _ ->
|
||||||
|
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(context, true)
|
||||||
|
finishDialog(dialog)
|
||||||
|
}
|
||||||
|
setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ ->
|
||||||
|
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(context, false)
|
||||||
|
finishDialog(dialog)
|
||||||
|
}
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun finishDialog(dialog: DialogInterface) {
|
||||||
|
dialog.dismiss()
|
||||||
|
loadTemplateSettings()
|
||||||
|
templateView.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------
|
||||||
|
* Attachments
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
private fun assignAttachments(attachments: List<Attachment>) {
|
||||||
|
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
attachmentsAdapter?.assignItems(attachments.map {
|
||||||
|
EntryAttachmentState(it, StreamDirection.DOWNLOAD)
|
||||||
|
})
|
||||||
|
attachmentsAdapter?.onItemClickListener = { item ->
|
||||||
|
mEntryViewModel.onAttachmentSelected(item.attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putAttachment(attachmentToDownload: EntryAttachmentState) {
|
||||||
|
attachmentsAdapter?.putItem(attachmentToDownload)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------
|
||||||
|
* Education
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun firstEntryFieldCopyView(): View? {
|
||||||
|
return try {
|
||||||
|
templateView.getActionImageView()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchEntryCopyEducationAction() {
|
||||||
|
val appNameString = getString(R.string.app_name)
|
||||||
|
mClipboardHelper?.timeoutCopyToClipboard(appNameString, appNameString)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun getInstance(): EntryFragment {
|
||||||
|
return EntryFragment().apply {
|
||||||
|
arguments = Bundle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||