mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
819 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
798c95d8a8 | ||
|
|
ef77c2acfb | ||
|
|
11a98267a2 | ||
|
|
b2aa1155d0 | ||
|
|
5d6aac2d1b | ||
|
|
c6723ecd4e | ||
|
|
838c8f48d3 | ||
|
|
65229fae1f | ||
|
|
f25ea89160 | ||
|
|
18401d5d1e | ||
|
|
1f2cf08108 | ||
|
|
74e86badba | ||
|
|
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 | ||
|
|
d7d68ccdeb | ||
|
|
5cf6362db4 | ||
|
|
4efcc48160 | ||
|
|
383274ce0f | ||
|
|
c9dec3a2f7 | ||
|
|
2d4bf2903b | ||
|
|
2b88cfbda0 | ||
|
|
eb6ab7a156 | ||
|
|
688cbe50f2 | ||
|
|
e0577d1628 | ||
|
|
0f8dd17fde | ||
|
|
4bc8a08606 | ||
|
|
cf34433186 | ||
|
|
279bd16b74 | ||
|
|
2e0081b66c |
13
.github/FUNDING.yml
vendored
Normal file
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']
|
||||
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -8,9 +8,11 @@ assignees: ''
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
@@ -18,9 +20,11 @@ Steps to reproduce the behavior:
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**KeePass Database**
|
||||
|
||||
- Created with: [e.g Windows KeePass 2.42]
|
||||
- Version: [e.g. 2]
|
||||
- Location: [e.g. Remote file retrieved with GDrive app]
|
||||
@@ -28,15 +32,18 @@ A clear and concise description of what you expected to happen.
|
||||
- Size: [e.g. 150Mo]
|
||||
- Contains attachment: [e.g. Yes]
|
||||
|
||||
**KeePassDX (please complete the following information):**
|
||||
**KeePassDX:**
|
||||
|
||||
- Version: [e.g. 2.5.0.0beta23]
|
||||
- Build: [e.g. Free]
|
||||
- Language: [e.g. French]
|
||||
|
||||
**Android (please complete the following information):**
|
||||
**Android:**
|
||||
|
||||
- Device: [e.g. GalaxyS8]
|
||||
- Version: [e.g. 8.1]
|
||||
|
||||
**Additional context**
|
||||
|
||||
Add any other context about the problem here.
|
||||
- Browser for Autofill: [e.g. Chrome version X]
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -80,6 +80,9 @@ art/screen*.png
|
||||
art/logo_512.png
|
||||
art/store_screens/
|
||||
|
||||
# Release
|
||||
releases/*
|
||||
|
||||
# Dir linux
|
||||
.directory
|
||||
*/.directory
|
||||
|
||||
77
CHANGELOG
77
CHANGELOG
@@ -1,3 +1,80 @@
|
||||
KeePassDX(4.0.2)
|
||||
* Fix Autofill with API 33
|
||||
|
||||
KeePassDX(4.0.1)
|
||||
* Fix back lock #1635 #1629 #1634
|
||||
* Fix lock button in settings #1630
|
||||
* Improve theme translation #1631
|
||||
|
||||
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
|
||||
|
||||
10
Gemfile
Normal file
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
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
|
||||
10
README.md
10
README.md
@@ -1,6 +1,6 @@
|
||||
# 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">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
- Create database files / entries and groups.
|
||||
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
|
||||
- **Compatible** with the majority of alternative programs (KeePass, KeePassX, KeePassXC, …).
|
||||
- **Compatible** with the majority of alternative programs (KeePass, KeePassXC, KeeWeb, …).
|
||||
- Allows opening and **copying URI / URL fields quickly**.
|
||||
- **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*.
|
||||
- **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA).
|
||||
@@ -53,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"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/)
|
||||
|
||||
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
|
||||
alt="Get it on Google Play"
|
||||
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
|
||||
[<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
|
||||
|
||||
@@ -72,7 +74,7 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2022 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||
Copyright © 2023 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||
|
||||
This file is part of KeePassDX.
|
||||
|
||||
|
||||
@@ -4,16 +4,16 @@ apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
buildToolsVersion "31.0.0"
|
||||
ndkVersion "21.4.7075529"
|
||||
namespace 'com.kunzisoft.keepass'
|
||||
compileSdkVersion 33
|
||||
buildToolsVersion "33.0.2"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 31
|
||||
versionCode = 104
|
||||
versionName = "3.3.2"
|
||||
targetSdkVersion 33
|
||||
versionCode = 125
|
||||
versionName = "4.0.2"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
@@ -33,7 +33,6 @@ android {
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled = false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +49,9 @@ android {
|
||||
"\"KeepassDXStyle_Reply\"," +
|
||||
"\"KeepassDXStyle_Reply_Night\"," +
|
||||
"\"KeepassDXStyle_Purple\"," +
|
||||
"\"KeepassDXStyle_Purple_Dark\"}"
|
||||
"\"KeepassDXStyle_Purple_Dark\"," +
|
||||
"\"KeepassDXStyle_Dynamic_Light\"," +
|
||||
"\"KeepassDXStyle_Dynamic_Night\"}"
|
||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||
}
|
||||
free {
|
||||
@@ -59,16 +60,16 @@ android {
|
||||
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
||||
buildConfigField "boolean", "CLOSED_STORE", "true"
|
||||
buildConfigField "String[]", "STYLES_DISABLED",
|
||||
"{\"KeepassDXStyle_Simple\"," +
|
||||
"\"KeepassDXStyle_Simple_Night\"," +
|
||||
"\"KeepassDXStyle_Blue\"," +
|
||||
"{\"KeepassDXStyle_Blue\"," +
|
||||
"\"KeepassDXStyle_Blue_Night\"," +
|
||||
"\"KeepassDXStyle_Red\"," +
|
||||
"\"KeepassDXStyle_Red_Night\"," +
|
||||
"\"KeepassDXStyle_Reply\"," +
|
||||
"\"KeepassDXStyle_Reply_Night\"," +
|
||||
"\"KeepassDXStyle_Purple\"," +
|
||||
"\"KeepassDXStyle_Purple_Dark\"}"
|
||||
"\"KeepassDXStyle_Purple_Dark\"," +
|
||||
"\"KeepassDXStyle_Dynamic_Light\"," +
|
||||
"\"KeepassDXStyle_Dynamic_Night\"}"
|
||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
|
||||
}
|
||||
@@ -93,7 +94,7 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
def room_version = "2.4.2"
|
||||
def room_version = "2.5.1"
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
@@ -101,18 +102,18 @@ dependencies {
|
||||
implementation "androidx.appcompat:appcompat:$android_appcompat_version"
|
||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.biometric:biometric:1.1.0'
|
||||
implementation 'androidx.media:media:1.5.0'
|
||||
implementation 'androidx.media:media:1.6.0'
|
||||
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||
implementation "androidx.core:core-ktx:$android_core_version"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.4.1'
|
||||
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-beta04"
|
||||
implementation "com.splitwise:tokenautocomplete:4.0.0-beta05"
|
||||
// Database
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
@@ -127,11 +128,12 @@ dependencies {
|
||||
// Apache Commons
|
||||
implementation 'commons-io:commons-io:2.8.0'
|
||||
implementation 'commons-codec:commons-codec:1.15'
|
||||
// Encrypt lib
|
||||
implementation project(path: ':crypto')
|
||||
// Icon pack
|
||||
implementation project(path: ':icon-pack-classic')
|
||||
implementation project(path: ':icon-pack-material')
|
||||
// Password generator
|
||||
implementation 'me.gosimple:nbvcxz:1.5.0'
|
||||
|
||||
// Modules import
|
||||
implementation project(path: ':database')
|
||||
implementation project(path: ':icon-pack')
|
||||
|
||||
// Tests
|
||||
androidTestImplementation "androidx.test:runner:$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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
15
app/src/free/res/drawable-v24/ic_launcher_monochrome.xml
Normal file
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
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
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>
|
||||
15
app/src/libre/res/drawable-v24/ic_launcher_monochrome.xml
Normal file
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
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
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>
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.kunzisoft.keepass"
|
||||
android:installLocation="auto">
|
||||
<supports-screens
|
||||
android:smallScreens="true"
|
||||
@@ -10,6 +9,8 @@
|
||||
android:anyDensity="true" />
|
||||
<uses-permission
|
||||
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
|
||||
@@ -20,6 +21,12 @@
|
||||
<uses-permission
|
||||
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
|
||||
android:label="@string/app_name"
|
||||
@@ -39,7 +46,6 @@
|
||||
android:value="${googleAndroidBackupAPIKey}" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.FileDatabaseSelectActivity"
|
||||
android:theme="@style/KeepassDXStyle.SplashScreen"
|
||||
android:launchMode="singleTop"
|
||||
android:exported="true"
|
||||
android:configChanges="keyboardHidden"
|
||||
@@ -131,12 +137,15 @@
|
||||
<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
|
||||
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
android:windowSoftInputMode="adjustPan" />
|
||||
<!-- About and Settings -->
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
||||
@@ -147,14 +156,21 @@
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
|
||||
android:theme="@style/Theme.Transparent"
|
||||
android:configChanges="keyboardHidden" />
|
||||
android:configChanges="keyboardHidden"
|
||||
android:excludeFromRecents="true"/>
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
|
||||
android:name="com.kunzisoft.keepass.settings.AdvancedUnlockSettingsActivity" />
|
||||
<activity
|
||||
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
|
||||
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
|
||||
android:theme="@style/Theme.Transparent"
|
||||
android:launchMode="singleInstance"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
@@ -165,13 +181,11 @@
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="otpauth" android:host="totp" />
|
||||
<data android:scheme="otpauth" android:host="hotp" />
|
||||
<data android:scheme="otpauth"/>
|
||||
<data android:host="totp"/>
|
||||
<data android:host="hotp"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.MagikeyboardLauncherActivity"
|
||||
android:theme="@style/Theme.Transparent" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
||||
android:label="@string/keyboard_setting_label"
|
||||
|
||||
@@ -172,16 +172,16 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
private val onScaleGestureListener: ScaleGestureDetector.OnScaleGestureListener =
|
||||
object : ScaleGestureDetector.OnScaleGestureListener {
|
||||
|
||||
override fun onScale(detector: ScaleGestureDetector?): Boolean {
|
||||
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||
if (isDragging() || isBitmapTranslateAnimationRunning || isBitmapScaleAnimationRunninng) {
|
||||
return false
|
||||
}
|
||||
|
||||
val scaleFactor = detector?.scaleFactor ?: 1.0f
|
||||
val focalX = detector?.focusX ?: bitmapBounds.centerX()
|
||||
val focalY = detector?.focusY ?: bitmapBounds.centerY()
|
||||
val scaleFactor = detector.scaleFactor
|
||||
val focalX = detector.focusX
|
||||
val focalY = detector.focusY
|
||||
|
||||
if (detector?.scaleFactor == 1.0f) {
|
||||
if (detector.scaleFactor == 1.0f) {
|
||||
// scale is not changing
|
||||
return true
|
||||
}
|
||||
@@ -191,22 +191,23 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onScaleBegin(p0: ScaleGestureDetector?): Boolean = true
|
||||
override fun onScaleBegin(p0: ScaleGestureDetector): Boolean = true
|
||||
|
||||
override fun onScaleEnd(p0: ScaleGestureDetector) {}
|
||||
|
||||
override fun onScaleEnd(p0: ScaleGestureDetector?) {}
|
||||
}
|
||||
|
||||
private val onGestureListener: GestureDetector.OnGestureListener =
|
||||
object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onDown(e: MotionEvent?): Boolean = true
|
||||
override fun onDown(e: MotionEvent): Boolean = true
|
||||
|
||||
override fun onScroll(
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent?,
|
||||
distanceX: Float,
|
||||
distanceY: Float
|
||||
e1: MotionEvent,
|
||||
e2: MotionEvent,
|
||||
distanceX: Float,
|
||||
distanceY: Float
|
||||
): Boolean {
|
||||
if (e2?.pointerCount != 1) {
|
||||
if (e2.pointerCount != 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -219,13 +220,11 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
}
|
||||
|
||||
override fun onFling(
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent?,
|
||||
velocityX: Float,
|
||||
velocityY: Float
|
||||
e1: MotionEvent,
|
||||
e2: MotionEvent,
|
||||
velocityX: Float,
|
||||
velocityY: Float
|
||||
): Boolean {
|
||||
e1 ?: return true
|
||||
|
||||
if (scale > minScale) {
|
||||
processFlingBitmap(velocityX, velocityY)
|
||||
} else {
|
||||
@@ -234,9 +233,7 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDoubleTap(e: MotionEvent?): Boolean {
|
||||
e ?: return false
|
||||
|
||||
override fun onDoubleTap(e: MotionEvent): Boolean {
|
||||
if (isBitmapScaleAnimationRunninng) {
|
||||
return true
|
||||
}
|
||||
@@ -376,21 +373,21 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||
}
|
||||
.setListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator?) {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator?) {
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
onViewTranslateListener?.onDismiss(imageView)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator?) {
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator?) {
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
@@ -409,21 +406,21 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||
}
|
||||
addListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator?) {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator?) {
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
onViewTranslateListener?.onDismiss(imageView)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator?) {
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator?) {
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
@@ -480,20 +477,20 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
setTransform()
|
||||
}
|
||||
addListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator?) {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
isBitmapTranslateAnimationRunning = true
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator?) {
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
isBitmapTranslateAnimationRunning = false
|
||||
constrainBitmapBounds()
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator?) {
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
isBitmapTranslateAnimationRunning = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator?) {
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
@@ -531,11 +528,11 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
setTransform()
|
||||
}
|
||||
addListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator?) {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
isBitmapScaleAnimationRunninng = true
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator?) {
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
isBitmapScaleAnimationRunninng = false
|
||||
if (endScale == minScale) {
|
||||
zoomToTargetScale(minScale, focalX, focalY)
|
||||
@@ -543,11 +540,11 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator?) {
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
isBitmapScaleAnimationRunninng = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator?) {
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
@@ -585,11 +582,11 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
setTransform()
|
||||
}
|
||||
addListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator?) {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
isBitmapScaleAnimationRunninng = true
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator?) {
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
isBitmapScaleAnimationRunninng = false
|
||||
if (endScale == minScale) {
|
||||
scale = minScale
|
||||
@@ -599,11 +596,11 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator?) {
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
isBitmapScaleAnimationRunninng = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator?) {
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
@@ -669,19 +666,19 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
onViewTranslateListener?.onViewTranslate(this, amount)
|
||||
}
|
||||
.setListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator?) {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator?) {
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
onViewTranslateListener?.onRestore(imageView)
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator?) {
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator?) {
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
@@ -696,19 +693,19 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||
}
|
||||
addListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator?) {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator?) {
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
onViewTranslateListener?.onRestore(imageView)
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator?) {
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator?) {
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
@@ -737,27 +734,27 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
onViewTranslateListener?.onViewTranslate(this, amount)
|
||||
}
|
||||
.setListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator?) {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = true
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator?) {
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
onViewTranslateListener?.onDismiss(imageView)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator?) {
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator?) {
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, imageView.translationY.toFloat()).apply {
|
||||
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, imageView.translationY).apply {
|
||||
duration = dismissAnimationDuration
|
||||
interpolator = AccelerateDecelerateInterpolator()
|
||||
addUpdateListener {
|
||||
@@ -766,21 +763,21 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||
}
|
||||
addListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator?) {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = true
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator?) {
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
onViewTranslateListener?.onDismiss(imageView)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator?) {
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator?) {
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
|
||||
@@ -30,6 +30,8 @@ import androidx.core.text.HtmlCompat
|
||||
import com.kunzisoft.keepass.BuildConfig
|
||||
import com.kunzisoft.keepass.R
|
||||
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
|
||||
|
||||
class AboutActivity : StylishActivity() {
|
||||
@@ -45,10 +47,16 @@ class AboutActivity : StylishActivity() {
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(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 build: String
|
||||
try {
|
||||
version = packageManager.getPackageInfo(packageName, 0).versionName
|
||||
version = packageManager.getPackageInfoCompat(packageName).versionName
|
||||
build = BuildConfig.BUILD_VERSION
|
||||
} catch (e: NameNotFoundException) {
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
text = HtmlCompat.fromHtml(getString(R.string.html_about_contribution),
|
||||
|
||||
@@ -36,11 +36,14 @@ import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.autofill.CompatInlineSuggestionsRequest
|
||||
import com.kunzisoft.keepass.autofill.KeeAutofillService
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
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)
|
||||
class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
@@ -58,7 +61,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
// Retrieve selection mode
|
||||
@@ -69,11 +72,11 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
// To pass extra inline request
|
||||
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
compatInlineSuggestionsRequest = bundle.getParcelable(KEY_INLINE_SUGGESTION)
|
||||
compatInlineSuggestionsRequest = bundle.getParcelableCompat(KEY_INLINE_SUGGESTION)
|
||||
}
|
||||
// Build search param
|
||||
bundle.getParcelable<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
|
||||
SearchInfo.getConcreteWebDomain(
|
||||
bundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
|
||||
WebDomain.getConcreteWebDomain(
|
||||
this,
|
||||
searchInfo.webDomain
|
||||
) { concreteWebDomain ->
|
||||
@@ -99,9 +102,9 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
}
|
||||
SpecialMode.REGISTRATION -> {
|
||||
// To register info
|
||||
val registerInfo = intent.getParcelableExtra<RegisterInfo>(KEY_REGISTER_INFO)
|
||||
val registerInfo = intent.getParcelableExtraCompat<RegisterInfo>(KEY_REGISTER_INFO)
|
||||
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
launchRegistration(database, searchInfo, registerInfo)
|
||||
}
|
||||
@@ -115,7 +118,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchSelection(database: Database?,
|
||||
private fun launchSelection(database: ContextualDatabase?,
|
||||
autofillComponent: AutofillComponent?,
|
||||
searchInfo: SearchInfo) {
|
||||
if (autofillComponent == null) {
|
||||
@@ -158,7 +161,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchRegistration(database: Database?,
|
||||
private fun launchRegistration(database: ContextualDatabase?,
|
||||
searchInfo: SearchInfo,
|
||||
registerInfo: RegisterInfo?) {
|
||||
if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
@@ -30,28 +31,34 @@ import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.progressindicator.LinearProgressIndicator
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.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.Database
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||
@@ -66,23 +73,28 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||
import com.kunzisoft.keepass.view.WindowInsetPosition
|
||||
import com.kunzisoft.keepass.view.applyWindowInsets
|
||||
import com.kunzisoft.keepass.view.changeControlColor
|
||||
import com.kunzisoft.keepass.view.changeTitleColor
|
||||
import com.kunzisoft.keepass.view.hideByFading
|
||||
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
|
||||
class EntryActivity : DatabaseLockActivity() {
|
||||
|
||||
private var footer: ViewGroup? = null
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
||||
private var appBarLayout: AppBarLayout? = null
|
||||
private var titleIconView: ImageView? = null
|
||||
private var historyView: View? = null
|
||||
private var tagsListView: RecyclerView? = null
|
||||
private var entryContentTab: TabLayout? = null
|
||||
private var tagsAdapter: TagsAdapter? = null
|
||||
private var entryProgress: LinearProgressIndicator? = null
|
||||
private var lockView: View? = null
|
||||
@@ -96,7 +108,6 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
private var mMainEntryId: NodeId<UUID>? = null
|
||||
private var mHistoryPosition: Int = -1
|
||||
private var mEntryIsHistory: Boolean = false
|
||||
private var mUrl: String? = null
|
||||
private var mEntryLoaded = false
|
||||
|
||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||
@@ -109,9 +120,9 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
}
|
||||
|
||||
private var mIcon: IconImage? = null
|
||||
private var mColorAccent: Int = 0
|
||||
private var mControlColor: Int = 0
|
||||
private var mColorPrimary: Int = 0
|
||||
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
|
||||
@@ -127,32 +138,42 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
// Get views
|
||||
footer = findViewById(R.id.activity_entry_footer)
|
||||
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
||||
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
||||
appBarLayout = findViewById(R.id.app_bar)
|
||||
titleIconView = findViewById(R.id.entry_icon)
|
||||
historyView = findViewById(R.id.history_container)
|
||||
tagsListView = findViewById(R.id.entry_tags_list_view)
|
||||
entryContentTab = findViewById(R.id.entry_content_tab)
|
||||
entryProgress = findViewById(R.id.entry_progress)
|
||||
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 taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
val taControlColor = theme.obtainStyledAttributes(intArrayOf(R.attr.toolbarColorControl))
|
||||
val taColorPrimary = theme.obtainStyledAttributes(intArrayOf(R.attr.colorPrimary))
|
||||
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))
|
||||
mColorAccent = taColorAccent.getColor(0, Color.BLACK)
|
||||
mControlColor = taControlColor.getColor(0, Color.BLACK)
|
||||
mColorPrimary = taColorPrimary.getColor(0, Color.BLACK)
|
||||
mColorSecondary = taColorSecondary.getColor(0, Color.BLACK)
|
||||
mColorSurface = taColorSurface.getColor(0, Color.BLACK)
|
||||
mColorOnSurface = taColorOnSurface.getColor(0, Color.BLACK)
|
||||
mColorBackground = taColorBackground.getColor(0, Color.BLACK)
|
||||
taColorAccent.recycle()
|
||||
taControlColor.recycle()
|
||||
taColorPrimary.recycle()
|
||||
taColorSecondary.recycle()
|
||||
taColorSurface.recycle()
|
||||
taColorOnSurface.recycle()
|
||||
taColorBackground.recycle()
|
||||
|
||||
// Init Tags adapter
|
||||
@@ -162,9 +183,22 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
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.getParcelableExtra<NodeId<UUID>?>(KEY_ENTRY)?.let { mainEntryId ->
|
||||
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)
|
||||
@@ -193,6 +227,10 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
lockAndExit()
|
||||
}
|
||||
|
||||
mEntryViewModel.sectionSelected.observe(this) { entrySection ->
|
||||
entryContentTab?.getTabAt(entrySection.position)?.select()
|
||||
}
|
||||
|
||||
mEntryViewModel.entryInfoHistory.observe(this) { entryInfoHistory ->
|
||||
if (entryInfoHistory != null) {
|
||||
this.mMainEntryId = entryInfoHistory.mainEntryId
|
||||
@@ -204,16 +242,16 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
this.mEntryIsHistory = entryIsHistory
|
||||
// Assign history dedicated view
|
||||
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
|
||||
// TODO History badge
|
||||
/*
|
||||
if (entryIsHistory) {
|
||||
collapsingToolbarLayout?.contentScrim =
|
||||
ColorDrawable(mColorAccent)
|
||||
}
|
||||
}*/
|
||||
|
||||
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.launchNotificationIfAllowed(this, entryInfo)
|
||||
ClipboardEntryNotificationService.checkAndLaunchNotification(this, entryInfo)
|
||||
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
||||
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
|
||||
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
|
||||
@@ -226,7 +264,6 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
if (entryInfo.title.isNotEmpty()) entryInfo.title else UuidUtil.toHexString(entryInfo.id)
|
||||
collapsingToolbarLayout?.title = entryTitle
|
||||
toolbar?.title = entryTitle
|
||||
mUrl = entryInfo.url
|
||||
// Assign tags
|
||||
val tags = entryInfo.tags
|
||||
tagsListView?.visibility = if (tags.isEmpty()) View.GONE else View.VISIBLE
|
||||
@@ -290,14 +327,14 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
return coordinatorLayout
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
mEntryViewModel.loadDatabase(database)
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
@@ -345,8 +382,8 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
}
|
||||
|
||||
private fun applyToolbarColors() {
|
||||
appBarLayout?.setBackgroundColor(mBackgroundColor ?: mColorPrimary)
|
||||
collapsingToolbarLayout?.contentScrim = ColorDrawable(mBackgroundColor ?: mColorPrimary)
|
||||
collapsingToolbarLayout?.setBackgroundColor(mBackgroundColor ?: mColorSurface)
|
||||
collapsingToolbarLayout?.contentScrim = ColorDrawable(mBackgroundColor ?: mColorSurface)
|
||||
val backgroundDarker = if (mBackgroundColor != null) {
|
||||
ColorUtils.blendARGB(mBackgroundColor!!, Color.WHITE, 0.1f)
|
||||
} else {
|
||||
@@ -356,15 +393,15 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
.createBlendModeColorFilterCompat(backgroundDarker, BlendModeCompat.SRC_IN)
|
||||
mIcon?.let { icon ->
|
||||
titleIconView?.let { iconView ->
|
||||
mIconDrawableFactory?.assignDatabaseIcon(
|
||||
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(
|
||||
iconView,
|
||||
icon,
|
||||
mForegroundColor ?: mColorAccent
|
||||
mForegroundColor ?: mColorSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
toolbar?.changeControlColor(mForegroundColor ?: mControlColor)
|
||||
collapsingToolbarLayout?.changeTitleColor(mForegroundColor ?: mControlColor)
|
||||
toolbar?.changeControlColor(mForegroundColor ?: mColorOnSurface)
|
||||
collapsingToolbarLayout?.changeTitleColor(mForegroundColor ?: mColorOnSurface)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
@@ -388,9 +425,6 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||
if (mUrl?.isEmpty() != false) {
|
||||
menu?.findItem(R.id.menu_goto_url)?.isVisible = false
|
||||
}
|
||||
if (mEntryIsHistory || mDatabaseReadOnly) {
|
||||
menu?.findItem(R.id.menu_save_database)?.isVisible = false
|
||||
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||
@@ -451,12 +485,6 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.menu_goto_url -> {
|
||||
mUrl?.let { url ->
|
||||
UriUtil.gotoUrl(this, url)
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.menu_restore_entry_history -> {
|
||||
mMainEntryId?.let { mainEntryId ->
|
||||
restoreEntryHistory(
|
||||
@@ -506,7 +534,7 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
* Open standard Entry activity
|
||||
*/
|
||||
fun launch(activity: Activity,
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
entryId: NodeId<UUID>,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||
if (database.loaded) {
|
||||
@@ -522,7 +550,7 @@ class EntryActivity : DatabaseLockActivity() {
|
||||
* Open history Entry activity
|
||||
*/
|
||||
fun launch(activity: Activity,
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
entryId: NodeId<UUID>,
|
||||
historyPosition: Int,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.DatePickerDialog
|
||||
import android.app.TimePickerDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
@@ -32,7 +30,10 @@ import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.Spinner
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
@@ -44,10 +45,16 @@ import androidx.core.view.isVisible
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.google.android.material.datepicker.MaterialDatePicker
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.timepicker.MaterialTimePicker
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.*
|
||||
import com.kunzisoft.keepass.activities.dialogs.ColorPickerDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.EntryCustomFieldDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE
|
||||
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
||||
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
@@ -55,12 +62,20 @@ import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
|
||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Field
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.template.*
|
||||
import com.kunzisoft.keepass.database.element.template.Template
|
||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||
import com.kunzisoft.keepass.model.*
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
||||
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||
@@ -72,24 +87,29 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.*
|
||||
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||
import com.kunzisoft.keepass.view.ToolbarAction
|
||||
import com.kunzisoft.keepass.view.WindowInsetPosition
|
||||
import com.kunzisoft.keepass.view.applyWindowInsets
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.view.hideByFading
|
||||
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
||||
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
|
||||
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
||||
import org.joda.time.DateTime
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import java.util.UUID
|
||||
|
||||
class EntryEditActivity : DatabaseLockActivity(),
|
||||
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
|
||||
GeneratePasswordDialogFragment.GeneratePasswordListener,
|
||||
SetOTPDialogFragment.CreateOtpListener,
|
||||
DatePickerDialog.OnDateSetListener,
|
||||
TimePickerDialog.OnTimeSetListener,
|
||||
FileTooBigDialogFragment.ActionChooseListener,
|
||||
ReplaceFileDialogFragment.ActionChooseListener {
|
||||
|
||||
// Views
|
||||
private var footer: ViewGroup? = null
|
||||
private var container: ViewGroup? = null
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var scrollView: NestedScrollView? = null
|
||||
private var templateSelectorSpinner: Spinner? = null
|
||||
@@ -119,6 +139,20 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
mEntryEditViewModel.selectIcon(icon)
|
||||
}
|
||||
|
||||
private var mPasswordField: Field? = null
|
||||
private var mKeyGeneratorResultLauncher = KeyGeneratorActivity.registerForGeneratedKeyResult(this) { keyGenerated ->
|
||||
keyGenerated?.let {
|
||||
mPasswordField?.let {
|
||||
it.protectedValue.stringValue = keyGenerated
|
||||
mEntryEditViewModel.selectPassword(it)
|
||||
}
|
||||
}
|
||||
mPasswordField = null
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
performedNextEducation()
|
||||
}
|
||||
}
|
||||
|
||||
// To ask data lost only one time
|
||||
private var backPressedAlreadyApproved = false
|
||||
|
||||
@@ -128,10 +162,8 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
|
||||
// Bottom Bar
|
||||
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
|
||||
setSupportActionBar(entryEditAddToolBar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
supportActionBar?.setDisplayShowTitleEnabled(false)
|
||||
footer = findViewById(R.id.activity_entry_edit_footer)
|
||||
container = findViewById(R.id.activity_entry_edit_container)
|
||||
coordinatorLayout = findViewById(R.id.entry_edit_coordinator_layout)
|
||||
scrollView = findViewById(R.id.entry_edit_scroll)
|
||||
scrollView?.scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
||||
@@ -140,19 +172,30 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
validateButton = findViewById(R.id.entry_edit_validate)
|
||||
loadingView = findViewById(R.id.loading)
|
||||
|
||||
setSupportActionBar(entryEditAddToolBar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
supportActionBar?.setDisplayShowTitleEnabled(false)
|
||||
|
||||
// To apply fit window with transparency
|
||||
setTransparentNavigationBar(applyToStatusBar = true) {
|
||||
container?.applyWindowInsets(WindowInsetPosition.TOP)
|
||||
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
|
||||
}
|
||||
|
||||
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
||||
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
||||
|
||||
// Entry is retrieve, it's an entry to update
|
||||
var entryId: NodeId<UUID>? = null
|
||||
intent.getParcelableExtra<NodeId<UUID>>(KEY_ENTRY)?.let { entryToUpdate ->
|
||||
intent.getParcelableExtraCompat<NodeId<UUID>>(KEY_ENTRY)?.let { entryToUpdate ->
|
||||
intent.removeExtra(KEY_ENTRY)
|
||||
entryId = entryToUpdate
|
||||
}
|
||||
|
||||
// Parent is retrieve, it's a new entry to create
|
||||
var parentId: NodeId<*>? = null
|
||||
intent.getParcelableExtra<NodeId<*>>(KEY_PARENT)?.let { parent ->
|
||||
intent.getParcelableExtraCompat<NodeId<*>>(KEY_PARENT)?.let { parent ->
|
||||
intent.removeExtra(KEY_PARENT)
|
||||
parentId = parent
|
||||
}
|
||||
@@ -169,7 +212,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
uri?.let { attachmentToUploadUri ->
|
||||
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
|
||||
attachmentToUploadUri.getDocumentFile(this)?.also { documentFile ->
|
||||
documentFile.name?.let { fileName ->
|
||||
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
|
||||
FileTooBigDialogFragment.build(attachmentToUploadUri, fileName)
|
||||
@@ -187,7 +230,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
// Lock button
|
||||
lockView?.setOnClickListener { lockAndExit() }
|
||||
// Save button
|
||||
validateButton?.setOnClickListener { saveEntry() }
|
||||
validateButton?.setOnClickListener { validateEntry() }
|
||||
|
||||
mEntryEditViewModel.onTemplateChanged.observe(this) { template ->
|
||||
this.mTemplate = template
|
||||
@@ -205,7 +248,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
this@EntryEditActivity,
|
||||
templates
|
||||
).apply {
|
||||
iconDrawableFactory = mIconDrawableFactory
|
||||
iconDrawableFactory = mDatabase?.iconDrawableFactory
|
||||
}
|
||||
adapter = mTemplatesSelectorAdapter
|
||||
val selectedTemplate = if (mTemplate != null)
|
||||
@@ -256,21 +299,26 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
mEntryEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
|
||||
if (dateInstant.type == DateInstant.Type.TIME) {
|
||||
// Launch the time picker
|
||||
val dateTime = DateTime(dateInstant.date)
|
||||
TimePickerFragment.getInstance(dateTime.hourOfDay, dateTime.minuteOfHour)
|
||||
.show(supportFragmentManager, "TimePickerFragment")
|
||||
MaterialTimePicker.Builder().build().apply {
|
||||
addOnPositiveButtonClickListener {
|
||||
mEntryEditViewModel.selectTime(this.hour, this.minute)
|
||||
}
|
||||
show(supportFragmentManager, "TimePickerFragment")
|
||||
}
|
||||
} else {
|
||||
// Launch the date picker
|
||||
val dateTime = DateTime(dateInstant.date)
|
||||
DatePickerFragment.getInstance(dateTime.year, dateTime.monthOfYear - 1, dateTime.dayOfMonth)
|
||||
.show(supportFragmentManager, "DatePickerFragment")
|
||||
MaterialDatePicker.Builder.datePicker().build().apply {
|
||||
addOnPositiveButtonClickListener {
|
||||
mEntryEditViewModel.selectDate(it)
|
||||
}
|
||||
show(supportFragmentManager, "DatePickerFragment")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mEntryEditViewModel.requestPasswordSelection.observe(this) { passwordField ->
|
||||
GeneratePasswordDialogFragment
|
||||
.getInstance(passwordField)
|
||||
.show(supportFragmentManager, "PasswordGeneratorFragment")
|
||||
mPasswordField = passwordField
|
||||
KeyGeneratorActivity.launch(this, mKeyGeneratorResultLauncher)
|
||||
}
|
||||
|
||||
mEntryEditViewModel.requestCustomFieldEdition.observe(this) { field ->
|
||||
@@ -353,19 +401,19 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
mAllowCustomFields = database?.allowEntryCustomFields() == true
|
||||
mAllowOTP = database?.allowOTP == true
|
||||
mEntryEditViewModel.loadDatabase(database)
|
||||
mTemplatesSelectorAdapter?.apply {
|
||||
iconDrawableFactory = mIconDrawableFactory
|
||||
iconDrawableFactory = mDatabase?.iconDrawableFactory
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
@@ -418,17 +466,18 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
finishForEntryResult(entry)
|
||||
}
|
||||
|
||||
private fun entryValidatedForKeyboardSelection(database: Database, entry: Entry) {
|
||||
private fun entryValidatedForKeyboardSelection(database: ContextualDatabase, entry: Entry) {
|
||||
// Populate Magikeyboard with entry
|
||||
populateKeyboardAndMoveAppToBackground(this,
|
||||
entry.getEntryInfo(database),
|
||||
intent)
|
||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||
this,
|
||||
entry.getEntryInfo(database)
|
||||
)
|
||||
onValidateSpecialMode()
|
||||
// Don't keep activity history for entry edition
|
||||
finishForEntryResult(entry)
|
||||
}
|
||||
|
||||
private fun entryValidatedForAutofillSelection(database: Database, entry: Entry) {
|
||||
private fun entryValidatedForAutofillSelection(database: ContextualDatabase, entry: Entry) {
|
||||
// Build Autofill response with the entry selected
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity,
|
||||
@@ -463,6 +512,11 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the screen on
|
||||
if (PreferencesUtil.isKeepScreenOnEnabled(this)) {
|
||||
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -525,9 +579,9 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the new entry or update an existing entry in the database
|
||||
* Validate the new entry or update an existing entry in the database
|
||||
*/
|
||||
private fun saveEntry() {
|
||||
private fun validateEntry() {
|
||||
mAttachmentFileBinderManager?.stopUploadAllAttachments()
|
||||
mEntryEditViewModel.requestEntryInfoUpdate(mDatabase)
|
||||
}
|
||||
@@ -609,14 +663,29 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
)
|
||||
if (!addAttachmentEducationPerformed) {
|
||||
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
|
||||
setupOtpView != null
|
||||
val validateEntryEducationPerformed = setupOtpView != null
|
||||
&& setupOtpView.isVisible
|
||||
&& mEntryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
|
||||
setupOtpView,
|
||||
{
|
||||
setupOtp()
|
||||
},
|
||||
{
|
||||
performedNextEducation()
|
||||
}
|
||||
)
|
||||
if (!validateEntryEducationPerformed) {
|
||||
val entryValidateView = validateButton
|
||||
mAllowCustomFields
|
||||
&& entryValidateView != null
|
||||
&& entryValidateView.isVisible
|
||||
&& mEntryEditActivityEducation.checkAndPerformedValidateEntryEducation(
|
||||
entryValidateView,
|
||||
{
|
||||
validateEntry()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -637,39 +706,16 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
return true
|
||||
}
|
||||
android.R.id.home -> {
|
||||
onBackPressed()
|
||||
onDatabaseBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) {
|
||||
// To fix android 4.4 issue
|
||||
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
|
||||
if (datePicker?.isShown == true) {
|
||||
mEntryEditViewModel.selectDate(year, month, day)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTimeSet(timePicker: TimePicker?, hours: Int, minutes: Int) {
|
||||
mEntryEditViewModel.selectTime(hours, minutes)
|
||||
}
|
||||
|
||||
override fun acceptPassword(passwordField: Field) {
|
||||
mEntryEditViewModel.selectPassword(passwordField)
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
performedNextEducation()
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancelPassword(passwordField: Field) {
|
||||
// Do nothing here
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
override fun onDatabaseBackPressed() {
|
||||
onApprovedBackPressed {
|
||||
super@EntryEditActivity.onBackPressed()
|
||||
super@EntryEditActivity.onDatabaseBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -724,7 +770,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
entryAddedOrUpdatedListener.invoke(
|
||||
result.data?.getParcelableExtra(ADD_OR_UPDATE_ENTRY_KEY)
|
||||
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
|
||||
)
|
||||
} else {
|
||||
entryAddedOrUpdatedListener.invoke(null)
|
||||
@@ -737,7 +783,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
entryAddedOrUpdatedListener.invoke(
|
||||
result.data?.getParcelableExtra(ADD_OR_UPDATE_ENTRY_KEY)
|
||||
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
|
||||
)
|
||||
} else {
|
||||
entryAddedOrUpdatedListener.invoke(null)
|
||||
@@ -749,7 +795,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
* Launch EntryEditActivity to update an existing entry by his [entryId]
|
||||
*/
|
||||
fun launchToUpdate(activity: Activity,
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
entryId: NodeId<UUID>,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
@@ -765,7 +811,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
* Launch EntryEditActivity to add a new entry in an existent group
|
||||
*/
|
||||
fun launchToCreate(activity: Activity,
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
groupId: NodeId<*>,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
@@ -778,7 +824,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
|
||||
fun launchToUpdateForSave(context: Context,
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
entryId: NodeId<UUID>,
|
||||
searchInfo: SearchInfo) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
@@ -795,7 +841,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
}
|
||||
|
||||
fun launchToCreateForSave(context: Context,
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
groupId: NodeId<*>,
|
||||
searchInfo: SearchInfo) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
@@ -815,7 +861,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
* Launch EntryEditActivity to add a new entry in keyboard selection
|
||||
*/
|
||||
fun launchForKeyboardSelectionResult(context: Context,
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
groupId: NodeId<*>,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
@@ -836,7 +882,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
groupId: NodeId<*>,
|
||||
@@ -860,7 +906,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
* Launch EntryEditActivity to register an updated entry (from autofill)
|
||||
*/
|
||||
fun launchToUpdateForRegistration(context: Context,
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
entryId: NodeId<UUID>,
|
||||
registerInfo: RegisterInfo? = null) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
@@ -880,7 +926,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
||||
* Launch EntryEditActivity to register a new entry (from autofill)
|
||||
*/
|
||||
fun launchToCreateForRegistration(context: Context,
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
groupId: NodeId<*>,
|
||||
registerInfo: RegisterInfo? = null) {
|
||||
if (database.loaded && !database.isReadOnly) {
|
||||
|
||||
@@ -19,20 +19,21 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
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.search.SearchHelper
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
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,
|
||||
@@ -45,156 +46,181 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
||||
}
|
||||
|
||||
override fun finishActivityIfReloadRequested(): Boolean {
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
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 ->
|
||||
val keySelectionBundle = intent.getBundleExtra(KEY_SELECTION_BUNDLE)
|
||||
if (keySelectionBundle != null) {
|
||||
// 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
|
||||
else
|
||||
sharedWebDomain = Uri.parse(extra).host
|
||||
}
|
||||
launchSelection(database, sharedWebDomain, otpString)
|
||||
}
|
||||
else -> {
|
||||
if (database != null) {
|
||||
GroupActivity.launch(this, database)
|
||||
} else {
|
||||
FileDatabaseSelectActivity.launch(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
Intent.ACTION_VIEW -> {
|
||||
// Retrieve OTP
|
||||
intent.dataString?.let { extra ->
|
||||
if (OtpEntryFields.isOTPUri(extra))
|
||||
otpString = extra
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
launch(database, searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
private fun launch(database: Database?,
|
||||
private fun launch(database: ContextualDatabase?,
|
||||
searchInfo: SearchInfo) {
|
||||
|
||||
if (!searchInfo.containsOnlyNullValues()) {
|
||||
// Setting to integrate Magikeyboard
|
||||
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
|
||||
// Setting to integrate Magikeyboard
|
||||
val searchShareForMagikeyboard = isKeyboardActivatedInSettings()
|
||||
|
||||
// If database is open
|
||||
val readOnly = database?.isReadOnly != false
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
database,
|
||||
searchInfo,
|
||||
{ openedDatabase, items ->
|
||||
// Items found
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
GroupActivity.launchForSaveResult(
|
||||
this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false)
|
||||
} else {
|
||||
Toast.makeText(applicationContext,
|
||||
R.string.autofill_read_only_save,
|
||||
Toast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
if (items.size == 1) {
|
||||
// Automatically populate keyboard
|
||||
val entryPopulate = items[0]
|
||||
populateKeyboardAndMoveAppToBackground(
|
||||
this,
|
||||
entryPopulate,
|
||||
intent)
|
||||
} else {
|
||||
// Select the one we want
|
||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
true)
|
||||
}
|
||||
} else {
|
||||
GroupActivity.launchForSearchResult(this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
true)
|
||||
}
|
||||
},
|
||||
{ openedDatabase ->
|
||||
// Show the database UI to select the entry
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
GroupActivity.launchForSaveResult(this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false)
|
||||
} else {
|
||||
Toast.makeText(applicationContext,
|
||||
R.string.autofill_read_only_save,
|
||||
Toast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
} else if (readOnly || searchShareForMagikeyboard) {
|
||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||
// If database is open
|
||||
val readOnly = database?.isReadOnly != false
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
database,
|
||||
searchInfo,
|
||||
{ openedDatabase, items ->
|
||||
// Items found
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
GroupActivity.launchForSaveResult(
|
||||
this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false)
|
||||
} else {
|
||||
Toast.makeText(applicationContext,
|
||||
R.string.autofill_read_only_save,
|
||||
Toast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
MagikeyboardService.performSelection(
|
||||
items,
|
||||
{ entryInfo ->
|
||||
// Automatically populate keyboard
|
||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||
this,
|
||||
entryInfo
|
||||
)
|
||||
},
|
||||
{ autoSearch ->
|
||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
autoSearch)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
GroupActivity.launchForSearchResult(this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
true)
|
||||
}
|
||||
},
|
||||
{ openedDatabase ->
|
||||
// Show the database UI to select the entry
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
GroupActivity.launchForSaveResult(this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false)
|
||||
}
|
||||
},
|
||||
{
|
||||
// If database not open
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
FileDatabaseSelectActivity.launchForSaveResult(this,
|
||||
searchInfo)
|
||||
} else {
|
||||
Toast.makeText(applicationContext,
|
||||
R.string.autofill_read_only_save,
|
||||
Toast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
|
||||
searchInfo)
|
||||
} else {
|
||||
FileDatabaseSelectActivity.launchForSearchResult(this,
|
||||
searchInfo)
|
||||
Toast.makeText(applicationContext,
|
||||
R.string.autofill_read_only_save,
|
||||
Toast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false)
|
||||
} else {
|
||||
GroupActivity.launchForSearchResult(this,
|
||||
openedDatabase,
|
||||
searchInfo,
|
||||
false)
|
||||
}
|
||||
)
|
||||
},
|
||||
{
|
||||
// If database not open
|
||||
if (searchInfo.otpString != null) {
|
||||
FileDatabaseSelectActivity.launchForSaveResult(this,
|
||||
searchInfo)
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
|
||||
searchInfo)
|
||||
} else {
|
||||
FileDatabaseSelectActivity.launchForSearchResult(this,
|
||||
searchInfo)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
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)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
||||
entry: EntryInfo,
|
||||
intent: Intent,
|
||||
toast: Boolean = true) {
|
||||
// Populate Magikeyboard with entry
|
||||
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
||||
// Consume the selection mode
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
activity.moveTaskToBack(true)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
@@ -54,9 +53,10 @@ import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||
@@ -65,8 +65,16 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
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.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
@@ -156,8 +164,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
||||
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
|
||||
launchPasswordActivity(
|
||||
databaseFileUri,
|
||||
fileDatabaseHistoryEntityToOpen.keyFileUri
|
||||
databaseFileUri,
|
||||
fileDatabaseHistoryEntityToOpen.keyFileUri,
|
||||
fileDatabaseHistoryEntityToOpen.hardwareKey
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -177,7 +186,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
|
||||
val databasePath = PreferencesUtil.getDefaultDatabasePath(this)
|
||||
|
||||
UriUtil.parse(databasePath)?.let { databaseFileUri ->
|
||||
databasePath?.parseUri()?.let { databaseFileUri ->
|
||||
launchPasswordActivityWithPath(databaseFileUri)
|
||||
} ?: run {
|
||||
Log.i(TAG, "No default database to prepare")
|
||||
@@ -187,7 +196,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
// Retrieve the database URI provided by file manager after an orientation change
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(EXTRA_DATABASE_URI)) {
|
||||
mDatabaseFileUri = savedInstanceState.getParcelable(EXTRA_DATABASE_URI)
|
||||
mDatabaseFileUri = savedInstanceState.getParcelableCompat(EXTRA_DATABASE_URI)
|
||||
}
|
||||
|
||||
// Observe list of databases
|
||||
@@ -226,7 +235,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
if (database != null) {
|
||||
launchGroupActivityIfLoaded(database)
|
||||
@@ -234,7 +243,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
@@ -245,13 +254,14 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_CREATE_TASK,
|
||||
ACTION_DATABASE_LOAD_TASK -> {
|
||||
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
|
||||
result.data?.getParcelableCompat<Uri>(DATABASE_URI_KEY)?.let { databaseUri ->
|
||||
val mainCredential =
|
||||
result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY)
|
||||
result.data?.getParcelableCompat(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY)
|
||||
?: MainCredential()
|
||||
databaseFilesViewModel.addDatabaseFile(
|
||||
databaseUri,
|
||||
mainCredential.keyFileUri
|
||||
mainCredential.keyFileUri,
|
||||
mainCredential.hardwareKey
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -269,18 +279,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var resultError = ""
|
||||
val resultMessage = result.message
|
||||
// Show error message
|
||||
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
||||
resultError = "$resultError $resultMessage"
|
||||
}
|
||||
Log.e(TAG, resultError)
|
||||
Snackbar.make(coordinatorLayout,
|
||||
resultError,
|
||||
Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
coordinatorLayout.showActionErrorIfNeeded(result)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -298,10 +298,11 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
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?) {
|
||||
MainCredentialActivity.launch(this,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
{ exception ->
|
||||
fileNoFoundAction(exception)
|
||||
},
|
||||
@@ -310,7 +311,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
mAutofillActivityResultLauncher)
|
||||
}
|
||||
|
||||
private fun launchGroupActivityIfLoaded(database: Database) {
|
||||
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
||||
if (database.loaded) {
|
||||
GroupActivity.launch(this,
|
||||
database,
|
||||
@@ -321,18 +322,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onValidateSpecialMode() {
|
||||
super.onValidateSpecialMode()
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onCancelSpecialMode() {
|
||||
super.onCancelSpecialMode()
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
||||
launchPasswordActivity(databaseUri, null)
|
||||
launchPasswordActivity(databaseUri, null, null)
|
||||
// Delete flickering for kitkat <=
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
||||
overridePendingTransition(0, 0)
|
||||
@@ -342,12 +333,12 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
super.onResume()
|
||||
|
||||
// Define special title
|
||||
specialTitle?.isVisible = UriUtil.contributingUser(this)
|
||||
specialTitle?.isVisible = this.isContributingUser()
|
||||
|
||||
// Show open and create button or special mode
|
||||
when (mSpecialMode) {
|
||||
SpecialMode.DEFAULT -> {
|
||||
if (ExternalFileHelper.allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
||||
if (packageManager.allowCreateDocumentByStorageAccessFramework()) {
|
||||
// There is an activity which can handle this intent.
|
||||
createDatabaseButtonView?.visibility = View.VISIBLE
|
||||
} else{
|
||||
@@ -442,7 +433,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
|
||||
android.R.id.home -> this.openUrl(R.string.file_manager_explanation_url)
|
||||
}
|
||||
MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
||||
return super.onOptionsItemSelected(item)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -41,16 +41,24 @@ 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.element.Database
|
||||
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.UriUtil
|
||||
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.*
|
||||
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() {
|
||||
@@ -96,7 +104,7 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
lockAndExit()
|
||||
}
|
||||
|
||||
intent?.getParcelableExtra<IconImage>(EXTRA_ICON)?.let {
|
||||
intent?.getParcelableExtraCompat<IconImage>(EXTRA_ICON)?.let {
|
||||
mIconImage = it
|
||||
}
|
||||
|
||||
@@ -112,7 +120,7 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
), ICON_PICKER_FRAGMENT_TAG)
|
||||
}
|
||||
} else {
|
||||
mIconImage = savedInstanceState.getParcelable(EXTRA_ICON) ?: mIconImage
|
||||
mIconImage = savedInstanceState.getParcelableCompat(EXTRA_ICON) ?: mIconImage
|
||||
}
|
||||
|
||||
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
|
||||
@@ -166,7 +174,7 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
if (database?.allowCustomIcons == true) {
|
||||
@@ -231,7 +239,7 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
if (mCustomIconsSelectionMode) {
|
||||
iconPickerViewModel.deselectAllCustomIcons()
|
||||
} else {
|
||||
onBackPressed()
|
||||
onDatabaseBackPressed()
|
||||
}
|
||||
}
|
||||
R.id.menu_edit -> {
|
||||
@@ -243,7 +251,7 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
}
|
||||
}
|
||||
R.id.menu_external_icon -> {
|
||||
UriUtil.gotoUrl(this, R.string.external_icon_url)
|
||||
this.openUrl(R.string.external_icon_url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,7 +265,7 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
// on Progress with thread
|
||||
val asyncResult: Deferred<IconPickerViewModel.IconCustomState?> = async {
|
||||
val iconCustomState = IconPickerViewModel.IconCustomState(null, true, R.string.error_upload_file)
|
||||
UriUtil.getFileData(this@IconPickerActivity, iconToUploadUri)?.also { documentFile ->
|
||||
iconToUploadUri?.getDocumentFile(this@IconPickerActivity)?.also { documentFile ->
|
||||
if (documentFile.length() > MAX_ICON_SIZE) {
|
||||
iconCustomState.errorStringId = R.string.error_file_to_big
|
||||
} else {
|
||||
@@ -321,9 +329,9 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
})
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
override fun onDatabaseBackPressed() {
|
||||
setResult()
|
||||
super.onBackPressed()
|
||||
super.onDatabaseBackPressed()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -336,7 +344,7 @@ class IconPickerActivity : DatabaseLockActivity() {
|
||||
listener: (icon: IconImage) -> Unit): ActivityResultLauncher<Intent> {
|
||||
return context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
listener.invoke(result.data?.getParcelableExtra(EXTRA_ICON) ?: IconImage())
|
||||
listener.invoke(result.data?.getParcelableExtraCompat(EXTRA_ICON) ?: IconImage())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,9 +33,10 @@ 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.database.element.Database
|
||||
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||
import kotlin.math.max
|
||||
|
||||
class ImageViewerActivity : DatabaseLockActivity() {
|
||||
@@ -100,12 +101,12 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
try {
|
||||
progressView.visibility = View.VISIBLE
|
||||
intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
|
||||
intent.getParcelableExtraCompat<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
|
||||
|
||||
supportActionBar?.title = attachment.name
|
||||
|
||||
|
||||
@@ -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,59 +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 com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||
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 : DatabaseModeActivity() {
|
||||
|
||||
override fun applyCustomStyle(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun finishActivityIfReloadRequested(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
database,
|
||||
null,
|
||||
{ _, _ ->
|
||||
// Not called
|
||||
// if items found directly returns before calling this activity
|
||||
},
|
||||
{ openedDatabase ->
|
||||
// Select if not found
|
||||
GroupActivity.launchForKeyboardSelectionResult(this, openedDatabase)
|
||||
},
|
||||
{
|
||||
// Pass extra to get entry
|
||||
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this)
|
||||
}
|
||||
)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
@@ -34,11 +34,13 @@ 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
|
||||
@@ -52,23 +54,31 @@ 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.database.element.Database
|
||||
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
|
||||
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
|
||||
@@ -79,6 +89,8 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
// 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
|
||||
@@ -96,6 +108,8 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
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
|
||||
|
||||
@@ -116,6 +130,8 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
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)
|
||||
@@ -127,11 +143,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
PreferencesUtil.enableReadOnlyDatabase(this)
|
||||
}
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this)
|
||||
|
||||
mExternalFileHelper = ExternalFileHelper(this@MainCredentialActivity)
|
||||
// Build elements to manage keyfile selection
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
if (uri != null) {
|
||||
mainCredentialView?.populateKeyFileTextView(uri)
|
||||
mainCredentialView?.populateKeyFileView(uri)
|
||||
}
|
||||
}
|
||||
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
|
||||
@@ -142,6 +160,11 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
// 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?
|
||||
@@ -160,6 +183,16 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
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 ->
|
||||
@@ -193,10 +226,19 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
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)
|
||||
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri, hardwareKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +246,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
super.onResume()
|
||||
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity)
|
||||
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this@MainCredentialActivity)
|
||||
|
||||
// Back to previous keyboard is setting activated
|
||||
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@MainCredentialActivity)) {
|
||||
@@ -224,15 +267,24 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: 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: Database,
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
@@ -246,99 +298,93 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
launchGroupActivityIfLoaded(database)
|
||||
} else {
|
||||
mainCredentialView?.requestPasswordFocus()
|
||||
// Manage special exceptions
|
||||
when (result.exception) {
|
||||
is DuplicateUuidDatabaseException -> {
|
||||
// Relaunch loading if we need to fix UUID
|
||||
showLoadDatabaseDuplicateUuidMessage {
|
||||
|
||||
var resultError = ""
|
||||
val resultException = result.exception
|
||||
val resultMessage = result.message
|
||||
var databaseUri: Uri? = null
|
||||
var mainCredential = MainCredential()
|
||||
var readOnly = true
|
||||
var cipherEncryptDatabase: CipherEncryptDatabase? = null
|
||||
|
||||
if (resultException != null) {
|
||||
resultError = resultException.getLocalizedMessage(resources)
|
||||
|
||||
when (resultException) {
|
||||
is DuplicateUuidDatabaseException -> {
|
||||
// Relaunch loading if we need to fix UUID
|
||||
showLoadDatabaseDuplicateUuidMessage {
|
||||
|
||||
var databaseUri: Uri? = null
|
||||
var mainCredential = MainCredential()
|
||||
var readOnly = true
|
||||
var cipherEncryptDatabase: CipherEncryptDatabase? = null
|
||||
|
||||
result.data?.let { resultData ->
|
||||
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
||||
mainCredential =
|
||||
resultData.getParcelable(MAIN_CREDENTIAL_KEY)
|
||||
?: mainCredential
|
||||
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
||||
cipherEncryptDatabase =
|
||||
resultData.getParcelable(CIPHER_DATABASE_KEY)
|
||||
}
|
||||
|
||||
databaseUri?.let { databaseFileUri ->
|
||||
showProgressDialogAndLoadDatabase(
|
||||
databaseFileUri,
|
||||
mainCredential,
|
||||
readOnly,
|
||||
cipherEncryptDatabase,
|
||||
true
|
||||
)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
is FileNotFoundDatabaseException -> {
|
||||
// Remove this default database inaccessible
|
||||
if (mDefaultDatabase) {
|
||||
mDatabaseFileViewModel.removeDefaultDatabase()
|
||||
|
||||
databaseUri?.let { databaseFileUri ->
|
||||
showProgressDialogAndLoadDatabase(
|
||||
databaseFileUri,
|
||||
mainCredential,
|
||||
readOnly,
|
||||
cipherEncryptDatabase,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is FileNotFoundDatabaseException -> {
|
||||
// Remove this default database inaccessible
|
||||
if (mDefaultDatabase) {
|
||||
mDatabaseFileViewModel.removeDefaultDatabase()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show error message
|
||||
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
||||
resultError = "$resultError $resultMessage"
|
||||
}
|
||||
Log.e(TAG, resultError)
|
||||
Snackbar.make(
|
||||
coordinatorLayout,
|
||||
resultError,
|
||||
Snackbar.LENGTH_LONG
|
||||
).asError().show()
|
||||
}
|
||||
}
|
||||
}
|
||||
coordinatorLayout.showActionErrorIfNeeded(result)
|
||||
}
|
||||
|
||||
private fun getUriFromIntent(intent: Intent?) {
|
||||
// If is a view intent
|
||||
val action = intent?.action
|
||||
if (action != null
|
||||
&& action == VIEW_INTENT) {
|
||||
mDatabaseFileUri = intent.data
|
||||
mainCredentialView?.populateKeyFileTextView(UriUtil.getUriFromIntent(intent, KEY_KEYFILE))
|
||||
if (action == VIEW_INTENT) {
|
||||
fillCredentials(
|
||||
intent.data,
|
||||
intent.getUri(KEY_KEYFILE),
|
||||
HardwareKey.getHardwareKeyFromString(intent.getStringExtra(KEY_HARDWARE_KEY))
|
||||
)
|
||||
} else {
|
||||
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
|
||||
intent?.getParcelableExtra<Uri?>(KEY_KEYFILE)?.let {
|
||||
mainCredentialView?.populateKeyFileTextView(it)
|
||||
}
|
||||
fillCredentials(
|
||||
intent?.getParcelableExtraCompat(KEY_FILENAME),
|
||||
intent?.getParcelableExtraCompat(KEY_KEYFILE),
|
||||
HardwareKey.getHardwareKeyFromString(intent?.getStringExtra(KEY_HARDWARE_KEY))
|
||||
)
|
||||
}
|
||||
try {
|
||||
intent?.removeExtra(KEY_KEYFILE)
|
||||
} catch (e: Exception) {}
|
||||
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: Database) {
|
||||
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
||||
// Check if database really loaded
|
||||
if (database.loaded) {
|
||||
clearCredentialsViews(true)
|
||||
clearCredentialsViews(clearKeyFile = true, clearHardwareKey = true)
|
||||
GroupActivity.launch(this,
|
||||
database,
|
||||
{ onValidateSpecialMode() },
|
||||
@@ -349,16 +395,6 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
}
|
||||
}
|
||||
|
||||
override fun onValidateSpecialMode() {
|
||||
super.onValidateSpecialMode()
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onCancelSpecialMode() {
|
||||
super.onCancelSpecialMode()
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun retrieveCredentialForEncryption(): ByteArray {
|
||||
return mainCredentialView?.retrieveCredentialForStorage(credentialStorageListener)
|
||||
?: byteArrayOf()
|
||||
@@ -398,7 +434,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
|
||||
when (cipherDecryptDatabase.credentialStorage) {
|
||||
CredentialStorage.PASSWORD -> {
|
||||
mainCredential.masterPassword = String(cipherDecryptDatabase.decryptedValue)
|
||||
mainCredential.password = String(cipherDecryptDatabase.decryptedValue)
|
||||
}
|
||||
CredentialStorage.KEY_FILE -> {
|
||||
// TODO advanced unlock key file
|
||||
@@ -413,14 +449,23 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
)
|
||||
}
|
||||
|
||||
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
||||
private fun onDatabaseFileLoaded(databaseFileUri: Uri?,
|
||||
keyFileUri: Uri?,
|
||||
hardwareKey: HardwareKey?) {
|
||||
// Define Key File text
|
||||
if (mRememberKeyFile) {
|
||||
mainCredentialView?.populateKeyFileTextView(keyFileUri)
|
||||
mainCredentialView?.populateKeyFileView(keyFileUri)
|
||||
}
|
||||
|
||||
// Define hardware key
|
||||
if (mRememberHardwareKey) {
|
||||
mainCredentialView?.populateHardwareKeyView(hardwareKey)
|
||||
}
|
||||
|
||||
// Define listener for validate button
|
||||
confirmButtonView?.setOnClickListener { loadDatabase() }
|
||||
confirmButtonView?.setOnClickListener {
|
||||
mainCredentialView?.validateCredential()
|
||||
}
|
||||
|
||||
// If Activity is launch with a password and want to open directly
|
||||
val intent = intent
|
||||
@@ -452,10 +497,14 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
|
||||
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile,
|
||||
clearHardwareKey: Boolean = !mRememberHardwareKey) {
|
||||
mainCredentialView?.populatePasswordTextView(null)
|
||||
if (clearKeyFile) {
|
||||
mainCredentialView?.populateKeyFileTextView(null)
|
||||
mainCredentialView?.populateKeyFileView(null)
|
||||
}
|
||||
if (clearHardwareKey) {
|
||||
mainCredentialView?.populateHardwareKeyView(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,15 +637,29 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
{
|
||||
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
|
||||
)
|
||||
)
|
||||
},
|
||||
{
|
||||
|
||||
advancedUnlockFragment?.performEducation(mPasswordActivityEducation,
|
||||
readOnlyEducationPerformed,
|
||||
{
|
||||
performedNextEducation(menu)
|
||||
},
|
||||
{
|
||||
performedNextEducation(menu)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (ignored: Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -632,18 +695,24 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
|
||||
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?,
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -656,8 +725,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launch(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
}
|
||||
@@ -672,8 +742,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
fun launchForSearchResult(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
searchInfo: SearchInfo) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||
activity,
|
||||
intent,
|
||||
@@ -691,8 +762,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
fun launchForSaveResult(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
searchInfo: SearchInfo) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForSaveModeResult(
|
||||
activity,
|
||||
intent,
|
||||
@@ -710,8 +782,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
fun launchForKeyboardResult(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
searchInfo: SearchInfo?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
|
||||
activity,
|
||||
intent,
|
||||
@@ -730,10 +803,11 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
AutofillHelper.startActivityForAutofillResult(
|
||||
activity,
|
||||
intent,
|
||||
@@ -751,8 +825,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
fun launchForRegistration(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
registerInfo: RegisterInfo?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||
activity,
|
||||
intent,
|
||||
@@ -768,6 +843,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
fun launch(activity: AppCompatActivity,
|
||||
databaseUri: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
||||
onCancelSpecialMode: () -> Unit,
|
||||
onLaunchActivitySpecialMode: () -> Unit,
|
||||
@@ -776,43 +852,67 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
try {
|
||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||
{
|
||||
MainCredentialActivity.launch(activity,
|
||||
databaseUri, keyFile)
|
||||
launch(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey
|
||||
)
|
||||
},
|
||||
{ searchInfo -> // Search Action
|
||||
MainCredentialActivity.launchForSearchResult(activity,
|
||||
databaseUri, keyFile,
|
||||
searchInfo)
|
||||
launchForSearchResult(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo -> // Save Action
|
||||
MainCredentialActivity.launchForSaveResult(activity,
|
||||
databaseUri, keyFile,
|
||||
searchInfo)
|
||||
launchForSaveResult(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo -> // Keyboard Selection Action
|
||||
MainCredentialActivity.launchForKeyboardResult(activity,
|
||||
databaseUri, keyFile,
|
||||
searchInfo)
|
||||
launchForKeyboardResult(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo, autofillComponent -> // Autofill Selection Action
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
MainCredentialActivity.launchForAutofillResult(activity,
|
||||
databaseUri, keyFile,
|
||||
autofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo)
|
||||
launchForAutofillResult(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
autofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
} else {
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
},
|
||||
{ registerInfo -> // Registration Action
|
||||
MainCredentialActivity.launchForRegistration(activity,
|
||||
databaseUri, keyFile,
|
||||
registerInfo)
|
||||
launchForRegistration(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
registerInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -25,6 +25,7 @@ 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() {
|
||||
@@ -40,8 +41,8 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
|
||||
val oldSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelable(OLD_FILE_DATABASE_INFO)
|
||||
val newSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelable(NEW_FILE_DATABASE_INFO)
|
||||
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
|
||||
@@ -78,7 +79,8 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
||||
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
|
||||
|
||||
fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||
newSnapFileDatabaseInfo: SnapFileDatabaseInfo)
|
||||
newSnapFileDatabaseInfo: SnapFileDatabaseInfo
|
||||
)
|
||||
: DatabaseChangedDialogFragment {
|
||||
val fragment = DatabaseChangedDialogFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
|
||||
@@ -5,7 +5,7 @@ import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
@@ -13,7 +13,7 @@ import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
||||
|
||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||
private var mDatabase: Database? = null
|
||||
private var mDatabase: ContextualDatabase? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -36,12 +36,12 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
||||
resetAppTimeoutOnTouchOrFocus()
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
// Can be overridden by a subclass
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
|
||||
@@ -1,67 +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
|
||||
|
||||
// Not as DatabaseDialogFragment because crash on KitKat
|
||||
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 onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ 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() {
|
||||
@@ -72,7 +73,7 @@ class EntryCustomFieldDialogFragment: DatabaseDialogFragment() {
|
||||
customFieldDeleteButton = root?.findViewById(R.id.entry_custom_field_delete)
|
||||
customFieldProtectionButton = root?.findViewById(R.id.entry_custom_field_protection)
|
||||
|
||||
oldField = arguments?.getParcelable(KEY_FIELD)
|
||||
oldField = arguments?.getParcelableCompat(KEY_FIELD)
|
||||
oldField?.let { oldCustomField ->
|
||||
customFieldLabel?.text = oldCustomField.name
|
||||
customFieldProtectionButton?.isChecked = oldCustomField.protectedValue.isProtected
|
||||
|
||||
@@ -21,12 +21,12 @@ package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||
|
||||
class FileManagerDialogFragment : DialogFragment() {
|
||||
|
||||
@@ -42,7 +42,7 @@ class FileManagerDialogFragment : DialogFragment() {
|
||||
textDescription.text = getString(R.string.file_manager_install_description)
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ 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
|
||||
@@ -62,7 +63,7 @@ class FileTooBigDialogFragment : DialogFragment() {
|
||||
})
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mActionChooseListener?.onValidateUploadFileTooBig(
|
||||
arguments?.getParcelable(KEY_FILE_URI),
|
||||
arguments?.getParcelableCompat(KEY_FILE_URI),
|
||||
arguments?.getString(KEY_FILE_NAME))
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
|
||||
@@ -1,224 +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 android.view.View
|
||||
import android.widget.*
|
||||
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.password.PasswordGenerator
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||
|
||||
class GeneratePasswordDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
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 mPasswordField: Field? = 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 onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
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()
|
||||
val passwordCopyView: ImageView? = root?.findViewById(R.id.password_copy_button)
|
||||
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(activity))
|
||||
View.VISIBLE else View.GONE
|
||||
val clipboardHelper = ClipboardHelper(activity)
|
||||
passwordCopyView?.setOnClickListener {
|
||||
clipboardHelper.timeoutCopyToClipboard(passwordView!!.text.toString(),
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_password)))
|
||||
}
|
||||
|
||||
lengthTextView = root?.findViewById(R.id.length)
|
||||
|
||||
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)
|
||||
|
||||
mPasswordField = arguments?.getParcelable(KEY_PASSWORD_FIELD)
|
||||
|
||||
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) { _, _ ->
|
||||
mPasswordField?.let { passwordField ->
|
||||
passwordView?.text?.toString()?.let { passwordValue ->
|
||||
passwordField.protectedValue.stringValue = passwordValue
|
||||
}
|
||||
mListener?.acceptPassword(passwordField)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
mPasswordField?.let { passwordField ->
|
||||
mListener?.cancelPassword(passwordField)
|
||||
}
|
||||
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(passwordField: Field)
|
||||
fun cancelPassword(passwordField: Field)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_PASSWORD_FIELD = "KEY_PASSWORD_FIELD"
|
||||
|
||||
fun getInstance(field: Field): GeneratePasswordDialogFragment {
|
||||
return GeneratePasswordDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(KEY_PASSWORD_FIELD, field)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,11 +31,13 @@ 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.element.Database
|
||||
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() {
|
||||
@@ -60,7 +62,7 @@ class GroupDialogFragment : DatabaseDialogFragment() {
|
||||
private lateinit var uuidContainerView: ViewGroup
|
||||
private lateinit var uuidReferenceView: TextView
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
mPopulateIconMethod = { imageView, icon ->
|
||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||
@@ -106,17 +108,17 @@ class GroupDialogFragment : DatabaseDialogFragment() {
|
||||
uuidReferenceView = root.findViewById(R.id.group_UUID_reference)
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val ta = activity.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
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.getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
||||
mGroupInfo = savedInstanceState.getParcelableCompat(KEY_GROUP_INFO) ?: mGroupInfo
|
||||
} else {
|
||||
arguments?.apply {
|
||||
if (containsKey(KEY_GROUP_INFO)) {
|
||||
mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
||||
mGroupInfo = getParcelableCompat(KEY_GROUP_INFO) ?: mGroupInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,17 +24,22 @@ import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import android.widget.Button
|
||||
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.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.*
|
||||
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.adapters.TagsProposalAdapter
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
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.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
|
||||
@@ -85,13 +90,11 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
||||
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
||||
}
|
||||
|
||||
mGroupEditViewModel.onDateSelected.observe(this) { viewModelDate ->
|
||||
mGroupEditViewModel.onDateSelected.observe(this) { dateMilliseconds ->
|
||||
// Save the date
|
||||
mGroupInfo.expiryTime = DateInstant(
|
||||
DateTime(mGroupInfo.expiryTime.date)
|
||||
.withYear(viewModelDate.year)
|
||||
.withMonthOfYear(viewModelDate.month + 1)
|
||||
.withDayOfMonth(viewModelDate.day)
|
||||
.withMillis(dateMilliseconds)
|
||||
.toDate())
|
||||
expirationView.dateTime = mGroupInfo.expiryTime
|
||||
if (expirationView.dateTime.type == DateInstant.Type.DATE_TIME) {
|
||||
@@ -116,7 +119,7 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
mPopulateIconMethod = { imageView, icon ->
|
||||
@@ -170,13 +173,13 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
||||
&& savedInstanceState.containsKey(KEY_ACTION_ID)
|
||||
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
|
||||
mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID))
|
||||
mGroupInfo = savedInstanceState.getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
||||
mGroupInfo = savedInstanceState.getParcelableCompat(KEY_GROUP_INFO) ?: mGroupInfo
|
||||
} else {
|
||||
arguments?.apply {
|
||||
if (containsKey(KEY_ACTION_ID))
|
||||
mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID))
|
||||
if (containsKey(KEY_GROUP_INFO)) {
|
||||
mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
||||
mGroupInfo = getParcelableCompat(KEY_GROUP_INFO) ?: mGroupInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,11 @@ 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.element.Database
|
||||
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() {
|
||||
@@ -44,7 +45,7 @@ class IconEditDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mCustomIcon: IconImageCustom? = null
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
mPopulateIconMethod = { imageView, icon ->
|
||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon)
|
||||
@@ -63,11 +64,11 @@ class IconEditDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(KEY_CUSTOM_ICON_ID)) {
|
||||
mCustomIcon = savedInstanceState.getParcelable(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
|
||||
mCustomIcon = savedInstanceState.getParcelableCompat(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
|
||||
} else {
|
||||
arguments?.apply {
|
||||
if (containsKey(KEY_CUSTOM_ICON_ID)) {
|
||||
mCustomIcon = getParcelable(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
|
||||
mCustomIcon = getParcelableCompat(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,9 @@ 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.model.MainCredential
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
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() {
|
||||
@@ -65,7 +66,7 @@ class MainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
var databaseUri: Uri? = null
|
||||
arguments?.apply {
|
||||
if (containsKey(KEY_ASK_CREDENTIAL_URI))
|
||||
databaseUri = getParcelable(KEY_ASK_CREDENTIAL_URI)
|
||||
databaseUri = getParcelableCompat(KEY_ASK_CREDENTIAL_URI)
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
@@ -74,7 +75,7 @@ class MainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
mainCredentialView = root.findViewById(R.id.main_credential_view)
|
||||
databaseUri?.let {
|
||||
root.findViewById<TextView>(R.id.title_database)?.text =
|
||||
UriUtil.getFileData(requireContext(), it)?.name
|
||||
it.getDocumentFile(requireContext())?.name
|
||||
}
|
||||
builder.setView(root)
|
||||
// Add action buttons
|
||||
@@ -95,7 +96,7 @@ class MainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
if (uri != null) {
|
||||
mainCredentialView?.populateKeyFileTextView(uri)
|
||||
mainCredentialView?.populateKeyFileView(uri)
|
||||
}
|
||||
}
|
||||
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
|
||||
|
||||
@@ -26,7 +26,8 @@ import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
|
||||
class PasswordEncodingDialogFragment : DialogFragment() {
|
||||
|
||||
@@ -49,8 +50,8 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
||||
val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY)
|
||||
val mainCredential: MainCredential = savedInstanceState?.getParcelable(MAIN_CREDENTIAL) ?: MainCredential()
|
||||
val databaseUri: Uri? = savedInstanceState?.getParcelableCompat(DATABASE_URI_KEY)
|
||||
val mainCredential: MainCredential = savedInstanceState?.getParcelableCompat(MAIN_CREDENTIAL) ?: MainCredential()
|
||||
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
@@ -78,8 +79,10 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
||||
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
|
||||
private const val MAIN_CREDENTIAL = "MAIN_CREDENTIAL"
|
||||
|
||||
fun getInstance(databaseUri: Uri,
|
||||
mainCredential: MainCredential): SortDialogFragment {
|
||||
fun getInstance(
|
||||
databaseUri: Uri,
|
||||
mainCredential: MainCredential
|
||||
): SortDialogFragment {
|
||||
val fragment = SortDialogFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
putParcelable(DATABASE_URI_KEY, databaseUri)
|
||||
|
||||
@@ -28,7 +28,7 @@ import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.BuildConfig
|
||||
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.
|
||||
@@ -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_buy_pro), FROM_HTML_MODE_LEGACY))
|
||||
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 {
|
||||
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))
|
||||
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||
UriUtil.gotoUrl(requireContext(), R.string.contribution_url)
|
||||
activity.openUrl(R.string.contribution_url)
|
||||
}
|
||||
}
|
||||
builder.setMessage(stringBuilder)
|
||||
|
||||
@@ -27,6 +27,7 @@ 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
|
||||
@@ -62,8 +63,8 @@ class ReplaceFileDialogFragment : DatabaseDialogFragment() {
|
||||
})
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mActionChooseListener?.onValidateReplaceFile(
|
||||
arguments?.getParcelable(KEY_FILE_URI),
|
||||
arguments?.getParcelable(KEY_ENTRY_ATTACHMENT))
|
||||
arguments?.getParcelableCompat(KEY_FILE_URI),
|
||||
arguments?.getParcelableCompat(KEY_ENTRY_ATTACHMENT))
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
dismiss()
|
||||
|
||||
@@ -35,42 +35,54 @@ 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.model.MainCredential
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
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 mKeyFile: Uri? = null
|
||||
private var mKeyFileUri: Uri? = null
|
||||
private var mHardwareKey: HardwareKey? = null
|
||||
|
||||
private var rootView: View? = null
|
||||
private lateinit var rootView: View
|
||||
|
||||
private var passwordCheckBox: CompoundButton? = null
|
||||
private lateinit var passwordCheckBox: CompoundButton
|
||||
private lateinit var passwordView: PassKeyView
|
||||
private lateinit var passwordRepeatTextInputLayout: TextInputLayout
|
||||
private lateinit var passwordRepeatView: TextView
|
||||
|
||||
private var passwordTextInputLayout: TextInputLayout? = null
|
||||
private var passwordView: TextView? = null
|
||||
private var passwordRepeatTextInputLayout: TextInputLayout? = null
|
||||
private var passwordRepeatView: TextView? = null
|
||||
private lateinit var keyFileCheckBox: CompoundButton
|
||||
private lateinit var keyFileSelectionView: KeyFileSelectionView
|
||||
|
||||
private var keyFileCheckBox: CompoundButton? = null
|
||||
private var keyFileSelectionView: KeyFileSelectionView? = null
|
||||
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
|
||||
passwordCheckBox.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,13 +112,19 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
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 ->
|
||||
|
||||
var allowNoMasterKey = false
|
||||
arguments?.apply {
|
||||
if (containsKey(ALLOW_NO_MASTER_KEY_ARG))
|
||||
allowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false)
|
||||
mAllowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false)
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
@@ -118,63 +136,63 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
|
||||
rootView?.findViewById<View>(R.id.credentials_information)?.setOnClickListener {
|
||||
UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
|
||||
rootView.findViewById<View>(R.id.credentials_information)?.setOnClickListener {
|
||||
activity.openUrl(R.string.credentials_explanation_url)
|
||||
}
|
||||
|
||||
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
||||
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
|
||||
passwordView = rootView?.findViewById(R.id.pass_password)
|
||||
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
|
||||
passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password)
|
||||
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_checkox)
|
||||
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
||||
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 ->
|
||||
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
||||
keyFileSelectionView?.error = null
|
||||
keyFileCheckBox?.isChecked = true
|
||||
keyFileSelectionView?.uri = pathUri
|
||||
pathUri.getDocumentFile(requireContext())?.length()?.let { lengthFile ->
|
||||
keyFileSelectionView.error = null
|
||||
keyFileCheckBox.isChecked = true
|
||||
keyFileSelectionView.uri = pathUri
|
||||
if (lengthFile <= 0L) {
|
||||
showEmptyKeyFileConfirmationDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||
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 {
|
||||
|
||||
if (passwordCheckBox != null && keyFileCheckBox!= null) {
|
||||
dialog.setOnShowListener { dialog1 ->
|
||||
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
positiveButton.setOnClickListener {
|
||||
mMasterPassword = ""
|
||||
mKeyFileUri = null
|
||||
mHardwareKey = null
|
||||
|
||||
mMasterPassword = ""
|
||||
mKeyFile = null
|
||||
|
||||
var error = verifyPassword() || verifyKeyFile()
|
||||
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
|
||||
error = true
|
||||
if (allowNoMasterKey)
|
||||
showNoKeyConfirmationDialog()
|
||||
else {
|
||||
passwordTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
|
||||
}
|
||||
}
|
||||
if (!error) {
|
||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
|
||||
negativeButton.setOnClickListener {
|
||||
mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
|
||||
dismiss()
|
||||
}
|
||||
approveMainCredential()
|
||||
}
|
||||
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
|
||||
negativeButton.setOnClickListener {
|
||||
mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,67 +202,113 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
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 keyFile = if (keyFileCheckBox!!.isChecked) mKeyFile else null
|
||||
return MainCredential(masterPassword, keyFile)
|
||||
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)
|
||||
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())
|
||||
&& (keyFileCheckBox == null
|
||||
|| !keyFileCheckBox!!.isChecked
|
||||
|| keyFileSelectionView?.uri == null)) {
|
||||
error = true
|
||||
showEmptyPasswordConfirmationDialog()
|
||||
}
|
||||
}
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
private fun verifyKeyFile(): 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
|
||||
passwordView.removeTextChangedListener(passwordTextWatcher)
|
||||
}
|
||||
|
||||
private fun showEmptyPasswordConfirmationDialog() {
|
||||
@@ -252,10 +316,8 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
val builder = AlertDialog.Builder(it)
|
||||
builder.setMessage(R.string.warning_empty_password)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
if (!verifyKeyFile()) {
|
||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||
this@SetMainCredentialDialogFragment.dismiss()
|
||||
}
|
||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||
this@SetMainCredentialDialogFragment.dismiss()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
mEmptyPasswordConfirmationDialog = builder.create()
|
||||
@@ -289,8 +351,8 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
})
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
keyFileCheckBox?.isChecked = false
|
||||
keyFileSelectionView?.uri = null
|
||||
keyFileCheckBox.isChecked = false
|
||||
keyFileSelectionView.uri = null
|
||||
}
|
||||
mEmptyKeyFileConfirmationDialog = builder.create()
|
||||
mEmptyKeyFileConfirmationDialog?.show()
|
||||
|
||||
@@ -32,7 +32,6 @@ import android.view.inputmethod.EditorInfo
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.BuildConfig
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.model.OtpModel
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
@@ -45,7 +44,9 @@ import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
|
||||
import com.kunzisoft.keepass.otp.OtpTokenType
|
||||
import com.kunzisoft.keepass.otp.OtpType
|
||||
import com.kunzisoft.keepass.otp.TokenCalculator
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import java.util.*
|
||||
|
||||
class SetOTPDialogFragment : DatabaseDialogFragment() {
|
||||
@@ -126,14 +127,14 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
||||
// Retrieve OTP model from instance state
|
||||
if (savedInstanceState != null) {
|
||||
if (savedInstanceState.containsKey(KEY_OTP)) {
|
||||
savedInstanceState.getParcelable<OtpModel>(KEY_OTP)?.let { otpModel ->
|
||||
savedInstanceState.getParcelableCompat<OtpModel>(KEY_OTP)?.let { otpModel ->
|
||||
mOtpElement = OtpElement(otpModel)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
arguments?.apply {
|
||||
if (containsKey(KEY_OTP)) {
|
||||
getParcelable<OtpModel?>(KEY_OTP)?.let { otpModel ->
|
||||
getParcelableCompat<OtpModel>(KEY_OTP)?.let { otpModel ->
|
||||
mOtpElement = OtpElement(otpModel)
|
||||
}
|
||||
}
|
||||
@@ -206,7 +207,7 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
||||
}
|
||||
// Proprietary only on full version
|
||||
mTotpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
|
||||
UriUtil.contributingUser(activity)
|
||||
activity.isContributingUser()
|
||||
)
|
||||
totpTokenTypeAdapter = ArrayAdapter(activity,
|
||||
android.R.layout.simple_spinner_item, mTotpTokenTypeArray!!).apply {
|
||||
@@ -242,7 +243,7 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
||||
}
|
||||
|
||||
root?.findViewById<View>(R.id.otp_information)?.setOnClickListener {
|
||||
UriUtil.gotoUrl(activity, R.string.otp_explanation_url)
|
||||
activity.openUrl(R.string.otp_explanation_url)
|
||||
}
|
||||
|
||||
return builder.create()
|
||||
@@ -470,4 +471,4 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +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
|
||||
|
||||
// Not as DatabaseDialogFragment because crash on KitKat
|
||||
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 onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
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.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
class UnavailableFeatureDialogFragment : DialogFragment() {
|
||||
|
||||
@@ -26,7 +26,7 @@ import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
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.
|
||||
@@ -39,21 +39,22 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
val stringBuilder = SpannableStringBuilder()
|
||||
if (UriUtil.contributingUser(activity)) {
|
||||
/*
|
||||
if (activity.isContributingUser()) {
|
||||
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_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(" ")
|
||||
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_contibute), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||
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)
|
||||
// Create the AlertDialog object and return it
|
||||
return builder.create()
|
||||
|
||||
@@ -2,19 +2,19 @@ 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.activities.stylish.StylishFragment
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
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 : StylishFragment(), DatabaseRetrieval {
|
||||
abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
|
||||
|
||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||
protected var mDatabase: Database? = null
|
||||
protected var mDatabase: ContextualDatabase? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
@@ -38,7 +38,7 @@ abstract class DatabaseFragment : StylishFragment(), DatabaseRetrieval {
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
|
||||
@@ -35,14 +35,20 @@ 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.Database
|
||||
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.view.*
|
||||
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
|
||||
|
||||
@@ -71,12 +77,11 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
val taIconColor = context?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
mIconColor = taIconColor?.getColor(0, Color.BLACK) ?: Color.BLACK
|
||||
taIconColor?.recycle()
|
||||
|
||||
return inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.fragment_entry_edit, container, false)
|
||||
return inflater.inflate(R.layout.fragment_entry_edit, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View,
|
||||
@@ -124,7 +129,7 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
val attachments: List<Attachment> =
|
||||
savedInstanceState.getParcelableArrayList(ATTACHMENTS_TAG) ?: listOf()
|
||||
savedInstanceState.getParcelableList(ATTACHMENTS_TAG) ?: listOf()
|
||||
setAttachments(attachments)
|
||||
}
|
||||
|
||||
@@ -268,7 +273,7 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
|
||||
templateView.populateIconMethod = { imageView, icon ->
|
||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||
@@ -379,7 +384,7 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putParcelableArrayList(ATTACHMENTS_TAG, ArrayList(getAttachments()))
|
||||
outState.putParcelableList(ATTACHMENTS_TAG, getAttachments())
|
||||
}
|
||||
|
||||
/* -------------
|
||||
|
||||
@@ -14,24 +14,28 @@ 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.Database
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
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
|
||||
import java.util.*
|
||||
|
||||
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
|
||||
@@ -55,8 +59,7 @@ class EntryFragment: DatabaseFragment() {
|
||||
savedInstanceState: Bundle?): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
return inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.fragment_entry, container, false)
|
||||
return inflater.inflate(R.layout.fragment_entry, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View,
|
||||
@@ -72,6 +75,10 @@ class EntryFragment: DatabaseFragment() {
|
||||
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()
|
||||
|
||||
@@ -111,9 +118,22 @@ class EntryFragment: DatabaseFragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: Database?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
context?.let { context ->
|
||||
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
|
||||
attachmentsAdapter?.database = database
|
||||
@@ -138,11 +158,9 @@ class EntryFragment: DatabaseFragment() {
|
||||
|
||||
setOnCopyActionClickListener { field ->
|
||||
mClipboardHelper?.timeoutCopyToClipboard(
|
||||
TemplateField.getLocalizedName(context, field.name),
|
||||
field.protectedValue.stringValue,
|
||||
getString(
|
||||
R.string.copy_field,
|
||||
TemplateField.getLocalizedName(context, field.name)
|
||||
)
|
||||
field.protectedValue.isProtected
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -231,8 +249,7 @@ class EntryFragment: DatabaseFragment() {
|
||||
|
||||
fun launchEntryCopyEducationAction() {
|
||||
val appNameString = getString(R.string.app_name)
|
||||
mClipboardHelper?.timeoutCopyToClipboard(appNameString,
|
||||
getString(R.string.copy_field, appNameString))
|
||||
mClipboardHelper?.timeoutCopyToClipboard(appNameString, appNameString)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -4,16 +4,16 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||
|
||||
class EntryHistoryFragment: StylishFragment() {
|
||||
class EntryHistoryFragment: Fragment() {
|
||||
|
||||
private lateinit var historyContainerView: View
|
||||
private lateinit var historyListView: RecyclerView
|
||||
@@ -28,8 +28,7 @@ class EntryHistoryFragment: StylishFragment() {
|
||||
): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
return inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.fragment_entry_history, container, false)
|
||||
return inflater.inflate(R.layout.fragment_entry_history, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -22,27 +22,33 @@ package com.kunzisoft.keepass.activities.fragments
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
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 androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.EntryEditActivity
|
||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.adapters.NodesAdapter
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.KeyboardUtil.hideKeyboard
|
||||
import com.kunzisoft.keepass.viewmodels.GroupViewModel
|
||||
import java.util.*
|
||||
import java.util.LinkedList
|
||||
|
||||
class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListener {
|
||||
|
||||
@@ -73,19 +79,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
private var mRecycleBinEnable: Boolean = false
|
||||
private var mRecycleBin: Group? = null
|
||||
|
||||
var mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) { entryId ->
|
||||
entryId?.let {
|
||||
// Simply refresh the list
|
||||
rebuildList()
|
||||
// Scroll to the new entry
|
||||
mDatabase?.getEntryById(it)?.let { entry ->
|
||||
mAdapter?.indexOf(entry)?.let { position ->
|
||||
mNodesRecyclerView?.scrollToPosition(position)
|
||||
}
|
||||
}
|
||||
} ?: Log.e(this.javaClass.name, "Entry cannot be retrieved in Activity Result")
|
||||
}
|
||||
|
||||
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
super.onScrollStateChanged(recyclerView, newState)
|
||||
@@ -99,6 +92,40 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
}
|
||||
}
|
||||
|
||||
private val menuProvider: MenuProvider = object: MenuProvider {
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.tree, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
return when (menuItem.itemId) {
|
||||
R.id.menu_sort -> {
|
||||
context?.let { context ->
|
||||
val sortDialogFragment: SortDialogFragment =
|
||||
if (mRecycleBinEnable) {
|
||||
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")
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
|
||||
@@ -137,23 +164,16 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
mRecycleBinEnable = database?.isRecycleBinEnabled == true
|
||||
mRecycleBin = database?.recycleBin
|
||||
|
||||
contextThemed?.let { context ->
|
||||
context?.let { context ->
|
||||
database?.let { database ->
|
||||
mAdapter = NodesAdapter(context, database).apply {
|
||||
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
|
||||
override fun onNodeClick(database: Database, node: Node) {
|
||||
if (mCurrentGroup?.isVirtual == false
|
||||
&& nodeActionSelectionMode) {
|
||||
override fun onNodeClick(database: ContextualDatabase, node: Node) {
|
||||
if (nodeActionSelectionMode) {
|
||||
if (listActionNodes.contains(node)) {
|
||||
// Remove selected item if already selected
|
||||
listActionNodes.remove(node)
|
||||
@@ -169,9 +189,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNodeLongClick(database: Database, node: Node): Boolean {
|
||||
if (mCurrentGroup?.isVirtual == false
|
||||
&& nodeActionPasteMode == PasteMode.UNDEFINED) {
|
||||
override fun onNodeLongClick(database: ContextualDatabase, node: Node): Boolean {
|
||||
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
|
||||
// Select the first item after a long click
|
||||
if (!listActionNodes.contains(node))
|
||||
listActionNodes.add(node)
|
||||
@@ -180,6 +199,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
|
||||
setActionNodes(listActionNodes)
|
||||
notifyNodeChanged(node)
|
||||
activity?.hideKeyboard()
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -191,7 +211,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
@@ -207,13 +227,14 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
// To apply theme
|
||||
return inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.fragment_nodes, container, false)
|
||||
return inflater.inflate(R.layout.fragment_nodes, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
|
||||
|
||||
mNodesRecyclerView = view.findViewById(R.id.nodes_list)
|
||||
notFoundView = view.findViewById(R.id.not_found_container)
|
||||
|
||||
@@ -242,8 +263,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
activity?.intent?.let {
|
||||
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
|
||||
}
|
||||
|
||||
rebuildList()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -293,43 +312,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
}
|
||||
}
|
||||
|
||||
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 (mRecycleBinEnable) {
|
||||
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(database: Database,
|
||||
fun actionNodesCallback(database: ContextualDatabase,
|
||||
nodes: List<Node>,
|
||||
menuListener: NodesActionMenuListener?,
|
||||
onDestroyActionMode: (mode: ActionMode?) -> Unit) : ActionMode.Callback {
|
||||
@@ -363,14 +346,12 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
}
|
||||
|
||||
// Move
|
||||
if (database.isReadOnly
|
||||
|| isASearchResult) {
|
||||
if (database.isReadOnly) {
|
||||
menu?.removeItem(R.id.menu_move)
|
||||
}
|
||||
|
||||
// Copy (not allowed for group)
|
||||
if (database.isReadOnly
|
||||
|| isASearchResult
|
||||
|| nodes.any { it.type == Type.GROUP }) {
|
||||
menu?.removeItem(R.id.menu_copy)
|
||||
}
|
||||
@@ -433,20 +414,20 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
||||
* Callback listener to redefine to do an action when a node is click
|
||||
*/
|
||||
interface NodeClickListener {
|
||||
fun onNodeClick(database: Database, node: Node)
|
||||
fun onNodeSelected(database: Database, nodes: List<Node>): Boolean
|
||||
fun onNodeClick(database: ContextualDatabase, node: Node)
|
||||
fun onNodeSelected(database: ContextualDatabase, nodes: List<Node>): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu listener to redefine to do an action in menu
|
||||
*/
|
||||
interface NodesActionMenuListener {
|
||||
fun onOpenMenuClick(database: Database, node: Node): Boolean
|
||||
fun onEditMenuClick(database: Database, node: Node): Boolean
|
||||
fun onCopyMenuClick(database: Database, nodes: List<Node>): Boolean
|
||||
fun onMoveMenuClick(database: Database, nodes: List<Node>): Boolean
|
||||
fun onDeleteMenuClick(database: Database, nodes: List<Node>): Boolean
|
||||
fun onPasteMenuClick(database: Database, pasteMode: PasteMode?, nodes: List<Node>): Boolean
|
||||
fun onOpenMenuClick(database: ContextualDatabase, node: Node): Boolean
|
||||
fun onEditMenuClick(database: ContextualDatabase, node: Node): Boolean
|
||||
fun onCopyMenuClick(database: ContextualDatabase, nodes: List<Node>): Boolean
|
||||
fun onMoveMenuClick(database: ContextualDatabase, nodes: List<Node>): Boolean
|
||||
fun onDeleteMenuClick(database: ContextualDatabase, nodes: List<Node>): Boolean
|
||||
fun onPasteMenuClick(database: ContextualDatabase, pasteMode: PasteMode?, nodes: List<Node>): Boolean
|
||||
}
|
||||
|
||||
enum class PasteMode {
|
||||
|
||||
@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.activities.fragments
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
|
||||
return R.layout.fragment_icon_grid
|
||||
}
|
||||
|
||||
override fun defineIconList(database: Database?) {
|
||||
override fun defineIconList(database: ContextualDatabase?) {
|
||||
database?.doForEachCustomIcons { customIcon, _ ->
|
||||
iconPickerAdapter.addIcon(customIcon, false)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.adapters.IconPickerAdapter
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
|
||||
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -47,7 +47,7 @@ abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
|
||||
|
||||
abstract fun retrieveMainLayoutId(): Int
|
||||
|
||||
abstract fun defineIconList(database: Database?)
|
||||
abstract fun defineIconList(database: ContextualDatabase?)
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@@ -59,7 +59,7 @@ abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val ta = contextThemed?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
val ta = context?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
val tintColor = ta?.getColor(0, Color.BLACK) ?: Color.BLACK
|
||||
ta?.recycle()
|
||||
|
||||
@@ -71,7 +71,7 @@ abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
|
||||
@@ -10,7 +10,7 @@ import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.adapters.IconPickerPagerAdapter
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||
|
||||
class IconPickerFragment : DatabaseFragment() {
|
||||
@@ -26,14 +26,14 @@ class IconPickerFragment : DatabaseFragment() {
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_icon_picker, container, false)
|
||||
return inflater.inflate(R.layout.fragment_tabs_pagination, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
viewPager = view.findViewById(R.id.icon_picker_pager)
|
||||
tabLayout = view.findViewById(R.id.icon_picker_tabs)
|
||||
viewPager = view.findViewById(R.id.tabs_view_pager)
|
||||
tabLayout = view.findViewById(R.id.tabs_layout)
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||
|
||||
arguments?.apply {
|
||||
@@ -48,7 +48,7 @@ class IconPickerFragment : DatabaseFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
|
||||
if (database?.allowCustomIcons == true) 2 else 1)
|
||||
viewPager.adapter = iconPickerPagerAdapter
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
package com.kunzisoft.keepass.activities.fragments
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class IconStandardFragment : IconFragment<IconImageStandard>() {
|
||||
return R.layout.fragment_icon_grid
|
||||
}
|
||||
|
||||
override fun defineIconList(database: Database?) {
|
||||
override fun defineIconList(database: ContextualDatabase?) {
|
||||
database?.doForEachStandardIcons { standardIcon ->
|
||||
iconPickerAdapter.addIcon(standardIcon, false)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* 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.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.adapters.KeyGeneratorPagerAdapter
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
||||
|
||||
class KeyGeneratorFragment : DatabaseFragment() {
|
||||
|
||||
private var keyGeneratorPagerAdapter: KeyGeneratorPagerAdapter? = null
|
||||
private lateinit var viewPager: ViewPager2
|
||||
private lateinit var tabLayout: TabLayout
|
||||
|
||||
private val mKeyGeneratorViewModel: KeyGeneratorViewModel by activityViewModels()
|
||||
|
||||
private var mSelectedTab = KeyGeneratorTab.PASSWORD
|
||||
private var mOnPageChangeCallback: ViewPager2.OnPageChangeCallback = object:
|
||||
ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
mSelectedTab = KeyGeneratorTab.getKeyGeneratorTabByPosition(position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_tabs_pagination, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
keyGeneratorPagerAdapter = KeyGeneratorPagerAdapter(this, )
|
||||
viewPager = view.findViewById(R.id.tabs_view_pager)
|
||||
tabLayout = view.findViewById(R.id.tabs_layout)
|
||||
viewPager.adapter = keyGeneratorPagerAdapter
|
||||
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
|
||||
tab.text = getString(KeyGeneratorTab.getKeyGeneratorTabByPosition(position).stringId)
|
||||
}.attach()
|
||||
viewPager.registerOnPageChangeCallback(mOnPageChangeCallback)
|
||||
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||
|
||||
arguments?.apply {
|
||||
if (containsKey(PASSWORD_TAB_ARG)) {
|
||||
viewPager.currentItem = getInt(PASSWORD_TAB_ARG)
|
||||
}
|
||||
remove(PASSWORD_TAB_ARG)
|
||||
}
|
||||
|
||||
mKeyGeneratorViewModel.requireKeyGeneration.observe(viewLifecycleOwner) {
|
||||
when (mSelectedTab) {
|
||||
KeyGeneratorTab.PASSWORD -> {
|
||||
mKeyGeneratorViewModel.requirePasswordGeneration()
|
||||
}
|
||||
KeyGeneratorTab.PASSPHRASE -> {
|
||||
mKeyGeneratorViewModel.requirePassphraseGeneration()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mKeyGeneratorViewModel.keyGeneratedValidated.observe(viewLifecycleOwner) {
|
||||
when (mSelectedTab) {
|
||||
KeyGeneratorTab.PASSWORD -> {
|
||||
mKeyGeneratorViewModel.validatePasswordGenerated()
|
||||
}
|
||||
KeyGeneratorTab.PASSPHRASE -> {
|
||||
mKeyGeneratorViewModel.validatePassphraseGenerated()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
viewPager.unregisterOnPageChangeCallback(mOnPageChangeCallback)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
// Nothing here
|
||||
}
|
||||
|
||||
enum class KeyGeneratorTab(@StringRes val stringId: Int) {
|
||||
PASSWORD(R.string.password), PASSPHRASE(R.string.passphrase);
|
||||
|
||||
companion object {
|
||||
fun getKeyGeneratorTabByPosition(position: Int): KeyGeneratorTab {
|
||||
return when (position) {
|
||||
0 -> PASSWORD
|
||||
else -> PASSPHRASE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val PASSWORD_TAB_ARG = "PASSWORD_TAB_ARG"
|
||||
|
||||
fun getInstance(keyGeneratorTab: KeyGeneratorTab): KeyGeneratorFragment {
|
||||
val fragment = KeyGeneratorFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
putInt(PASSWORD_TAB_ARG, keyGeneratorTab.ordinal)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* 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.fragments
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.password.PassphraseGenerator
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.view.PassKeyView
|
||||
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
||||
|
||||
class PassphraseGeneratorFragment : DatabaseFragment() {
|
||||
|
||||
private lateinit var passKeyView: PassKeyView
|
||||
|
||||
private lateinit var sliderWordCount: Slider
|
||||
private lateinit var wordCountText: EditText
|
||||
private lateinit var charactersCountText: TextView
|
||||
private lateinit var wordSeparator: EditText
|
||||
private lateinit var wordCaseSpinner: Spinner
|
||||
|
||||
private var minSliderWordCount: Int = 0
|
||||
private var maxSliderWordCount: Int = 0
|
||||
private var wordCaseAdapter: ArrayAdapter<String>? = null
|
||||
|
||||
private val mKeyGeneratorViewModel: KeyGeneratorViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View {
|
||||
return inflater.inflate(R.layout.fragment_generate_passphrase, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
passKeyView = view.findViewById(R.id.passphrase_view)
|
||||
val passphraseCopyView: ImageView? = view.findViewById(R.id.passphrase_copy_button)
|
||||
sliderWordCount = view.findViewById(R.id.slider_word_count)
|
||||
wordCountText = view.findViewById(R.id.word_count)
|
||||
charactersCountText = view.findViewById(R.id.character_count)
|
||||
wordSeparator = view.findViewById(R.id.word_separator)
|
||||
wordCaseSpinner = view.findViewById(R.id.word_case)
|
||||
|
||||
minSliderWordCount = resources.getInteger(R.integer.passphrase_generator_word_count_min)
|
||||
maxSliderWordCount = resources.getInteger(R.integer.passphrase_generator_word_count_max)
|
||||
|
||||
context?.let { context ->
|
||||
passphraseCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(context))
|
||||
View.VISIBLE else View.GONE
|
||||
val clipboardHelper = ClipboardHelper(context)
|
||||
passphraseCopyView?.setOnClickListener {
|
||||
clipboardHelper.timeoutCopyToClipboard(
|
||||
getString(R.string.passphrase),
|
||||
passKeyView.passwordString,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
wordCaseAdapter = ArrayAdapter(context,
|
||||
android.R.layout.simple_spinner_item, resources.getStringArray(R.array.word_case_array)).apply {
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
}
|
||||
wordCaseSpinner.adapter = wordCaseAdapter
|
||||
}
|
||||
|
||||
loadSettings()
|
||||
|
||||
var listenSlider = true
|
||||
var listenEditText = true
|
||||
sliderWordCount.addOnChangeListener { _, value, _ ->
|
||||
try {
|
||||
listenEditText = false
|
||||
if (listenSlider) {
|
||||
wordCountText.setText(value.toInt().toString())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to set the word count value", e)
|
||||
} finally {
|
||||
listenEditText = true
|
||||
}
|
||||
}
|
||||
sliderWordCount.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
|
||||
// TODO upgrade material-components lib
|
||||
// https://stackoverflow.com/questions/70873160/material-slider-onslidertouchlisteners-methods-can-only-be-called-from-within-t
|
||||
@SuppressLint("RestrictedApi")
|
||||
override fun onStartTrackingTouch(slider: Slider) {}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
override fun onStopTrackingTouch(slider: Slider) {
|
||||
generatePassphrase()
|
||||
}
|
||||
})
|
||||
wordCountText.doOnTextChanged { _, _, _, _ ->
|
||||
if (listenEditText) {
|
||||
try {
|
||||
listenSlider = false
|
||||
setSliderValue(getWordCount())
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to get the word count value", e)
|
||||
} finally {
|
||||
listenSlider = true
|
||||
generatePassphrase()
|
||||
}
|
||||
}
|
||||
}
|
||||
wordSeparator.doOnTextChanged { _, _, _, _ ->
|
||||
generatePassphrase()
|
||||
}
|
||||
wordCaseSpinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
generatePassphrase()
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
}
|
||||
|
||||
generatePassphrase()
|
||||
|
||||
mKeyGeneratorViewModel.passphraseGeneratedValidated.observe(viewLifecycleOwner) {
|
||||
mKeyGeneratorViewModel.setKeyGenerated(passKeyView.passwordString)
|
||||
}
|
||||
|
||||
mKeyGeneratorViewModel.requirePassphraseGeneration.observe(viewLifecycleOwner) {
|
||||
generatePassphrase()
|
||||
}
|
||||
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||
}
|
||||
|
||||
private fun getWordCount(): Int {
|
||||
return try {
|
||||
Integer.valueOf(wordCountText.text.toString())
|
||||
} catch (numberException: NumberFormatException) {
|
||||
minSliderWordCount
|
||||
}
|
||||
}
|
||||
|
||||
private fun setWordCount(wordCount: Int) {
|
||||
setSliderValue(wordCount)
|
||||
wordCountText.setText(wordCount.toString())
|
||||
}
|
||||
|
||||
private fun setSliderValue(value: Int) {
|
||||
when {
|
||||
value < minSliderWordCount -> {
|
||||
sliderWordCount.value = minSliderWordCount.toFloat()
|
||||
}
|
||||
value > maxSliderWordCount -> {
|
||||
sliderWordCount.value = maxSliderWordCount.toFloat()
|
||||
}
|
||||
else -> {
|
||||
sliderWordCount.value = value.toFloat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getWordSeparator(): String {
|
||||
return wordSeparator.text.toString().ifEmpty { " " }
|
||||
}
|
||||
|
||||
private fun getWordCase(): PassphraseGenerator.WordCase {
|
||||
var wordCase = PassphraseGenerator.WordCase.LOWER_CASE
|
||||
try {
|
||||
wordCase = PassphraseGenerator.WordCase.getByOrdinal(wordCaseSpinner.selectedItemPosition)
|
||||
} catch (caseException: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve the word case", caseException)
|
||||
}
|
||||
return wordCase
|
||||
}
|
||||
|
||||
private fun setWordCase(wordCase: PassphraseGenerator.WordCase) {
|
||||
wordCaseSpinner.setSelection(wordCase.ordinal)
|
||||
}
|
||||
|
||||
private fun getSeparator(): String {
|
||||
return wordSeparator.text?.toString() ?: ""
|
||||
}
|
||||
|
||||
private fun setSeparator(separator: String) {
|
||||
wordSeparator.setText(separator)
|
||||
}
|
||||
|
||||
private fun generatePassphrase() {
|
||||
var passphrase = ""
|
||||
try {
|
||||
passphrase = PassphraseGenerator().generatePassphrase(
|
||||
getWordCount(),
|
||||
getWordSeparator(),
|
||||
getWordCase())
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to generate a passphrase", e)
|
||||
}
|
||||
passKeyView.passwordString = passphrase
|
||||
charactersCountText.text = getString(R.string.character_count, passphrase.length)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
saveSettings()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun saveSettings() {
|
||||
context?.let { context ->
|
||||
PreferencesUtil.setDefaultPassphraseWordCount(context, getWordCount())
|
||||
PreferencesUtil.setDefaultPassphraseWordCase(context, getWordCase())
|
||||
PreferencesUtil.setDefaultPassphraseSeparator(context, getSeparator())
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadSettings() {
|
||||
context?.let { context ->
|
||||
setWordCount(PreferencesUtil.getDefaultPassphraseWordCount(context))
|
||||
setWordCase(PreferencesUtil.getDefaultPassphraseWordCase(context))
|
||||
setSeparator(PreferencesUtil.getDefaultPassphraseSeparator(context))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
// Nothing here
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PassphraseGnrtrFrgmt"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
/*
|
||||
* 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.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.password.PasswordGenerator
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.view.PassKeyView
|
||||
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
||||
|
||||
class PasswordGeneratorFragment : DatabaseFragment() {
|
||||
|
||||
private lateinit var passKeyView: PassKeyView
|
||||
|
||||
private lateinit var sliderLength: Slider
|
||||
private lateinit var lengthEditView: EditText
|
||||
|
||||
private lateinit var uppercaseCompound: CompoundButton
|
||||
private lateinit var lowercaseCompound: CompoundButton
|
||||
private lateinit var digitsCompound: CompoundButton
|
||||
private lateinit var minusCompound: CompoundButton
|
||||
private lateinit var underlineCompound: CompoundButton
|
||||
private lateinit var spaceCompound: CompoundButton
|
||||
private lateinit var specialsCompound: CompoundButton
|
||||
private lateinit var bracketsCompound: CompoundButton
|
||||
private lateinit var extendedCompound: CompoundButton
|
||||
private lateinit var considerCharsEditText: EditText
|
||||
private lateinit var ignoreCharsEditText: EditText
|
||||
private lateinit var atLeastOneCompound: CompoundButton
|
||||
private lateinit var excludeAmbiguousCompound: CompoundButton
|
||||
|
||||
private var minLengthSlider: Int = 0
|
||||
private var maxLengthSlider: Int = 0
|
||||
|
||||
private val mKeyGeneratorViewModel: KeyGeneratorViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View {
|
||||
return inflater.inflate(R.layout.fragment_generate_password, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
passKeyView = view.findViewById(R.id.password_view)
|
||||
val passwordCopyView: ImageView? = view.findViewById(R.id.password_copy_button)
|
||||
|
||||
sliderLength = view.findViewById(R.id.slider_length)
|
||||
lengthEditView = view.findViewById(R.id.length)
|
||||
|
||||
uppercaseCompound = view.findViewById(R.id.upperCase_filter)
|
||||
lowercaseCompound = view.findViewById(R.id.lowerCase_filter)
|
||||
digitsCompound = view.findViewById(R.id.digits_filter)
|
||||
minusCompound = view.findViewById(R.id.minus_filter)
|
||||
underlineCompound = view.findViewById(R.id.underline_filter)
|
||||
spaceCompound = view.findViewById(R.id.space_filter)
|
||||
specialsCompound = view.findViewById(R.id.special_filter)
|
||||
bracketsCompound = view.findViewById(R.id.brackets_filter)
|
||||
extendedCompound = view.findViewById(R.id.extendedASCII_filter)
|
||||
considerCharsEditText = view.findViewById(R.id.consider_chars_filter)
|
||||
ignoreCharsEditText = view.findViewById(R.id.ignore_chars_filter)
|
||||
atLeastOneCompound = view.findViewById(R.id.atLeastOne_filter)
|
||||
excludeAmbiguousCompound = view.findViewById(R.id.excludeAmbiguous_filter)
|
||||
|
||||
context?.let { context ->
|
||||
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(context))
|
||||
View.VISIBLE else View.GONE
|
||||
val clipboardHelper = ClipboardHelper(context)
|
||||
passwordCopyView?.setOnClickListener {
|
||||
clipboardHelper.timeoutCopyToClipboard(
|
||||
getString(R.string.password),
|
||||
passKeyView.passwordString,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
minLengthSlider = resources.getInteger(R.integer.password_generator_length_min)
|
||||
maxLengthSlider = resources.getInteger(R.integer.password_generator_length_max)
|
||||
|
||||
loadSettings()
|
||||
|
||||
uppercaseCompound.setOnCheckedChangeListener { _, _ ->
|
||||
generatePassword()
|
||||
}
|
||||
lowercaseCompound.setOnCheckedChangeListener { _, _ ->
|
||||
generatePassword()
|
||||
}
|
||||
digitsCompound.setOnCheckedChangeListener { _, _ ->
|
||||
generatePassword()
|
||||
}
|
||||
minusCompound.setOnCheckedChangeListener { _, _ ->
|
||||
generatePassword()
|
||||
}
|
||||
underlineCompound.setOnCheckedChangeListener { _, _ ->
|
||||
generatePassword()
|
||||
}
|
||||
spaceCompound.setOnCheckedChangeListener { _, _ ->
|
||||
generatePassword()
|
||||
}
|
||||
specialsCompound.setOnCheckedChangeListener { _, _ ->
|
||||
generatePassword()
|
||||
}
|
||||
bracketsCompound.setOnCheckedChangeListener { _, _ ->
|
||||
generatePassword()
|
||||
}
|
||||
extendedCompound.setOnCheckedChangeListener { _, _ ->
|
||||
generatePassword()
|
||||
}
|
||||
considerCharsEditText.doOnTextChanged { _, _, _, _ ->
|
||||
generatePassword()
|
||||
}
|
||||
ignoreCharsEditText.doOnTextChanged { _, _, _, _ ->
|
||||
generatePassword()
|
||||
}
|
||||
atLeastOneCompound.setOnCheckedChangeListener { _, _ ->
|
||||
generatePassword()
|
||||
}
|
||||
excludeAmbiguousCompound.setOnCheckedChangeListener { _, _ ->
|
||||
generatePassword()
|
||||
}
|
||||
|
||||
var listenSlider = true
|
||||
var listenEditText = true
|
||||
sliderLength.addOnChangeListener { _, value, _ ->
|
||||
try {
|
||||
listenEditText = false
|
||||
if (listenSlider) {
|
||||
lengthEditView.setText(value.toInt().toString())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to set the length value", e)
|
||||
} finally {
|
||||
listenEditText = true
|
||||
}
|
||||
}
|
||||
sliderLength.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
|
||||
// TODO upgrade material-components lib
|
||||
// https://stackoverflow.com/questions/70873160/material-slider-onslidertouchlisteners-methods-can-only-be-called-from-within-t
|
||||
@SuppressLint("RestrictedApi")
|
||||
override fun onStartTrackingTouch(slider: Slider) {}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
override fun onStopTrackingTouch(slider: Slider) {
|
||||
generatePassword()
|
||||
}
|
||||
})
|
||||
lengthEditView.doOnTextChanged { _, _, _, _ ->
|
||||
if (listenEditText) {
|
||||
try {
|
||||
listenSlider = false
|
||||
setSliderValue(getPasswordLength())
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to get the length value", e)
|
||||
} finally {
|
||||
listenSlider = true
|
||||
generatePassword()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-populate a password to possibly save the user a few clicks
|
||||
generatePassword()
|
||||
|
||||
mKeyGeneratorViewModel.passwordGeneratedValidated.observe(viewLifecycleOwner) {
|
||||
mKeyGeneratorViewModel.setKeyGenerated(passKeyView.passwordString)
|
||||
}
|
||||
|
||||
mKeyGeneratorViewModel.requirePasswordGeneration.observe(viewLifecycleOwner) {
|
||||
generatePassword()
|
||||
}
|
||||
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||
}
|
||||
|
||||
private fun getPasswordLength(): Int {
|
||||
return try {
|
||||
Integer.valueOf(lengthEditView.text.toString())
|
||||
} catch (numberException: NumberFormatException) {
|
||||
minLengthSlider
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPasswordLength(passwordLength: Int) {
|
||||
setSliderValue(passwordLength)
|
||||
lengthEditView.setText(passwordLength.toString())
|
||||
}
|
||||
|
||||
private fun getOptions(): Set<String> {
|
||||
val optionsSet = mutableSetOf<String>()
|
||||
if (uppercaseCompound.isChecked)
|
||||
optionsSet.add(getString(R.string.value_password_uppercase))
|
||||
if (lowercaseCompound.isChecked)
|
||||
optionsSet.add(getString(R.string.value_password_lowercase))
|
||||
if (digitsCompound.isChecked)
|
||||
optionsSet.add(getString(R.string.value_password_digits))
|
||||
if (minusCompound.isChecked)
|
||||
optionsSet.add(getString(R.string.value_password_minus))
|
||||
if (underlineCompound.isChecked)
|
||||
optionsSet.add(getString(R.string.value_password_underline))
|
||||
if (spaceCompound.isChecked)
|
||||
optionsSet.add(getString(R.string.value_password_space))
|
||||
if (specialsCompound.isChecked)
|
||||
optionsSet.add(getString(R.string.value_password_special))
|
||||
if (bracketsCompound.isChecked)
|
||||
optionsSet.add(getString(R.string.value_password_brackets))
|
||||
if (extendedCompound.isChecked)
|
||||
optionsSet.add(getString(R.string.value_password_extended))
|
||||
if (atLeastOneCompound.isChecked)
|
||||
optionsSet.add(getString(R.string.value_password_atLeastOne))
|
||||
if (excludeAmbiguousCompound.isChecked)
|
||||
optionsSet.add(getString(R.string.value_password_excludeAmbiguous))
|
||||
return optionsSet
|
||||
}
|
||||
|
||||
private fun setOptions(options: Set<String>) {
|
||||
uppercaseCompound.isChecked = false
|
||||
lowercaseCompound.isChecked = false
|
||||
digitsCompound.isChecked = false
|
||||
minusCompound.isChecked = false
|
||||
underlineCompound.isChecked = false
|
||||
spaceCompound.isChecked = false
|
||||
specialsCompound.isChecked = false
|
||||
bracketsCompound.isChecked = false
|
||||
extendedCompound.isChecked = false
|
||||
atLeastOneCompound.isChecked = false
|
||||
excludeAmbiguousCompound.isChecked = false
|
||||
for (option in options) {
|
||||
when (option) {
|
||||
getString(R.string.value_password_uppercase) -> uppercaseCompound.isChecked = true
|
||||
getString(R.string.value_password_lowercase) -> lowercaseCompound.isChecked = true
|
||||
getString(R.string.value_password_digits) -> digitsCompound.isChecked = true
|
||||
getString(R.string.value_password_minus) -> minusCompound.isChecked = true
|
||||
getString(R.string.value_password_underline) -> underlineCompound.isChecked = true
|
||||
getString(R.string.value_password_space) -> spaceCompound.isChecked = true
|
||||
getString(R.string.value_password_special) -> specialsCompound.isChecked = true
|
||||
getString(R.string.value_password_brackets) -> bracketsCompound.isChecked = true
|
||||
getString(R.string.value_password_extended) -> extendedCompound.isChecked = true
|
||||
getString(R.string.value_password_atLeastOne) -> atLeastOneCompound.isChecked = true
|
||||
getString(R.string.value_password_excludeAmbiguous) -> excludeAmbiguousCompound.isChecked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConsiderChars(): String {
|
||||
return considerCharsEditText.text.toString()
|
||||
}
|
||||
|
||||
private fun setConsiderChars(chars: String) {
|
||||
considerCharsEditText.setText(chars)
|
||||
}
|
||||
|
||||
private fun getIgnoreChars(): String {
|
||||
return ignoreCharsEditText.text.toString()
|
||||
}
|
||||
|
||||
private fun setIgnoreChars(chars: String) {
|
||||
ignoreCharsEditText.setText(chars)
|
||||
}
|
||||
|
||||
private fun generatePassword() {
|
||||
var password = ""
|
||||
try {
|
||||
password = PasswordGenerator(resources).generatePassword(getPasswordLength(),
|
||||
uppercaseCompound.isChecked,
|
||||
lowercaseCompound.isChecked,
|
||||
digitsCompound.isChecked,
|
||||
minusCompound.isChecked,
|
||||
underlineCompound.isChecked,
|
||||
spaceCompound.isChecked,
|
||||
specialsCompound.isChecked,
|
||||
bracketsCompound.isChecked,
|
||||
extendedCompound.isChecked,
|
||||
getConsiderChars(),
|
||||
getIgnoreChars(),
|
||||
atLeastOneCompound.isChecked,
|
||||
excludeAmbiguousCompound.isChecked)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to generate a password", e)
|
||||
}
|
||||
passKeyView.passwordString = password
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
saveSettings()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
// Nothing here
|
||||
}
|
||||
|
||||
private fun saveSettings() {
|
||||
context?.let { context ->
|
||||
PreferencesUtil.setDefaultPasswordOptions(context, getOptions())
|
||||
PreferencesUtil.setDefaultPasswordLength(context, getPasswordLength())
|
||||
PreferencesUtil.setDefaultPasswordConsiderChars(context, getConsiderChars())
|
||||
PreferencesUtil.setDefaultPasswordIgnoreChars(context, getIgnoreChars())
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadSettings() {
|
||||
context?.let { context ->
|
||||
setOptions(PreferencesUtil.getDefaultPasswordOptions(context))
|
||||
setPasswordLength(PreferencesUtil.getDefaultPasswordLength(context))
|
||||
setConsiderChars(PreferencesUtil.getDefaultPasswordConsiderChars(context))
|
||||
setIgnoreChars(PreferencesUtil.getDefaultPasswordIgnoreChars(context))
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSliderValue(value: Int) {
|
||||
when {
|
||||
value < minLengthSlider -> {
|
||||
sliderLength.value = minLengthSlider.toFloat()
|
||||
}
|
||||
value > maxLengthSlider -> {
|
||||
sliderLength.value = maxLengthSlider.toFloat()
|
||||
}
|
||||
else -> {
|
||||
sliderLength.value = value.toFloat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PasswordGeneratorFrgmt"
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,9 @@ import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import java.io.Serializable
|
||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||
import com.kunzisoft.keepass.utils.getEnumExtra
|
||||
import com.kunzisoft.keepass.utils.putEnumExtra
|
||||
|
||||
object EntrySelectionHelper {
|
||||
|
||||
@@ -82,17 +84,17 @@ object EntrySelectionHelper {
|
||||
}
|
||||
|
||||
fun retrieveSearchInfoFromIntent(intent: Intent): SearchInfo? {
|
||||
return intent.getParcelableExtra(KEY_SEARCH_INFO)
|
||||
return intent.getParcelableExtraCompat(KEY_SEARCH_INFO)
|
||||
}
|
||||
|
||||
fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
|
||||
private fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
|
||||
registerInfo?.let {
|
||||
intent.putExtra(KEY_REGISTER_INFO, it)
|
||||
}
|
||||
}
|
||||
|
||||
fun retrieveRegisterInfoFromIntent(intent: Intent): RegisterInfo? {
|
||||
return intent.getParcelableExtra(KEY_REGISTER_INFO)
|
||||
return intent.getParcelableExtraCompat(KEY_REGISTER_INFO)
|
||||
}
|
||||
|
||||
fun removeInfoFromIntent(intent: Intent) {
|
||||
@@ -101,7 +103,7 @@ object EntrySelectionHelper {
|
||||
}
|
||||
|
||||
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
|
||||
intent.putExtra(KEY_SPECIAL_MODE, specialMode as Serializable)
|
||||
intent.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
|
||||
}
|
||||
|
||||
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
|
||||
@@ -109,12 +111,11 @@ object EntrySelectionHelper {
|
||||
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
|
||||
return SpecialMode.SELECTION
|
||||
}
|
||||
return intent.getSerializableExtra(KEY_SPECIAL_MODE) as SpecialMode?
|
||||
?: SpecialMode.DEFAULT
|
||||
return intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
|
||||
}
|
||||
|
||||
fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
|
||||
intent.putExtra(KEY_TYPE_MODE, typeMode as Serializable)
|
||||
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
|
||||
intent.putEnumExtra(KEY_TYPE_MODE, typeMode)
|
||||
}
|
||||
|
||||
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
|
||||
@@ -122,7 +123,7 @@ object EntrySelectionHelper {
|
||||
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
|
||||
return TypeMode.AUTOFILL
|
||||
}
|
||||
return intent.getSerializableExtra(KEY_TYPE_MODE) as TypeMode? ?: TypeMode.DEFAULT
|
||||
return intent.getEnumExtra<TypeMode>(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
|
||||
}
|
||||
|
||||
fun removeModesFromIntent(intent: Intent) {
|
||||
@@ -175,7 +176,7 @@ object EntrySelectionHelper {
|
||||
}
|
||||
}
|
||||
if (!autofillComponentInit) {
|
||||
if (intent.getSerializableExtra(KEY_SPECIAL_MODE) != null) {
|
||||
if (intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) != null) {
|
||||
when (retrieveTypeModeFromIntent(intent)) {
|
||||
TypeMode.DEFAULT -> {
|
||||
removeModesFromIntent(intent)
|
||||
|
||||
@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.activities.helpers
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
@@ -33,7 +32,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil.takeUriPermission
|
||||
|
||||
class ExternalFileHelper {
|
||||
|
||||
@@ -56,11 +55,9 @@ class ExternalFileHelper {
|
||||
|
||||
fun buildOpenDocument(onFileSelected: ((uri: Uri?) -> Unit)?) {
|
||||
|
||||
val resultCallback = ActivityResultCallback<Uri> { result ->
|
||||
result?.let { uri ->
|
||||
UriUtil.takeUriPermission(activity?.contentResolver, uri)
|
||||
onFileSelected?.invoke(uri)
|
||||
}
|
||||
val resultCallback = ActivityResultCallback<Uri?> { result ->
|
||||
activity?.contentResolver?.takeUriPermission(result)
|
||||
onFileSelected?.invoke(result)
|
||||
}
|
||||
|
||||
getContentResultLauncher = if (fragment != null) {
|
||||
@@ -91,7 +88,7 @@ class ExternalFileHelper {
|
||||
fun buildCreateDocument(typeString: String = "application/octet-stream",
|
||||
onFileCreated: (fileCreated: Uri?)->Unit) {
|
||||
|
||||
val resultCallback = ActivityResultCallback<Uri> { result ->
|
||||
val resultCallback = ActivityResultCallback<Uri?> { result ->
|
||||
onFileCreated.invoke(result)
|
||||
}
|
||||
|
||||
@@ -150,7 +147,7 @@ class ExternalFileHelper {
|
||||
|
||||
class OpenDocument : ActivityResultContracts.OpenDocument() {
|
||||
@SuppressLint("InlinedApi")
|
||||
override fun createIntent(context: Context, input: Array<out String>): Intent {
|
||||
override fun createIntent(context: Context, input: Array<String>): Intent {
|
||||
return super.createIntent(context, input).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
@@ -178,34 +175,17 @@ class ExternalFileHelper {
|
||||
}
|
||||
}
|
||||
|
||||
class CreateDocument(private val typeString: String) : ActivityResultContracts.CreateDocument() {
|
||||
class CreateDocument(typeString: String) : ActivityResultContracts.CreateDocument(typeString) {
|
||||
override fun createIntent(context: Context, input: String): Intent {
|
||||
return super.createIntent(context, input).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = typeString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "OpenFileHelper"
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
fun allowCreateDocumentByStorageAccessFramework(packageManager: PackageManager,
|
||||
typeString: String = "application/octet-stream"): Boolean {
|
||||
return when {
|
||||
// To check if a custom file manager can manage the ACTION_CREATE_DOCUMENT
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT -> {
|
||||
packageManager.queryIntentActivities(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = typeString
|
||||
}, PackageManager.MATCH_DEFAULT_ONLY).isNotEmpty()
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,23 +4,24 @@ import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.getBinaryDir
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
|
||||
abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
||||
|
||||
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
|
||||
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
||||
protected var mDatabase: Database? = null
|
||||
protected var mDatabase: ContextualDatabase? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
mDatabaseTaskProvider = DatabaseTaskProvider(this)
|
||||
mDatabaseTaskProvider = DatabaseTaskProvider(this, showDatabaseDialog())
|
||||
|
||||
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
||||
val databaseWasReloaded = database?.wasReloaded == true
|
||||
@@ -36,14 +37,25 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
protected open fun showDatabaseDialog(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
mDatabaseTaskProvider?.destroy()
|
||||
mDatabaseTaskProvider = null
|
||||
mDatabase = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
mDatabase = database
|
||||
mDatabaseViewModel.defineDatabase(database)
|
||||
// optional method implementation
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
@@ -51,21 +63,25 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
||||
// optional method implementation
|
||||
}
|
||||
|
||||
fun createDatabase(databaseUri: Uri,
|
||||
mainCredential: MainCredential) {
|
||||
fun createDatabase(
|
||||
databaseUri: Uri,
|
||||
mainCredential: MainCredential
|
||||
) {
|
||||
mDatabaseTaskProvider?.startDatabaseCreate(databaseUri, mainCredential)
|
||||
}
|
||||
|
||||
fun loadDatabase(databaseUri: Uri,
|
||||
mainCredential: MainCredential,
|
||||
readOnly: Boolean,
|
||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||
fixDuplicateUuid: Boolean) {
|
||||
fun loadDatabase(
|
||||
databaseUri: Uri,
|
||||
mainCredential: MainCredential,
|
||||
readOnly: Boolean,
|
||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||
fixDuplicateUuid: Boolean
|
||||
) {
|
||||
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEncryptDatabase, fixDuplicateUuid)
|
||||
}
|
||||
|
||||
protected fun closeDatabase() {
|
||||
mDatabase?.clearAndClose(this)
|
||||
mDatabase?.clearAndClose(this.getBinaryDir())
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
||||
@@ -24,24 +24,25 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
import com.kunzisoft.keepass.model.GroupInfo
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
@@ -65,8 +66,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
protected var mMergeDataAllowed: Boolean = false
|
||||
private var mAutoSaveEnable: Boolean = true
|
||||
|
||||
protected var mIconDrawableFactory: IconDrawableFactory? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -88,8 +87,8 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
mDatabaseTaskProvider?.startDatabaseSave(save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.mergeDatabase.observe(this) {
|
||||
mDatabaseTaskProvider?.startDatabaseMerge()
|
||||
mDatabaseViewModel.mergeDatabase.observe(this) { save ->
|
||||
mDatabaseTaskProvider?.startDatabaseMerge(save)
|
||||
}
|
||||
|
||||
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
|
||||
@@ -165,7 +164,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
// End activity if database not loaded
|
||||
@@ -205,16 +204,24 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
|
||||
mDatabaseReadOnly = database.isReadOnly
|
||||
mMergeDataAllowed = database.isMergeDataAllowed()
|
||||
mIconDrawableFactory = database.iconDrawableFactory
|
||||
|
||||
checkRegister()
|
||||
}
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
// To fix weird crash
|
||||
try {
|
||||
super.finish()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to finish the activity", e)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun viewToInvalidateTimeout(): View?
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
@@ -225,6 +232,9 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
// Reload the current activity
|
||||
if (result.isSuccess) {
|
||||
reloadActivity()
|
||||
if (actionTask == DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK) {
|
||||
Toast.makeText(this, R.string.merge_success, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} else {
|
||||
this.showActionErrorIfNeeded(result)
|
||||
finish()
|
||||
@@ -233,15 +243,19 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPasswordEncodingValidateListener(databaseUri: Uri?,
|
||||
mainCredential: MainCredential) {
|
||||
override fun onPasswordEncodingValidateListener(
|
||||
databaseUri: Uri?,
|
||||
mainCredential: MainCredential
|
||||
) {
|
||||
assignDatabasePassword(databaseUri, mainCredential)
|
||||
}
|
||||
|
||||
private fun assignDatabasePassword(databaseUri: Uri?,
|
||||
mainCredential: MainCredential) {
|
||||
private fun assignDatabasePassword(
|
||||
databaseUri: Uri?,
|
||||
mainCredential: MainCredential
|
||||
) {
|
||||
if (databaseUri != null) {
|
||||
mDatabaseTaskProvider?.startDatabaseAssignPassword(databaseUri, mainCredential)
|
||||
mDatabaseTaskProvider?.startDatabaseAssignCredential(databaseUri, mainCredential)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +263,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
mDatabase?.let { database ->
|
||||
database.fileUri?.let { databaseUri ->
|
||||
// Show the progress dialog now or after dialog confirmation
|
||||
if (database.validatePasswordEncoding(mainCredential)) {
|
||||
if (database.isValidCredential(mainCredential.toMasterCredential(contentResolver))) {
|
||||
assignDatabasePassword(databaseUri, mainCredential)
|
||||
} else {
|
||||
PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
|
||||
@@ -268,11 +282,11 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
}
|
||||
|
||||
fun mergeDatabase() {
|
||||
mDatabaseTaskProvider?.startDatabaseMerge()
|
||||
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable)
|
||||
}
|
||||
|
||||
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
|
||||
mDatabaseTaskProvider?.startDatabaseMerge(uri, mainCredential)
|
||||
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable, uri, mainCredential)
|
||||
}
|
||||
|
||||
fun reloadDatabase() {
|
||||
@@ -301,7 +315,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
mDatabaseTaskProvider?.startDatabaseMoveNodes(nodesToMove, newParent, mAutoSaveEnable)
|
||||
}
|
||||
|
||||
private fun eachNodeRecyclable(database: Database, nodes: List<Node>): Boolean {
|
||||
private fun eachNodeRecyclable(database: ContextualDatabase, nodes: List<Node>): Boolean {
|
||||
return nodes.find { node ->
|
||||
var cannotRecycle = true
|
||||
if (node is Entry) {
|
||||
@@ -317,7 +331,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
mDatabase?.let { database ->
|
||||
// If recycle bin enabled, ensure it exists
|
||||
if (database.isRecycleBinEnabled) {
|
||||
database.ensureRecycleBinExists(resources)
|
||||
database.ensureRecycleBinExists(resources.getString(R.string.recycle_bin))
|
||||
}
|
||||
|
||||
// If recycle bin enabled and not in recycle bin, move in recycle bin
|
||||
@@ -431,7 +445,17 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
}
|
||||
|
||||
protected fun lockAndExit() {
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
// Ask confirmation if modification not saved
|
||||
if (mDatabase?.dataModifiedSinceLastLoading == true) {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.discard_changes)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.lock) { _, _ ->
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
}.create().show()
|
||||
} else {
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
}
|
||||
}
|
||||
|
||||
fun resetAppTimeout() {
|
||||
@@ -439,14 +463,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
mDatabase?.loaded ?: false)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
override fun onDatabaseBackPressed() {
|
||||
if (mTimeoutEnable) {
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
|
||||
mDatabase?.loaded == true) {
|
||||
super.onBackPressed()
|
||||
super.onDatabaseBackPressed()
|
||||
}
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
super.onDatabaseBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,25 +491,33 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
fun View.resetAppTimeoutWhenViewTouchedOrFocused(context: Context, databaseLoaded: Boolean?) {
|
||||
// Log.d(DatabaseLockActivity.TAG, "View prepared to reset app timeout")
|
||||
setOnTouchListener { _, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
// Log.d(DatabaseLockActivity.TAG, "View touched, try to reset app timeout")
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context,
|
||||
databaseLoaded ?: false)
|
||||
try {
|
||||
// Log.d(DatabaseLockActivity.TAG, "View prepared to reset app timeout")
|
||||
setOnTouchListener { _, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
// Log.d(DatabaseLockActivity.TAG, "View touched, try to reset app timeout")
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(
|
||||
context,
|
||||
databaseLoaded ?: false
|
||||
)
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
setOnFocusChangeListener { _, _ ->
|
||||
// Log.d(DatabaseLockActivity.TAG, "View focused, try to reset app timeout")
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(
|
||||
context,
|
||||
databaseLoaded ?: false
|
||||
)
|
||||
}
|
||||
if (this is ViewGroup) {
|
||||
for (i in 0..childCount) {
|
||||
getChildAt(i)?.resetAppTimeoutWhenViewTouchedOrFocused(context, databaseLoaded)
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
setOnFocusChangeListener { _, _ ->
|
||||
// Log.d(DatabaseLockActivity.TAG, "View focused, try to reset app timeout")
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context,
|
||||
databaseLoaded ?: false)
|
||||
}
|
||||
if (this is ViewGroup) {
|
||||
for (i in 0..childCount) {
|
||||
getChildAt(i)?.resetAppTimeoutWhenViewTouchedOrFocused(context, databaseLoaded)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AppTimeout", "Unable to reset app timeout", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
package com.kunzisoft.keepass.activities.legacy
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.helpers.TypeMode
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.SpecialModeView
|
||||
import com.kunzisoft.keepass.view.ToolbarSpecial
|
||||
|
||||
|
||||
/**
|
||||
@@ -22,20 +21,22 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
|
||||
private var mTypeMode: TypeMode = TypeMode.DEFAULT
|
||||
|
||||
private var mSpecialModeView: SpecialModeView? = null
|
||||
private var mToolbarSpecial: ToolbarSpecial? = null
|
||||
|
||||
override fun onBackPressed() {
|
||||
open fun onDatabaseBackPressed() {
|
||||
if (mSpecialMode != SpecialMode.DEFAULT)
|
||||
onCancelSpecialMode()
|
||||
else
|
||||
super.onBackPressed()
|
||||
onRegularBackPressed()
|
||||
}
|
||||
|
||||
/**
|
||||
* To call the regular onBackPressed() method in special mode
|
||||
*/
|
||||
protected fun onRegularBackPressed() {
|
||||
super.onBackPressed()
|
||||
// Do not call onBackPressedDispatcher.onBackPressed() to avoid loop
|
||||
// Calling onBackPressed() is now deprecated, directly finish the activity
|
||||
finish()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,7 +75,7 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
open fun onCancelSpecialMode() {
|
||||
if (isIntentSender()) {
|
||||
// To get the app caller, only for IntentSender
|
||||
super.onBackPressed()
|
||||
onRegularBackPressed()
|
||||
} else {
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||
@@ -87,7 +88,7 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
protected fun backToTheAppCaller() {
|
||||
if (isIntentSender()) {
|
||||
// To get the app caller, only for IntentSender
|
||||
super.onBackPressed()
|
||||
onRegularBackPressed()
|
||||
} else {
|
||||
backToTheMainAppAndFinish()
|
||||
}
|
||||
@@ -96,15 +97,18 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
private fun backToTheMainAppAndFinish() {
|
||||
// To move the app in background and return to the main app
|
||||
moveTaskToBack(true)
|
||||
// To remove this instance in the OS app selector
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
finish()
|
||||
}, 500)
|
||||
// Not using FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS or finish() because kills the service
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
onDatabaseBackPressed()
|
||||
}
|
||||
})
|
||||
|
||||
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
||||
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
||||
}
|
||||
@@ -118,8 +122,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
||||
|
||||
// To show the selection mode
|
||||
mSpecialModeView = findViewById(R.id.special_mode_view)
|
||||
mSpecialModeView?.apply {
|
||||
mToolbarSpecial = findViewById(R.id.special_mode_view)
|
||||
mToolbarSpecial?.apply {
|
||||
// Populate title
|
||||
val selectionModeStringId = when (mSpecialMode) {
|
||||
SpecialMode.DEFAULT, // Not important because hidden
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.kunzisoft.keepass.activities.legacy
|
||||
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
interface DatabaseRetrieval {
|
||||
fun onDatabaseRetrieved(database: Database?)
|
||||
fun onDatabaseActionFinished(database: Database,
|
||||
fun onDatabaseRetrieved(database: ContextualDatabase?)
|
||||
fun onDatabaseActionFinished(database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result)
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.util.Log
|
||||
import androidx.annotation.StyleRes
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
|
||||
@@ -38,7 +39,7 @@ object Stylish {
|
||||
* @param context Context to retrieve the theme preference
|
||||
*/
|
||||
fun load(context: Context) {
|
||||
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
|
||||
Log.d(Stylish::class.java.name, "Attaching to " + context.packageName)
|
||||
try {
|
||||
themeString = PreferencesUtil.getStyle(context)
|
||||
} catch (e: Exception) {
|
||||
@@ -46,7 +47,7 @@ object Stylish {
|
||||
}
|
||||
}
|
||||
|
||||
fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {
|
||||
fun retrieveEquivalentSystemStyle(context: Context, styleString: String?): String {
|
||||
val systemNightMode = when (PreferencesUtil.getStyleBrightness(context)) {
|
||||
context.getString(R.string.list_style_brightness_light) -> false
|
||||
context.getString(R.string.list_style_brightness_night) -> true
|
||||
@@ -58,14 +59,21 @@ object Stylish {
|
||||
}
|
||||
}
|
||||
return if (systemNightMode) {
|
||||
retrieveEquivalentNightStyle(context, styleString)
|
||||
retrieveEquivalentNightStyle(
|
||||
context,
|
||||
styleString ?: context.getString(R.string.list_style_name_night)
|
||||
)
|
||||
} else {
|
||||
retrieveEquivalentLightStyle(context, styleString)
|
||||
retrieveEquivalentLightStyle(
|
||||
context,
|
||||
styleString ?: context.getString(R.string.list_style_name_light)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun retrieveEquivalentLightStyle(context: Context, styleString: String): String {
|
||||
return when (styleString) {
|
||||
context.getString(R.string.list_style_name_dynamic_night) -> context.getString(R.string.list_style_name_dynamic_light)
|
||||
context.getString(R.string.list_style_name_night) -> context.getString(R.string.list_style_name_light)
|
||||
context.getString(R.string.list_style_name_black) -> context.getString(R.string.list_style_name_white)
|
||||
context.getString(R.string.list_style_name_dark) -> context.getString(R.string.list_style_name_clear)
|
||||
@@ -80,6 +88,7 @@ object Stylish {
|
||||
|
||||
private fun retrieveEquivalentNightStyle(context: Context, styleString: String): String {
|
||||
return when (styleString) {
|
||||
context.getString(R.string.list_style_name_dynamic_light) -> context.getString(R.string.list_style_name_dynamic_night)
|
||||
context.getString(R.string.list_style_name_light) -> context.getString(R.string.list_style_name_night)
|
||||
context.getString(R.string.list_style_name_white) -> context.getString(R.string.list_style_name_black)
|
||||
context.getString(R.string.list_style_name_clear) -> context.getString(R.string.list_style_name_dark)
|
||||
@@ -104,6 +113,13 @@ object Stylish {
|
||||
PreferencesUtil.setStyle(context, styleString)
|
||||
}
|
||||
|
||||
fun isDynamic(context: Context): Boolean {
|
||||
return DynamicColors.isDynamicColorAvailable() && (
|
||||
themeString == context.getString(R.string.list_style_name_dynamic_night)
|
||||
|| themeString == context.getString(R.string.list_style_name_dynamic_light)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that returns the current id of the style selected in the preference
|
||||
* @param context Context to retrieve the id
|
||||
@@ -111,7 +127,7 @@ object Stylish {
|
||||
*/
|
||||
@StyleRes
|
||||
fun getThemeId(context: Context): Int {
|
||||
return when (retrieveEquivalentSystemStyle(context, themeString ?: context.getString(R.string.list_style_name_light))) {
|
||||
return when (retrieveEquivalentSystemStyle(context, themeString)) {
|
||||
context.getString(R.string.list_style_name_night) -> R.style.KeepassDXStyle_Night
|
||||
context.getString(R.string.list_style_name_white) -> R.style.KeepassDXStyle_White
|
||||
context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black
|
||||
@@ -127,6 +143,8 @@ object Stylish {
|
||||
context.getString(R.string.list_style_name_reply_night) -> R.style.KeepassDXStyle_Reply_Night
|
||||
context.getString(R.string.list_style_name_purple) -> R.style.KeepassDXStyle_Purple
|
||||
context.getString(R.string.list_style_name_purple_dark) -> R.style.KeepassDXStyle_Purple_Dark
|
||||
context.getString(R.string.list_style_name_dynamic_light) -> R.style.KeepassDXStyle_Light_Dynamic
|
||||
context.getString(R.string.list_style_name_dynamic_night) -> R.style.KeepassDXStyle_Night_Dynamic
|
||||
else -> R.style.KeepassDXStyle_Light
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,14 +21,22 @@ package com.kunzisoft.keepass.activities.stylish
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.WindowManager
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
import android.view.WindowManager.LayoutParams.FLAG_SECURE
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_PREFERENCE_CHANGED
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
|
||||
/**
|
||||
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
|
||||
@@ -77,12 +85,33 @@ abstract class StylishActivity : AppCompatActivity() {
|
||||
|
||||
customStyle = applyCustomStyle()
|
||||
if (customStyle) {
|
||||
// Preconfigured themes
|
||||
this.themeId = Stylish.getThemeId(this)
|
||||
setTheme(themeId)
|
||||
if (Stylish.isDynamic(this)) {
|
||||
// Material You theme
|
||||
DynamicColors.applyToActivityIfAvailable(this)
|
||||
}
|
||||
}
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.registerOnSharedPreferenceChangeListener(onScreenshotModePrefListener)
|
||||
}
|
||||
private val onScreenshotModePrefListener = OnSharedPreferenceChangeListener { _, key ->
|
||||
if (key != getString(R.string.enable_screenshot_mode_key)) return@OnSharedPreferenceChangeListener
|
||||
|
||||
setScreenshotMode(PreferencesUtil.isScreenshotModeEnabled(this))
|
||||
}
|
||||
|
||||
private fun setScreenshotMode(isEnabled: Boolean) {
|
||||
findViewById<View>(R.id.screenshot_mode_banner)?.visibility = if (isEnabled) VISIBLE else GONE
|
||||
|
||||
// Several gingerbread devices have problems with FLAG_SECURE
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
||||
if (isEnabled) {
|
||||
window.clearFlags(FLAG_SECURE)
|
||||
} else {
|
||||
window.setFlags(FLAG_SECURE, FLAG_SECURE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -94,6 +123,7 @@ abstract class StylishActivity : AppCompatActivity() {
|
||||
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
|
||||
recreateActivity()
|
||||
}
|
||||
setScreenshotMode(PreferencesUtil.isScreenshotModeEnabled(this))
|
||||
}
|
||||
|
||||
private fun recreateActivity() {
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.stylish
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
abstract class StylishFragment : Fragment() {
|
||||
|
||||
@StyleRes
|
||||
protected var themeId: Int = 0
|
||||
protected var contextThemed: Context? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
this.themeId = Stylish.getThemeId(context)
|
||||
contextThemed = ContextThemeWrapper(context, themeId)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
// To fix status bar color
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val window = requireActivity().window
|
||||
val defaultColor = Color.BLACK
|
||||
val windowInset = WindowInsetsControllerCompat(window, window.decorView)
|
||||
try {
|
||||
val taStatusBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.statusBarColor))
|
||||
window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor
|
||||
taStatusBarColor?.recycle()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve theme : status bar color", e)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
try {
|
||||
val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar))
|
||||
windowInset.isAppearanceLightStatusBars = taWindowStatusLight
|
||||
?.getBoolean(0, false) == true
|
||||
taWindowStatusLight?.recycle()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve theme : window light status bar", e)
|
||||
}
|
||||
}
|
||||
try {
|
||||
val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor))
|
||||
window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor
|
||||
taNavigationBarColor?.recycle()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve theme : navigation bar color", e)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
try {
|
||||
val taWindowLightNavigationBar = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightNavigationBar))
|
||||
windowInset.isAppearanceLightNavigationBars = taWindowLightNavigationBar
|
||||
?.getBoolean(0, false) == true
|
||||
taWindowLightNavigationBar?.recycle()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to retrieve theme : navigation light navigation bar", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.onCreateView(inflater, container, savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
contextThemed = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = StylishFragment::class.java.simpleName
|
||||
}
|
||||
}
|
||||
@@ -39,10 +39,10 @@ class BreadcrumbAdapter(val context: Context)
|
||||
mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
|
||||
mShowUUID = PreferencesUtil.showUUID(context)
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
||||
mIconColor = taTextColor.getColor(0, Color.WHITE)
|
||||
taTextColor.recycle()
|
||||
// Retrieve the color to tint the icon
|
||||
val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnSurface))
|
||||
mIconColor = taIconColor.getColor(0, Color.WHITE)
|
||||
taIconColor.recycle()
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
@@ -71,7 +71,7 @@ class BreadcrumbAdapter(val context: Context)
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BreadcrumbGroupViewHolder {
|
||||
return BreadcrumbGroupViewHolder(inflater.inflate(
|
||||
when (viewType) {
|
||||
0 -> R.layout.item_group
|
||||
0 -> R.layout.item_breadcrumb_important
|
||||
else -> R.layout.item_breadcrumb
|
||||
}, parent, false)
|
||||
)
|
||||
@@ -112,8 +112,12 @@ class BreadcrumbAdapter(val context: Context)
|
||||
|
||||
holder.groupNumbersView?.apply {
|
||||
if (mShowNumberEntries) {
|
||||
group.refreshNumberOfChildEntries(Group.ChildFilter.getDefaults(context))
|
||||
text = group.numberOfChildEntries.toString()
|
||||
group.refreshNumberOfChildEntries(
|
||||
Group.ChildFilter.getDefaults(
|
||||
PreferencesUtil.showExpiredEntries(context)
|
||||
)
|
||||
)
|
||||
text = group.recursiveNumberOfChildEntries.toString()
|
||||
visibility = View.VISIBLE
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
|
||||
@@ -27,16 +27,18 @@ import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.ImageViewerActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.helper.getLocalizedName
|
||||
import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
import com.kunzisoft.keepass.services.AttachmentFileNotificationService.Companion.FILE_PROGRESSION_MAX
|
||||
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||
import com.kunzisoft.keepass.view.expand
|
||||
import kotlin.math.max
|
||||
@@ -45,7 +47,7 @@ import kotlin.math.max
|
||||
class EntryAttachmentsItemsAdapter(context: Context)
|
||||
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
|
||||
|
||||
var database: Database? = null
|
||||
var database: ContextualDatabase? = null
|
||||
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
|
||||
var onBinaryPreviewLoaded: ((item: EntryAttachmentState) -> Unit)? = null
|
||||
|
||||
@@ -130,13 +132,14 @@ class EntryAttachmentsItemsAdapter(context: Context)
|
||||
holder.binaryFileSize.text = Formatter.formatFileSize(context, size)
|
||||
holder.binaryFileCompression.apply {
|
||||
if (entryAttachmentState.attachment.binaryData.isCompressed) {
|
||||
text = CompressionAlgorithm.GZip.getName(context.resources)
|
||||
text = CompressionAlgorithm.GZIP.getLocalizedName(context.resources)
|
||||
visibility = View.VISIBLE
|
||||
} else {
|
||||
text = ""
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
holder.binaryFileProgress.max = FILE_PROGRESSION_MAX
|
||||
when (entryAttachmentState.streamDirection) {
|
||||
StreamDirection.UPLOAD -> {
|
||||
holder.binaryFileProgressIcon.isActivated = true
|
||||
@@ -181,7 +184,7 @@ class EntryAttachmentsItemsAdapter(context: Context)
|
||||
AttachmentState.START,
|
||||
AttachmentState.IN_PROGRESS -> View.VISIBLE
|
||||
}
|
||||
progress = entryAttachmentState.downloadProgression
|
||||
setProgressCompat(entryAttachmentState.downloadProgression, true)
|
||||
}
|
||||
holder.binaryFileInfo.setOnClickListener {
|
||||
onItemClickListener?.invoke(entryAttachmentState)
|
||||
@@ -200,7 +203,7 @@ class EntryAttachmentsItemsAdapter(context: Context)
|
||||
var binaryFileCompression: TextView = itemView.findViewById(R.id.item_attachment_compression)
|
||||
var binaryFileProgressContainer: View = itemView.findViewById(R.id.item_attachment_progress_container)
|
||||
var binaryFileProgressIcon: ImageView = itemView.findViewById(R.id.item_attachment_icon)
|
||||
var binaryFileProgress: ProgressBar = itemView.findViewById(R.id.item_attachment_progress)
|
||||
var binaryFileProgress: CircularProgressIndicator = itemView.findViewById(R.id.item_attachment_progress)
|
||||
var binaryFileDeleteButton: View = itemView.findViewById(R.id.item_attachment_delete_button)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
||||
|
||||
class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHistoryAdapter.EntryHistoryViewHolder>() {
|
||||
|
||||
|
||||
@@ -23,12 +23,14 @@ import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import android.net.Uri
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.annotation.ColorInt
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.ViewSwitcher
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||
@@ -84,20 +86,6 @@ class FileDatabaseHistoryAdapter(context: Context)
|
||||
}
|
||||
)
|
||||
|
||||
@ColorInt
|
||||
private val defaultColor: Int
|
||||
@ColorInt
|
||||
private val warningColor: Int
|
||||
|
||||
init {
|
||||
val typedValue = TypedValue()
|
||||
val theme = context.theme
|
||||
theme.resolveAttribute(R.attr.colorAccent, typedValue, true)
|
||||
warningColor = typedValue.data
|
||||
theme.resolveAttribute(android.R.attr.textColorHintInverse, typedValue, true)
|
||||
defaultColor = typedValue.data
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileDatabaseHistoryViewHolder {
|
||||
val view = inflater.inflate(R.layout.item_file_info, parent, false)
|
||||
return FileDatabaseHistoryViewHolder(view)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.kunzisoft.keepass.activities.fragments.PassphraseGeneratorFragment
|
||||
import com.kunzisoft.keepass.activities.fragments.PasswordGeneratorFragment
|
||||
import com.kunzisoft.keepass.activities.fragments.KeyGeneratorFragment
|
||||
|
||||
class KeyGeneratorPagerAdapter(fragment: Fragment)
|
||||
: FragmentStateAdapter(fragment) {
|
||||
|
||||
private val passwordGeneratorFragment = PasswordGeneratorFragment()
|
||||
private val passphraseGeneratorFragment = PassphraseGeneratorFragment()
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return KeyGeneratorFragment.KeyGeneratorTab.values().size
|
||||
}
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (KeyGeneratorFragment.KeyGeneratorTab.getKeyGeneratorTabByPosition(position)) {
|
||||
KeyGeneratorFragment.KeyGeneratorTab.PASSWORD -> passwordGeneratorFragment
|
||||
KeyGeneratorFragment.KeyGeneratorTab.PASSPHRASE -> passphraseGeneratorFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,21 +20,23 @@
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||
import com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||
@@ -42,21 +44,23 @@ import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.database.helper.getLocalizedName
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.otp.OtpType
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.view.setTextSize
|
||||
import com.kunzisoft.keepass.view.strikeOut
|
||||
import java.util.*
|
||||
import java.util.LinkedList
|
||||
|
||||
/**
|
||||
* Create node list adapter with contextMenu or not
|
||||
* @param context Context to use
|
||||
*/
|
||||
class NodesAdapter (private val context: Context,
|
||||
private val database: Database)
|
||||
: RecyclerView.Adapter<NodesAdapter.NodeViewHolder>() {
|
||||
class NodesAdapter (
|
||||
private val context: Context,
|
||||
private val database: ContextualDatabase
|
||||
) : RecyclerView.Adapter<NodesAdapter.NodeViewHolder>() {
|
||||
|
||||
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
|
||||
private val mNodeSortedListCallback: NodeSortedListCallback
|
||||
@@ -86,6 +90,8 @@ class NodesAdapter (private val context: Context,
|
||||
private var mNodeClickCallback: NodeClickCallback? = null
|
||||
private var mClipboardHelper = ClipboardHelper(context)
|
||||
|
||||
@ColorInt
|
||||
private val mColorSurfaceContainer: Int
|
||||
@ColorInt
|
||||
private val mTextColorPrimary: Int
|
||||
@ColorInt
|
||||
@@ -93,9 +99,9 @@ class NodesAdapter (private val context: Context,
|
||||
@ColorInt
|
||||
private val mTextColorSecondary: Int
|
||||
@ColorInt
|
||||
private val mColorAccentLight: Int
|
||||
private val mColorSecondary: Int
|
||||
@ColorInt
|
||||
private val mColorOnAccentColor: Int
|
||||
private val mColorOnSecondary: Int
|
||||
|
||||
/**
|
||||
* Determine if the adapter contains or not any element
|
||||
@@ -112,26 +118,29 @@ class NodesAdapter (private val context: Context,
|
||||
this.mNodeSortedListCallback = NodeSortedListCallback()
|
||||
this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback)
|
||||
|
||||
val taColorSurfaceContainer = context.obtainStyledAttributes(intArrayOf(R.attr.colorSurfaceContainer))
|
||||
this.mColorSurfaceContainer = taColorSurfaceContainer.getColor(0, Color.BLACK)
|
||||
taColorSurfaceContainer.recycle()
|
||||
// Retrieve the color to tint the icon
|
||||
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
||||
val taTextColorPrimary = context.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
||||
this.mTextColorPrimary = taTextColorPrimary.getColor(0, Color.BLACK)
|
||||
taTextColorPrimary.recycle()
|
||||
// To get text color
|
||||
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
val taTextColor = context.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
this.mTextColor = taTextColor.getColor(0, Color.BLACK)
|
||||
taTextColor.recycle()
|
||||
// To get text color secondary
|
||||
val taTextColorSecondary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorSecondary))
|
||||
val taTextColorSecondary = context.obtainStyledAttributes(intArrayOf(android.R.attr.textColorSecondary))
|
||||
this.mTextColorSecondary = taTextColorSecondary.getColor(0, Color.BLACK)
|
||||
taTextColorSecondary.recycle()
|
||||
// To get background color for selection
|
||||
val taColorAccentLight = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccentLight))
|
||||
this.mColorAccentLight = taColorAccentLight.getColor(0, Color.GRAY)
|
||||
taColorAccentLight.recycle()
|
||||
val taColorSecondary = context.obtainStyledAttributes(intArrayOf(R.attr.colorSecondary))
|
||||
this.mColorSecondary = taColorSecondary.getColor(0, Color.GRAY)
|
||||
taColorSecondary.recycle()
|
||||
// To get text color for selection
|
||||
val taColorOnAccentColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnAccentColor))
|
||||
this.mColorOnAccentColor = taColorOnAccentColor.getColor(0, Color.WHITE)
|
||||
taColorOnAccentColor.recycle()
|
||||
val taColorOnSecondary = context.obtainStyledAttributes(intArrayOf(R.attr.colorOnSecondary))
|
||||
this.mColorOnSecondary = taColorOnSecondary.getColor(0, Color.WHITE)
|
||||
taColorOnSecondary.recycle()
|
||||
}
|
||||
|
||||
private fun assignPreferences() {
|
||||
@@ -152,7 +161,9 @@ class NodesAdapter (private val context: Context,
|
||||
this.mShowOTP = PreferencesUtil.showOTPToken(context)
|
||||
this.mShowUUID = PreferencesUtil.showUUID(context)
|
||||
|
||||
this.mEntryFilters = Group.ChildFilter.getDefaults(context)
|
||||
this.mEntryFilters = Group.ChildFilter.getDefaults(
|
||||
PreferencesUtil.showExpiredEntries(context)
|
||||
)
|
||||
|
||||
// Reinit textSize for all view type
|
||||
mCalculateViewTypeTextSize.forEachIndexed { index, _ -> mCalculateViewTypeTextSize[index] = true }
|
||||
@@ -186,6 +197,7 @@ class NodesAdapter (private val context: Context,
|
||||
&& oldItem.containsAttachment() == newItem.containsAttachment()
|
||||
} else if (oldItem is Group && newItem is Group) {
|
||||
typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries
|
||||
&& oldItem.recursiveNumberOfChildEntries == newItem.recursiveNumberOfChildEntries
|
||||
&& oldItem.notes == newItem.notes
|
||||
}
|
||||
return typeContentTheSame
|
||||
@@ -376,10 +388,10 @@ class NodesAdapter (private val context: Context,
|
||||
|
||||
// Assign icon colors
|
||||
var iconColor = if (holder.container.isSelected)
|
||||
mColorOnAccentColor
|
||||
mColorOnSecondary
|
||||
else when (subNode.type) {
|
||||
Type.GROUP -> mTextColorPrimary
|
||||
Type.ENTRY -> mTextColor
|
||||
Type.GROUP -> mTextColor
|
||||
Type.ENTRY -> mColorSecondary
|
||||
}
|
||||
|
||||
// Specific elements for entry
|
||||
@@ -424,16 +436,8 @@ class NodesAdapter (private val context: Context,
|
||||
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
||||
|
||||
// Assign colors
|
||||
val backgroundColor = if (mShowEntryColors) entry.backgroundColor else null
|
||||
if (!holder.container.isSelected) {
|
||||
if (backgroundColor != null) {
|
||||
holder.container.setBackgroundColor(backgroundColor)
|
||||
} else {
|
||||
holder.container.setBackgroundColor(Color.TRANSPARENT)
|
||||
}
|
||||
} else {
|
||||
holder.container.setBackgroundColor(mColorAccentLight)
|
||||
}
|
||||
assignBackgroundColor(holder.container, entry)
|
||||
assignBackgroundColor(holder.otpContainer, entry)
|
||||
val foregroundColor = if (mShowEntryColors) entry.foregroundColor else null
|
||||
if (!holder.container.isSelected) {
|
||||
if (foregroundColor != null) {
|
||||
@@ -453,12 +457,12 @@ class NodesAdapter (private val context: Context,
|
||||
holder.meta.setTextColor(mTextColor)
|
||||
}
|
||||
} else {
|
||||
holder.text.setTextColor(mColorOnAccentColor)
|
||||
holder.subText?.setTextColor(mColorOnAccentColor)
|
||||
holder.otpToken?.setTextColor(mColorOnAccentColor)
|
||||
holder.otpProgress?.setIndicatorColor(mColorOnAccentColor)
|
||||
holder.attachmentIcon?.setColorFilter(mColorOnAccentColor)
|
||||
holder.meta.setTextColor(mColorOnAccentColor)
|
||||
holder.text.setTextColor(mColorOnSecondary)
|
||||
holder.subText?.setTextColor(mColorOnSecondary)
|
||||
holder.otpToken?.setTextColor(mColorOnSecondary)
|
||||
holder.otpProgress?.setIndicatorColor(mColorOnSecondary)
|
||||
holder.attachmentIcon?.setColorFilter(mColorOnSecondary)
|
||||
holder.meta.setTextColor(mColorOnSecondary)
|
||||
}
|
||||
|
||||
database.stopManageEntry(entry)
|
||||
@@ -469,7 +473,7 @@ class NodesAdapter (private val context: Context,
|
||||
if (mShowNumberEntries) {
|
||||
holder.numberChildren?.apply {
|
||||
text = (subNode as Group)
|
||||
.numberOfChildEntries
|
||||
.recursiveNumberOfChildEntries
|
||||
.toString()
|
||||
setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier)
|
||||
visibility = View.VISIBLE
|
||||
@@ -516,22 +520,40 @@ class NodesAdapter (private val context: Context,
|
||||
null -> {}
|
||||
}
|
||||
holder?.otpToken?.apply {
|
||||
text = otpElement?.token
|
||||
text = otpElement?.tokenString
|
||||
setTextSize(mTextSizeUnit, mOtpTokenTextDefaultDimension, mPrefSizeMultiplier)
|
||||
}
|
||||
holder?.otpContainer?.setOnClickListener {
|
||||
otpElement?.token?.let { token ->
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.copy_field,
|
||||
TemplateField.getLocalizedName(context, TemplateField.LABEL_TOKEN)),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
mClipboardHelper.copyToClipboard(token)
|
||||
try {
|
||||
mClipboardHelper.copyToClipboard(
|
||||
TemplateField.getLocalizedName(context, TemplateField.LABEL_TOKEN),
|
||||
token,
|
||||
true
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to copy the OTP token", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun assignBackgroundColor(view: View?, entry: Entry) {
|
||||
view?.let {
|
||||
ViewCompat.setBackgroundTintList(
|
||||
view,
|
||||
ColorStateList.valueOf(
|
||||
if (!view.isSelected) {
|
||||
(if (mShowEntryColors) entry.backgroundColor else null)
|
||||
?: mColorSurfaceContainer
|
||||
} else {
|
||||
mColorSecondary
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class OtpRunnable(val view: View?): Runnable {
|
||||
|
||||
var action: (() -> Unit)? = null
|
||||
@@ -561,8 +583,8 @@ class NodesAdapter (private val context: Context,
|
||||
* Callback listener to redefine to do an action when a node is click
|
||||
*/
|
||||
interface NodeClickCallback {
|
||||
fun onNodeClick(database: Database, node: Node)
|
||||
fun onNodeLongClick(database: Database, node: Node): Boolean
|
||||
fun onNodeClick(database: ContextualDatabase, node: Node)
|
||||
fun onNodeLongClick(database: ContextualDatabase, node: Node): Boolean
|
||||
}
|
||||
|
||||
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
@@ -11,11 +11,12 @@ import android.widget.TextView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.template.Template
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.database.helper.getLocalizedName
|
||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
|
||||
|
||||
class TemplatesSelectorAdapter(
|
||||
context: Context,
|
||||
private val context: Context,
|
||||
private var templates: List<Template>): BaseAdapter() {
|
||||
|
||||
var iconDrawableFactory: IconDrawableFactory? = null
|
||||
@@ -35,7 +36,9 @@ class TemplatesSelectorAdapter(
|
||||
var templateView = convertView
|
||||
if (templateView == null) {
|
||||
holder = TemplateSelectorViewHolder()
|
||||
templateView = inflater.inflate(R.layout.item_template, parent, false)
|
||||
templateView = inflater
|
||||
.cloneInContext(context)
|
||||
.inflate(R.layout.item_template, parent, false)
|
||||
holder.background = templateView?.findViewById(R.id.template_background)
|
||||
holder.icon = templateView?.findViewById(R.id.template_image)
|
||||
holder.name = templateView?.findViewById(R.id.template_name)
|
||||
@@ -74,4 +77,4 @@ class TemplatesSelectorAdapter(
|
||||
var icon: ImageView? = null
|
||||
var name: TextView? = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,15 @@ import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import android.content.Context
|
||||
import androidx.room.AutoMigration
|
||||
|
||||
@Database(version = 1, entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class])
|
||||
@Database(
|
||||
version = 2,
|
||||
entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class],
|
||||
autoMigrations = [
|
||||
AutoMigration (from = 1, to = 2)
|
||||
]
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun fileDatabaseHistoryDao(): FileDatabaseHistoryDao
|
||||
|
||||
@@ -19,24 +19,27 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.app.database
|
||||
|
||||
import android.content.*
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.IntentFilter
|
||||
import android.content.ServiceConnection
|
||||
import android.net.Uri
|
||||
import android.os.IBinder
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData.Companion.BASE64_FLAG
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.IOActionTask
|
||||
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
||||
import java.util.*
|
||||
import java.util.LinkedList
|
||||
|
||||
class CipherDatabaseAction(context: Context) {
|
||||
|
||||
private val applicationContext = context.applicationContext
|
||||
private val cipherDatabaseDao =
|
||||
AppDatabase
|
||||
.getDatabase(applicationContext)
|
||||
.cipherDatabaseDao()
|
||||
AppDatabase.getDatabase(applicationContext).cipherDatabaseDao()
|
||||
|
||||
// Temp DAO to easily remove content if object no longer in memory
|
||||
private var useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
|
||||
@@ -83,7 +86,7 @@ class CipherDatabaseAction(context: Context) {
|
||||
try {
|
||||
AdvancedUnlockNotificationService.bindService(applicationContext,
|
||||
mServiceConnection!!,
|
||||
Context.BIND_AUTO_CREATE)
|
||||
Context.BIND_AUTO_CREATE)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to start cipher action", e)
|
||||
performedAction.invoke()
|
||||
@@ -136,11 +139,11 @@ class CipherDatabaseAction(context: Context) {
|
||||
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
|
||||
this.encryptedValue = Base64.decode(
|
||||
cipherDatabaseEntity.encryptedValue,
|
||||
Base64.NO_WRAP
|
||||
BASE64_FLAG
|
||||
)
|
||||
this.specParameters = Base64.decode(
|
||||
cipherDatabaseEntity.specParameters,
|
||||
Base64.NO_WRAP
|
||||
BASE64_FLAG
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -148,8 +151,9 @@ class CipherDatabaseAction(context: Context) {
|
||||
}
|
||||
} else {
|
||||
IOActionTask(
|
||||
{
|
||||
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())?.let { cipherDatabaseEntity ->
|
||||
{
|
||||
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
|
||||
?.let { cipherDatabaseEntity ->
|
||||
CipherEncryptDatabase().apply {
|
||||
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
|
||||
this.encryptedValue = Base64.decode(
|
||||
@@ -162,10 +166,10 @@ class CipherDatabaseAction(context: Context) {
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
cipherDatabaseResultListener.invoke(it)
|
||||
}
|
||||
},
|
||||
{
|
||||
cipherDatabaseResultListener.invoke(it)
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
}
|
||||
@@ -183,8 +187,8 @@ class CipherDatabaseAction(context: Context) {
|
||||
|
||||
val cipherDatabaseEntity = CipherDatabaseEntity(
|
||||
databaseUri.toString(),
|
||||
Base64.encodeToString(cipherEncryptDatabase.encryptedValue, Base64.NO_WRAP),
|
||||
Base64.encodeToString(cipherEncryptDatabase.specParameters, Base64.NO_WRAP),
|
||||
Base64.encodeToString(cipherEncryptDatabase.encryptedValue, BASE64_FLAG),
|
||||
Base64.encodeToString(cipherEncryptDatabase.specParameters, BASE64_FLAG),
|
||||
)
|
||||
|
||||
if (useTempDao) {
|
||||
@@ -222,12 +226,12 @@ class CipherDatabaseAction(context: Context) {
|
||||
}
|
||||
} else {
|
||||
IOActionTask(
|
||||
{
|
||||
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
|
||||
},
|
||||
{
|
||||
cipherDatabaseResultListener?.invoke()
|
||||
}
|
||||
{
|
||||
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
|
||||
},
|
||||
{
|
||||
cipherDatabaseResultListener?.invoke()
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
}
|
||||
@@ -240,9 +244,9 @@ class CipherDatabaseAction(context: Context) {
|
||||
}
|
||||
// To erase the residues
|
||||
IOActionTask(
|
||||
{
|
||||
cipherDatabaseDao.deleteAll()
|
||||
}
|
||||
{
|
||||
cipherDatabaseDao.deleteAll()
|
||||
}
|
||||
).execute()
|
||||
// Unbind
|
||||
removeAllDataAndDetach()
|
||||
@@ -251,4 +255,4 @@ class CipherDatabaseAction(context: Context) {
|
||||
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction) {
|
||||
private val TAG = CipherDatabaseAction::class.java.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
@@ -22,204 +22,225 @@ package com.kunzisoft.keepass.app.database
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.model.DatabaseFile
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.IOActionTask
|
||||
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.utils.decodeUri
|
||||
import com.kunzisoft.keepass.utils.parseUri
|
||||
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
|
||||
|
||||
class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||
|
||||
private val databaseFileHistoryDao =
|
||||
AppDatabase
|
||||
.getDatabase(applicationContext)
|
||||
.fileDatabaseHistoryDao()
|
||||
AppDatabase.getDatabase(applicationContext).fileDatabaseHistoryDao()
|
||||
|
||||
fun getDatabaseFile(databaseUri: Uri,
|
||||
databaseFileResult: (DatabaseFile?) -> Unit) {
|
||||
IOActionTask(
|
||||
{
|
||||
val fileDatabaseHistoryEntity = databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||
val fileDatabaseInfo = FileDatabaseInfo(applicationContext, databaseUri)
|
||||
DatabaseFile(
|
||||
databaseUri,
|
||||
UriUtil.parse(fileDatabaseHistoryEntity?.keyFileUri),
|
||||
UriUtil.decode(fileDatabaseHistoryEntity?.databaseUri),
|
||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias ?: ""),
|
||||
fileDatabaseInfo.exists,
|
||||
fileDatabaseInfo.getLastModificationString(),
|
||||
fileDatabaseInfo.getSizeString()
|
||||
)
|
||||
},
|
||||
{
|
||||
databaseFileResult.invoke(it)
|
||||
}
|
||||
{
|
||||
val fileDatabaseHistoryEntity =
|
||||
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||
val fileDatabaseInfo = FileDatabaseInfo(
|
||||
applicationContext,
|
||||
databaseUri)
|
||||
DatabaseFile(
|
||||
databaseUri,
|
||||
fileDatabaseHistoryEntity?.keyFileUri?.parseUri(),
|
||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity?.hardwareKey),
|
||||
fileDatabaseHistoryEntity?.databaseUri?.decodeUri(),
|
||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias
|
||||
?: ""),
|
||||
fileDatabaseInfo.exists,
|
||||
fileDatabaseInfo.getLastModificationString(),
|
||||
fileDatabaseInfo.getSizeString()
|
||||
)
|
||||
},
|
||||
{
|
||||
databaseFileResult.invoke(it)
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun getKeyFileUriByDatabaseUri(databaseUri: Uri,
|
||||
keyFileUriResultListener: (Uri?) -> Unit) {
|
||||
IOActionTask(
|
||||
{
|
||||
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||
},
|
||||
{
|
||||
it?.let { fileHistoryEntity ->
|
||||
fileHistoryEntity.keyFileUri?.let { keyFileUri ->
|
||||
keyFileUriResultListener.invoke(UriUtil.parse(keyFileUri))
|
||||
}
|
||||
} ?: keyFileUriResultListener.invoke(null)
|
||||
}
|
||||
{
|
||||
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||
},
|
||||
{
|
||||
it?.let { fileHistoryEntity ->
|
||||
fileHistoryEntity.keyFileUri?.let { keyFileUri ->
|
||||
keyFileUriResultListener.invoke(keyFileUri.parseUri())
|
||||
}
|
||||
} ?: keyFileUriResultListener.invoke(null)
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun getDatabaseFileList(databaseFileListResult: (List<DatabaseFile>) -> Unit) {
|
||||
IOActionTask(
|
||||
{
|
||||
val hideBrokenLocations = PreferencesUtil.hideBrokenLocations(applicationContext)
|
||||
// Show only uri accessible
|
||||
val databaseFileListLoaded = ArrayList<DatabaseFile>()
|
||||
databaseFileHistoryDao.getAll().forEach { fileDatabaseHistoryEntity ->
|
||||
val fileDatabaseInfo = FileDatabaseInfo(applicationContext, fileDatabaseHistoryEntity.databaseUri)
|
||||
if (hideBrokenLocations && fileDatabaseInfo.exists
|
||||
|| !hideBrokenLocations) {
|
||||
databaseFileListLoaded.add(
|
||||
DatabaseFile(
|
||||
UriUtil.parse(fileDatabaseHistoryEntity.databaseUri),
|
||||
UriUtil.parse(fileDatabaseHistoryEntity.keyFileUri),
|
||||
UriUtil.decode(fileDatabaseHistoryEntity.databaseUri),
|
||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
|
||||
fileDatabaseInfo.exists,
|
||||
fileDatabaseInfo.getLastModificationString(),
|
||||
fileDatabaseInfo.getSizeString()
|
||||
)
|
||||
{
|
||||
val hideBrokenLocations =
|
||||
PreferencesUtil.hideBrokenLocations(
|
||||
applicationContext)
|
||||
// Show only uri accessible
|
||||
val databaseFileListLoaded = ArrayList<DatabaseFile>()
|
||||
databaseFileHistoryDao.getAll().forEach { fileDatabaseHistoryEntity ->
|
||||
val fileDatabaseInfo = FileDatabaseInfo(
|
||||
applicationContext,
|
||||
fileDatabaseHistoryEntity.databaseUri)
|
||||
if (hideBrokenLocations && fileDatabaseInfo.exists
|
||||
|| !hideBrokenLocations
|
||||
) {
|
||||
databaseFileListLoaded.add(
|
||||
DatabaseFile(
|
||||
fileDatabaseHistoryEntity.databaseUri.parseUri(),
|
||||
fileDatabaseHistoryEntity.keyFileUri?.parseUri(),
|
||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity.hardwareKey),
|
||||
fileDatabaseHistoryEntity.databaseUri.decodeUri(),
|
||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
|
||||
fileDatabaseInfo.exists,
|
||||
fileDatabaseInfo.getLastModificationString(),
|
||||
fileDatabaseInfo.getSizeString()
|
||||
)
|
||||
}
|
||||
}
|
||||
databaseFileListLoaded
|
||||
},
|
||||
{
|
||||
databaseFileList ->
|
||||
databaseFileList?.let {
|
||||
databaseFileListResult.invoke(it)
|
||||
)
|
||||
}
|
||||
}
|
||||
databaseFileListLoaded
|
||||
},
|
||||
{ databaseFileList ->
|
||||
databaseFileList?.let {
|
||||
databaseFileListResult.invoke(it)
|
||||
}
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null,
|
||||
fun addOrUpdateDatabaseUri(databaseUri: Uri,
|
||||
keyFileUri: Uri? = null,
|
||||
hardwareKey: HardwareKey? = null,
|
||||
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
|
||||
addOrUpdateDatabaseFile(DatabaseFile(
|
||||
databaseUri,
|
||||
keyFileUri
|
||||
databaseUri,
|
||||
keyFileUri,
|
||||
hardwareKey
|
||||
), databaseFileAddedOrUpdatedResult)
|
||||
}
|
||||
|
||||
fun addOrUpdateDatabaseFile(databaseFileToAddOrUpdate: DatabaseFile,
|
||||
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
|
||||
IOActionTask(
|
||||
{
|
||||
databaseFileToAddOrUpdate.databaseUri?.let { databaseUri ->
|
||||
// Try to get info in database first
|
||||
val fileDatabaseHistoryRetrieve = databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||
{
|
||||
databaseFileToAddOrUpdate.databaseUri?.let { databaseUri ->
|
||||
// Try to get info in database first
|
||||
val fileDatabaseHistoryRetrieve =
|
||||
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||
|
||||
// Complete alias if not exists
|
||||
val fileDatabaseHistory = FileDatabaseHistoryEntity(
|
||||
databaseUri.toString(),
|
||||
databaseFileToAddOrUpdate.databaseAlias
|
||||
?: fileDatabaseHistoryRetrieve?.databaseAlias
|
||||
?: "",
|
||||
databaseFileToAddOrUpdate.keyFileUri?.toString(),
|
||||
System.currentTimeMillis()
|
||||
// Complete alias if not exists
|
||||
val fileDatabaseHistory =
|
||||
FileDatabaseHistoryEntity(
|
||||
databaseUri.toString(),
|
||||
databaseFileToAddOrUpdate.databaseAlias
|
||||
?: fileDatabaseHistoryRetrieve?.databaseAlias
|
||||
?: "",
|
||||
databaseFileToAddOrUpdate.keyFileUri?.toString(),
|
||||
databaseFileToAddOrUpdate.hardwareKey?.value,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
|
||||
// Update values if history element not yet in the database
|
||||
try {
|
||||
if (fileDatabaseHistoryRetrieve == null) {
|
||||
databaseFileHistoryDao.add(fileDatabaseHistory)
|
||||
} else {
|
||||
databaseFileHistoryDao.update(fileDatabaseHistory)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to add or update database history", e)
|
||||
// Update values if history element not yet in the database
|
||||
try {
|
||||
if (fileDatabaseHistoryRetrieve == null) {
|
||||
databaseFileHistoryDao.add(fileDatabaseHistory)
|
||||
} else {
|
||||
databaseFileHistoryDao.update(fileDatabaseHistory)
|
||||
}
|
||||
|
||||
val fileDatabaseInfo = FileDatabaseInfo(applicationContext,
|
||||
fileDatabaseHistory.databaseUri)
|
||||
DatabaseFile(
|
||||
UriUtil.parse(fileDatabaseHistory.databaseUri),
|
||||
UriUtil.parse(fileDatabaseHistory.keyFileUri),
|
||||
UriUtil.decode(fileDatabaseHistory.databaseUri),
|
||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
|
||||
fileDatabaseInfo.exists,
|
||||
fileDatabaseInfo.getLastModificationString(),
|
||||
fileDatabaseInfo.getSizeString()
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to add or update database history", e)
|
||||
}
|
||||
},
|
||||
{
|
||||
databaseFileAddedOrUpdatedResult?.invoke(it)
|
||||
|
||||
val fileDatabaseInfo =
|
||||
FileDatabaseInfo(applicationContext,
|
||||
fileDatabaseHistory.databaseUri)
|
||||
DatabaseFile(
|
||||
fileDatabaseHistory.databaseUri.parseUri(),
|
||||
fileDatabaseHistory.keyFileUri?.parseUri(),
|
||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
|
||||
fileDatabaseHistory.databaseUri.decodeUri(),
|
||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
|
||||
fileDatabaseInfo.exists,
|
||||
fileDatabaseInfo.getLastModificationString(),
|
||||
fileDatabaseInfo.getSizeString()
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
databaseFileAddedOrUpdatedResult?.invoke(it)
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun deleteDatabaseFile(databaseFileToDelete: DatabaseFile,
|
||||
databaseFileDeletedResult: (DatabaseFile?) -> Unit) {
|
||||
IOActionTask(
|
||||
{
|
||||
databaseFileToDelete.databaseUri?.let { databaseUri ->
|
||||
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())?.let { fileDatabaseHistory ->
|
||||
{
|
||||
databaseFileToDelete.databaseUri?.let { databaseUri ->
|
||||
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||
?.let { fileDatabaseHistory ->
|
||||
val returnValue = databaseFileHistoryDao.delete(fileDatabaseHistory)
|
||||
if (returnValue > 0) {
|
||||
DatabaseFile(
|
||||
UriUtil.parse(fileDatabaseHistory.databaseUri),
|
||||
UriUtil.parse(fileDatabaseHistory.keyFileUri),
|
||||
UriUtil.decode(fileDatabaseHistory.databaseUri),
|
||||
databaseFileToDelete.databaseAlias
|
||||
fileDatabaseHistory.databaseUri.parseUri(),
|
||||
fileDatabaseHistory.keyFileUri?.parseUri(),
|
||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
|
||||
fileDatabaseHistory.databaseUri.decodeUri(),
|
||||
databaseFileToDelete.databaseAlias
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
databaseFileDeletedResult.invoke(it)
|
||||
}
|
||||
},
|
||||
{
|
||||
databaseFileDeletedResult.invoke(it)
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun deleteKeyFileByDatabaseUri(databaseUri: Uri,
|
||||
result: (() ->Unit)? = null) {
|
||||
IOActionTask(
|
||||
{
|
||||
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
|
||||
},
|
||||
{
|
||||
result?.invoke()
|
||||
}
|
||||
{
|
||||
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
|
||||
},
|
||||
{
|
||||
result?.invoke()
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun deleteAllKeyFiles(result: (() ->Unit)? = null) {
|
||||
IOActionTask(
|
||||
{
|
||||
databaseFileHistoryDao.deleteAllKeyFiles()
|
||||
},
|
||||
{
|
||||
result?.invoke()
|
||||
}
|
||||
{
|
||||
databaseFileHistoryDao.deleteAllKeyFiles()
|
||||
},
|
||||
{
|
||||
result?.invoke()
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun deleteAll(result: (() ->Unit)? = null) {
|
||||
IOActionTask(
|
||||
{
|
||||
databaseFileHistoryDao.deleteAll()
|
||||
},
|
||||
{
|
||||
result?.invoke()
|
||||
}
|
||||
{
|
||||
databaseFileHistoryDao.deleteAll()
|
||||
},
|
||||
{
|
||||
result?.invoke()
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@ data class FileDatabaseHistoryEntity(
|
||||
@ColumnInfo(name = "keyfile_uri")
|
||||
var keyFileUri: String?,
|
||||
|
||||
@ColumnInfo(name = "hardware_key")
|
||||
var hardwareKey: String?,
|
||||
|
||||
@ColumnInfo(name = "updated")
|
||||
val updated: Long
|
||||
) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.kunzisoft.keepass.autofill
|
||||
|
||||
import android.app.assist.AssistStructure
|
||||
import android.view.inputmethod.InlineSuggestionsRequest
|
||||
|
||||
data class AutofillComponent(val assistStructure: AssistStructure,
|
||||
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?)
|
||||
@@ -29,9 +29,12 @@ import android.graphics.BlendMode
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.service.autofill.Dataset
|
||||
import android.service.autofill.Field
|
||||
import android.service.autofill.FillResponse
|
||||
import android.service.autofill.InlinePresentation
|
||||
import android.service.autofill.Presentations
|
||||
import android.util.Log
|
||||
import android.view.autofill.AutofillId
|
||||
import android.view.autofill.AutofillManager
|
||||
import android.view.autofill.AutofillValue
|
||||
import android.widget.RemoteViews
|
||||
@@ -48,7 +51,7 @@ import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
@@ -56,6 +59,7 @@ import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@@ -65,10 +69,10 @@ object AutofillHelper {
|
||||
private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
|
||||
|
||||
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
|
||||
intent?.getParcelableExtra<AssistStructure?>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
|
||||
intent?.getParcelableExtraCompat<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
AutofillComponent(assistStructure,
|
||||
intent.getParcelableExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST))
|
||||
intent.getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST))
|
||||
} else {
|
||||
AutofillComponent(assistStructure, null)
|
||||
}
|
||||
@@ -89,39 +93,85 @@ object AutofillHelper {
|
||||
}
|
||||
|
||||
private fun newRemoteViews(context: Context,
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
remoteViewsText: String,
|
||||
remoteViewsIcon: IconImage? = null): RemoteViews {
|
||||
val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry)
|
||||
presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
|
||||
val remoteViews = RemoteViews(context.packageName, R.layout.item_autofill_entry)
|
||||
remoteViews.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
|
||||
if (remoteViewsIcon != null) {
|
||||
try {
|
||||
database.iconDrawableFactory.getBitmapFromIcon(context,
|
||||
remoteViewsIcon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
||||
presentation.setImageViewBitmap(R.id.autofill_entry_icon, bitmap)
|
||||
remoteViews.setImageViewBitmap(R.id.autofill_entry_icon, bitmap)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
||||
}
|
||||
}
|
||||
return presentation
|
||||
return remoteViews
|
||||
}
|
||||
|
||||
private fun buildDataset(context: Context,
|
||||
database: Database,
|
||||
entryInfo: EntryInfo,
|
||||
struct: StructureParser.Result,
|
||||
additionalBuild: ((build: Dataset.Builder) -> Unit)? = null): Dataset? {
|
||||
val title = makeEntryTitle(entryInfo)
|
||||
val views = newRemoteViews(context, database, title, entryInfo.icon)
|
||||
val builder = Dataset.Builder(views)
|
||||
builder.setId(entryInfo.id.toString())
|
||||
private fun Dataset.Builder.addValueToDatasetBuilder(
|
||||
id: AutofillId,
|
||||
autofillValue: AutofillValue?
|
||||
): Dataset.Builder {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
setField(
|
||||
id, autofillValue?.let {
|
||||
Field.Builder()
|
||||
.setValue(it)
|
||||
.build()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
setValue(id, autofillValue)
|
||||
}
|
||||
Log.d(TAG, "Set Autofill value $autofillValue for id $id")
|
||||
return this
|
||||
}
|
||||
|
||||
private fun buildDatasetForEntry(context: Context,
|
||||
database: ContextualDatabase,
|
||||
entryInfo: EntryInfo,
|
||||
struct: StructureParser.Result,
|
||||
inlinePresentation: InlinePresentation?): Dataset {
|
||||
val remoteViews: RemoteViews = newRemoteViews(context, database, makeEntryTitle(entryInfo), entryInfo.icon)
|
||||
|
||||
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
Dataset.Builder(Presentations.Builder()
|
||||
.apply {
|
||||
inlinePresentation?.let {
|
||||
setInlinePresentation(inlinePresentation)
|
||||
}
|
||||
}
|
||||
.setDialogPresentation(remoteViews)
|
||||
.setMenuPresentation(remoteViews)
|
||||
.build())
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
Dataset.Builder(remoteViews).apply {
|
||||
inlinePresentation?.let {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
setInlinePresentation(inlinePresentation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
datasetBuilder.setId(entryInfo.id.toString())
|
||||
|
||||
struct.usernameId?.let { usernameId ->
|
||||
builder.setValue(usernameId, AutofillValue.forText(entryInfo.username))
|
||||
datasetBuilder.addValueToDatasetBuilder(
|
||||
usernameId,
|
||||
AutofillValue.forText(entryInfo.username)
|
||||
)
|
||||
}
|
||||
struct.passwordId?.let { passwordId ->
|
||||
builder.setValue(passwordId, AutofillValue.forText(entryInfo.password))
|
||||
datasetBuilder.addValueToDatasetBuilder(
|
||||
passwordId,
|
||||
AutofillValue.forText(entryInfo.password)
|
||||
)
|
||||
}
|
||||
|
||||
if (entryInfo.expires) {
|
||||
@@ -134,9 +184,15 @@ object AutofillHelper {
|
||||
struct.creditCardExpirationDateId?.let {
|
||||
if (struct.isWebView) {
|
||||
// set date string as defined in https://html.spec.whatwg.org
|
||||
builder.setValue(it, AutofillValue.forText("$year\u002D$monthString"))
|
||||
datasetBuilder.addValueToDatasetBuilder(
|
||||
it,
|
||||
AutofillValue.forText("$year\u002D$monthString")
|
||||
)
|
||||
} else {
|
||||
builder.setValue(it, AutofillValue.forDate(entryInfo.expiryTime.date.time))
|
||||
datasetBuilder.addValueToDatasetBuilder(
|
||||
it,
|
||||
AutofillValue.forDate(entryInfo.expiryTime.date.time)
|
||||
)
|
||||
}
|
||||
}
|
||||
struct.creditCardExpirationYearId?.let {
|
||||
@@ -150,34 +206,58 @@ object AutofillHelper {
|
||||
}
|
||||
if (yearIndex != -1) {
|
||||
autofillValue = AutofillValue.forList(yearIndex)
|
||||
builder.setValue(it, autofillValue)
|
||||
datasetBuilder.addValueToDatasetBuilder(
|
||||
it,
|
||||
autofillValue
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (autofillValue == null) {
|
||||
builder.setValue(it, AutofillValue.forText(year.toString()))
|
||||
datasetBuilder.addValueToDatasetBuilder(
|
||||
it,
|
||||
AutofillValue.forText(year.toString())
|
||||
)
|
||||
}
|
||||
}
|
||||
struct.creditCardExpirationMonthId?.let {
|
||||
if (struct.isWebView) {
|
||||
builder.setValue(it, AutofillValue.forText(monthString))
|
||||
datasetBuilder.addValueToDatasetBuilder(
|
||||
it,
|
||||
AutofillValue.forText(monthString)
|
||||
)
|
||||
} else {
|
||||
if (struct.creditCardExpirationMonthOptions != null) {
|
||||
// index starts at 0
|
||||
builder.setValue(it, AutofillValue.forList(month - 1))
|
||||
datasetBuilder.addValueToDatasetBuilder(
|
||||
it,
|
||||
AutofillValue.forList(month - 1)
|
||||
)
|
||||
} else {
|
||||
builder.setValue(it, AutofillValue.forText(monthString))
|
||||
datasetBuilder.addValueToDatasetBuilder(
|
||||
it,
|
||||
AutofillValue.forText(monthString)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
struct.creditCardExpirationDayId?.let {
|
||||
if (struct.isWebView) {
|
||||
builder.setValue(it, AutofillValue.forText(dayString))
|
||||
datasetBuilder.addValueToDatasetBuilder(
|
||||
it,
|
||||
AutofillValue.forText(dayString)
|
||||
)
|
||||
} else {
|
||||
if (struct.creditCardExpirationDayOptions != null) {
|
||||
builder.setValue(it, AutofillValue.forList(day - 1))
|
||||
datasetBuilder.addValueToDatasetBuilder(
|
||||
it,
|
||||
AutofillValue.forList(day - 1)
|
||||
)
|
||||
} else {
|
||||
builder.setValue(it, AutofillValue.forText(dayString))
|
||||
datasetBuilder.addValueToDatasetBuilder(
|
||||
it,
|
||||
AutofillValue.forText(dayString)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,36 +265,39 @@ object AutofillHelper {
|
||||
for (field in entryInfo.customFields) {
|
||||
if (field.name == TemplateField.LABEL_HOLDER) {
|
||||
struct.creditCardHolderId?.let { ccNameId ->
|
||||
builder.setValue(ccNameId, AutofillValue.forText(field.protectedValue.stringValue))
|
||||
datasetBuilder.addValueToDatasetBuilder(
|
||||
ccNameId,
|
||||
AutofillValue.forText(field.protectedValue.stringValue)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (field.name == TemplateField.LABEL_NUMBER) {
|
||||
struct.creditCardNumberId?.let { ccnId ->
|
||||
builder.setValue(ccnId, AutofillValue.forText(field.protectedValue.stringValue))
|
||||
datasetBuilder.addValueToDatasetBuilder(
|
||||
ccnId,
|
||||
AutofillValue.forText(field.protectedValue.stringValue)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (field.name == TemplateField.LABEL_CVV) {
|
||||
struct.cardVerificationValueId?.let { cvvId ->
|
||||
builder.setValue(cvvId, AutofillValue.forText(field.protectedValue.stringValue))
|
||||
datasetBuilder.addValueToDatasetBuilder(
|
||||
cvvId,
|
||||
AutofillValue.forText(field.protectedValue.stringValue)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
additionalBuild?.invoke(builder)
|
||||
|
||||
return try {
|
||||
builder.build()
|
||||
} catch (e: Exception) {
|
||||
// at least one value must be set
|
||||
null
|
||||
}
|
||||
val dataset = datasetBuilder.build()
|
||||
Log.d(TAG, "Autofill Dataset $dataset created")
|
||||
return dataset
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to assign a drawable to a new icon from a database icon
|
||||
*/
|
||||
private fun buildIconFromEntry(context: Context,
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
entryInfo: EntryInfo): Icon? {
|
||||
try {
|
||||
database.iconDrawableFactory.getBitmapFromIcon(context,
|
||||
@@ -227,10 +310,10 @@ object AutofillHelper {
|
||||
return null
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
@SuppressLint("RestrictedApi")
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private fun buildInlinePresentationForEntry(context: Context,
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
|
||||
positionItem: Int,
|
||||
entryInfo: EntryInfo): InlinePresentation? {
|
||||
@@ -302,7 +385,7 @@ object AutofillHelper {
|
||||
}
|
||||
|
||||
fun buildResponse(context: Context,
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
entriesInfo: List<EntryInfo>,
|
||||
parseResult: StructureParser.Result,
|
||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? {
|
||||
@@ -334,25 +417,33 @@ object AutofillHelper {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
entriesInfo.forEachIndexed { _, entry ->
|
||||
if (numberInlineSuggestions > 0
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& compatInlineSuggestionsRequest != null) {
|
||||
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult) { builder ->
|
||||
buildInlinePresentationForEntry(context, database,
|
||||
compatInlineSuggestionsRequest, numberInlineSuggestions--, entry
|
||||
)?.let { inlinePresentation ->
|
||||
builder.setInlinePresentation(inlinePresentation)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult))
|
||||
try {
|
||||
// Build inline presentation for compatible keyboard
|
||||
var inlinePresentation: InlinePresentation? = null
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& numberInlineSuggestions > 0
|
||||
&& compatInlineSuggestionsRequest != null) {
|
||||
inlinePresentation = buildInlinePresentationForEntry(
|
||||
context,
|
||||
database,
|
||||
compatInlineSuggestionsRequest,
|
||||
numberInlineSuggestions--,
|
||||
entry
|
||||
)
|
||||
}
|
||||
// Create dataset for each entry
|
||||
responseBuilder.addDataset(
|
||||
buildDatasetForEntry(context, database, entry, parseResult, inlinePresentation)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to add dataset")
|
||||
}
|
||||
}
|
||||
|
||||
// Add a new dataset for manual selection
|
||||
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
||||
val searchInfo = SearchInfo().apply {
|
||||
applicationId = parseResult.applicationId
|
||||
@@ -364,29 +455,50 @@ object AutofillHelper {
|
||||
val pendingIntent = AutofillLauncherActivity.getPendingIntentForSelection(context,
|
||||
searchInfo, compatInlineSuggestionsRequest)
|
||||
|
||||
parseResult.allAutofillIds().let { autofillIds ->
|
||||
autofillIds.forEach { id ->
|
||||
val builder = Dataset.Builder(manualSelectionView)
|
||||
var inlinePresentation: InlinePresentation? = null
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
||||
inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent)
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||
val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
||||
val inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent)
|
||||
inlinePresentation?.let {
|
||||
builder.setInlinePresentation(it)
|
||||
}
|
||||
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
Dataset.Builder(Presentations.Builder()
|
||||
.apply {
|
||||
inlinePresentation?.let {
|
||||
setInlinePresentation(it)
|
||||
}
|
||||
}
|
||||
.setDialogPresentation(manualSelectionView)
|
||||
.setMenuPresentation(manualSelectionView)
|
||||
.build())
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
Dataset.Builder(manualSelectionView).apply {
|
||||
inlinePresentation?.let {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
setInlinePresentation(it)
|
||||
}
|
||||
}
|
||||
builder.setValue(id, null)
|
||||
builder.setAuthentication(pendingIntent.intentSender)
|
||||
responseBuilder.addDataset(builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
parseResult.allAutofillIds().let { autofillIds ->
|
||||
autofillIds.forEach { id ->
|
||||
datasetBuilder.addValueToDatasetBuilder(id, null)
|
||||
datasetBuilder.setAuthentication(pendingIntent.intentSender)
|
||||
}
|
||||
val dataset = datasetBuilder.build()
|
||||
Log.d(TAG, "Autofill Dataset for manual selection $dataset created")
|
||||
responseBuilder.addDataset(dataset)
|
||||
}
|
||||
}
|
||||
|
||||
return try {
|
||||
responseBuilder.build()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to create Autofill response", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -395,7 +507,7 @@ object AutofillHelper {
|
||||
* Build the Autofill response for one entry
|
||||
*/
|
||||
fun buildResponseAndSetResult(activity: Activity,
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
entryInfo: EntryInfo) {
|
||||
buildResponseAndSetResult(activity, database, ArrayList<EntryInfo>().apply { add(entryInfo) })
|
||||
}
|
||||
@@ -404,17 +516,17 @@ object AutofillHelper {
|
||||
* Build the Autofill response for many entry
|
||||
*/
|
||||
fun buildResponseAndSetResult(activity: Activity,
|
||||
database: Database,
|
||||
database: ContextualDatabase,
|
||||
entriesInfo: List<EntryInfo>) {
|
||||
if (entriesInfo.isEmpty()) {
|
||||
activity.setResult(Activity.RESULT_CANCELED)
|
||||
} else {
|
||||
var setResultOk = false
|
||||
activity.intent?.getParcelableExtra<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { structure ->
|
||||
activity.intent?.getParcelableExtraCompat<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { structure ->
|
||||
StructureParser(structure).parse()?.let { result ->
|
||||
// New Response
|
||||
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtra<CompatInlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
||||
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtraCompat<CompatInlineSuggestionsRequest>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
||||
if (compatInlineSuggestionsRequest != null) {
|
||||
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@@ -423,7 +535,7 @@ object AutofillHelper {
|
||||
buildResponse(activity, database, entriesInfo, result, null)
|
||||
}
|
||||
val mReplyIntent = Intent()
|
||||
Log.d(activity.javaClass.name, "Successed Autofill auth.")
|
||||
Log.d(activity.javaClass.name, "Success Autofill auth.")
|
||||
mReplyIntent.putExtra(
|
||||
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
||||
response)
|
||||
@@ -478,4 +590,6 @@ object AutofillHelper {
|
||||
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
|
||||
activityResultLauncher?.launch(intent)
|
||||
}
|
||||
|
||||
private val TAG = AutofillHelper::class.java.name
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import android.os.Parcelable
|
||||
import android.service.autofill.FillRequest
|
||||
import android.view.inputmethod.InlineSuggestionsRequest
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.kunzisoft.keepass.utils.readParcelableCompat
|
||||
|
||||
/**
|
||||
* Utility class only to prevent java.lang.NoClassDefFoundError for old Android version and new lib compilation
|
||||
@@ -52,8 +53,7 @@ class CompatInlineSuggestionsRequest : Parcelable {
|
||||
|
||||
constructor(parcel: Parcel) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
this.inlineSuggestionsRequest =
|
||||
parcel.readParcelable(FillRequest::class.java.classLoader)
|
||||
this.inlineSuggestionsRequest = parcel.readParcelableCompat()
|
||||
}
|
||||
else {
|
||||
this.inlineSuggestionsRequest = null
|
||||
|
||||
@@ -29,35 +29,33 @@ import android.os.CancellationSignal
|
||||
import android.service.autofill.*
|
||||
import android.util.Log
|
||||
import android.view.autofill.AutofillId
|
||||
import android.view.inputmethod.InlineSuggestionsRequest
|
||||
import android.widget.RemoteViews
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.autofill.inline.UiVersions
|
||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
||||
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||
import com.kunzisoft.keepass.model.CreditCard
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.WebDomain
|
||||
import org.joda.time.DateTime
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
class KeeAutofillService : AutofillService() {
|
||||
|
||||
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
||||
private var mDatabase: Database? = null
|
||||
private var mDatabase: ContextualDatabase? = null
|
||||
private var applicationIdBlocklist: Set<String>? = null
|
||||
private var webDomainBlocklist: Set<String>? = null
|
||||
private var askToSaveData: Boolean = false
|
||||
private var autofillInlineSuggestionsEnabled: Boolean = false
|
||||
private var mLock = AtomicBoolean()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@@ -90,41 +88,43 @@ class KeeAutofillService : AutofillService() {
|
||||
|
||||
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
|
||||
|
||||
// Lock
|
||||
if (!mLock.get()) {
|
||||
mLock.set(true)
|
||||
// Check user's settings for authenticating Responses and Datasets.
|
||||
val latestStructure = request.fillContexts.last().structure
|
||||
StructureParser(latestStructure).parse()?.let { parseResult ->
|
||||
if (request.flags and FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST != 0) {
|
||||
Log.d(TAG, "Autofill requested in compatibility mode")
|
||||
} else {
|
||||
Log.d(TAG, "Autofill requested in native mode")
|
||||
}
|
||||
|
||||
// Build search info only if applicationId or webDomain are not blocked
|
||||
if (autofillAllowedFor(parseResult.applicationId, applicationIdBlocklist)
|
||||
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
|
||||
val searchInfo = SearchInfo().apply {
|
||||
applicationId = parseResult.applicationId
|
||||
webDomain = parseResult.webDomain
|
||||
webScheme = parseResult.webScheme
|
||||
}
|
||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
|
||||
searchInfo.webDomain = webDomainWithoutSubDomain
|
||||
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& autofillInlineSuggestionsEnabled) {
|
||||
CompatInlineSuggestionsRequest(request)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
launchSelection(mDatabase,
|
||||
searchInfo,
|
||||
parseResult,
|
||||
inlineSuggestionsRequest,
|
||||
callback)
|
||||
// Check user's settings for authenticating Responses and Datasets.
|
||||
val latestStructure = request.fillContexts.last().structure
|
||||
StructureParser(latestStructure).parse()?.let { parseResult ->
|
||||
|
||||
// Build search info only if applicationId or webDomain are not blocked
|
||||
if (autofillAllowedFor(parseResult.applicationId, applicationIdBlocklist)
|
||||
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
|
||||
val searchInfo = SearchInfo().apply {
|
||||
applicationId = parseResult.applicationId
|
||||
webDomain = parseResult.webDomain
|
||||
webScheme = parseResult.webScheme
|
||||
}
|
||||
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
|
||||
searchInfo.webDomain = webDomainWithoutSubDomain
|
||||
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||
&& autofillInlineSuggestionsEnabled) {
|
||||
CompatInlineSuggestionsRequest(request)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
launchSelection(mDatabase,
|
||||
searchInfo,
|
||||
parseResult,
|
||||
inlineSuggestionsRequest,
|
||||
callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchSelection(database: Database?,
|
||||
private fun launchSelection(database: ContextualDatabase?,
|
||||
searchInfo: SearchInfo,
|
||||
parseResult: StructureParser.Result,
|
||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||
@@ -153,10 +153,11 @@ class KeeAutofillService : AutofillService() {
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
||||
database: Database?,
|
||||
database: ContextualDatabase?,
|
||||
searchInfo: SearchInfo,
|
||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||
callback: FillCallback) {
|
||||
var success = false
|
||||
parseResult.allAutofillIds().let { autofillIds ->
|
||||
if (autofillIds.isNotEmpty()) {
|
||||
// If the entire Autofill Response is authenticated, AuthActivity is used
|
||||
@@ -279,17 +280,44 @@ class KeeAutofillService : AutofillService() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build response
|
||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
try {
|
||||
// Buggy method on some API 33 devices
|
||||
responseBuilder.setAuthentication(
|
||||
autofillIds,
|
||||
intentSender,
|
||||
Presentations.Builder().apply {
|
||||
inlinePresentation?.let {
|
||||
setInlinePresentation(it)
|
||||
}
|
||||
setDialogPresentation(remoteViewsUnlock)
|
||||
}.build()
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to use the new setAuthentication method.", e)
|
||||
@Suppress("DEPRECATION")
|
||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
|
||||
}
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
|
||||
}
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
|
||||
}
|
||||
success = true
|
||||
callback.onSuccess(responseBuilder.build())
|
||||
}
|
||||
}
|
||||
if (!success)
|
||||
callback.onFailure("Unable to get Autofill ids for UI selection")
|
||||
}
|
||||
|
||||
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
|
||||
var success = false
|
||||
if (askToSaveData) {
|
||||
val latestStructure = request.fillContexts.last().structure
|
||||
StructureParser(latestStructure).parse(true)?.let { parseResult ->
|
||||
@@ -332,14 +360,16 @@ class KeeAutofillService : AutofillService() {
|
||||
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,
|
||||
// registerInfo))
|
||||
//} else {
|
||||
AutofillLauncherActivity.launchForRegistration(this, registerInfo)
|
||||
callback.onSuccess()
|
||||
AutofillLauncherActivity.launchForRegistration(this, registerInfo)
|
||||
success = true
|
||||
callback.onSuccess()
|
||||
//}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
callback.onFailure("Saving form values is not allowed")
|
||||
if (!success) {
|
||||
callback.onFailure("Saving form values is not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConnected() {
|
||||
@@ -348,7 +378,6 @@ class KeeAutofillService : AutofillService() {
|
||||
}
|
||||
|
||||
override fun onDisconnected() {
|
||||
mLock.set(false)
|
||||
Log.d(TAG, "onDisconnected")
|
||||
}
|
||||
|
||||
|
||||
@@ -32,24 +32,25 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.getkeepsafe.taptargetview.TapTargetView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.database.exception.IODatabaseException
|
||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
|
||||
import com.kunzisoft.keepass.model.CipherDecryptDatabase
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.CredentialStorage
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||
import com.kunzisoft.keepass.view.hideByFading
|
||||
import com.kunzisoft.keepass.view.showByFading
|
||||
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedUnlockCallback {
|
||||
class AdvancedUnlockFragment: Fragment(), AdvancedUnlockManager.AdvancedUnlockCallback {
|
||||
|
||||
private var mBuilderListener: BuilderListener? = null
|
||||
|
||||
@@ -66,17 +67,6 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
// TODO Retrieve credential storage from app database
|
||||
var credentialDatabaseStorage: CredentialStorage = CredentialStorage.DEFAULT
|
||||
|
||||
/**
|
||||
* Manage setting to auto open biometric prompt
|
||||
*/
|
||||
private var mAutoOpenPrompt: Boolean
|
||||
get() {
|
||||
return mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt && mAutoOpenPromptEnabled
|
||||
}
|
||||
set(value) {
|
||||
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = value
|
||||
}
|
||||
|
||||
// Variable to check if the prompt can be open (if the right activity is currently shown)
|
||||
// checkBiometricAvailability() allows open biometric prompt and onDestroy() removes the authorization
|
||||
private var allowOpenBiometricPrompt = false
|
||||
@@ -94,15 +84,37 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
// Only keep connection when we request a device credential activity
|
||||
private var keepConnection = false
|
||||
|
||||
private var mDeviceCredentialResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
private var mDeviceCredentialResultLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
|
||||
// To wait resume
|
||||
if (keepConnection) {
|
||||
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = result.resultCode == Activity.RESULT_OK
|
||||
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded =
|
||||
result.resultCode == Activity.RESULT_OK
|
||||
}
|
||||
keepConnection = false
|
||||
}
|
||||
|
||||
private val menuProvider: MenuProvider = object: MenuProvider {
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// biometric menu
|
||||
if (mAllowAdvancedUnlockMenu)
|
||||
menuInflater.inflate(R.menu.advanced_unlock, menu)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
when (menuItem.itemId) {
|
||||
R.id.menu_keystore_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
deleteEncryptedDatabaseKey()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
|
||||
@@ -121,8 +133,6 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext)
|
||||
|
||||
mAdvancedUnlockViewModel.onInitAdvancedUnlockModeRequested.observe(this) {
|
||||
@@ -141,14 +151,19 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
val rootView = inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.fragment_advanced_unlock, container, false)
|
||||
val rootView = inflater.inflate(R.layout.fragment_advanced_unlock, container, false)
|
||||
|
||||
mAdvancedUnlockInfoView = rootView.findViewById(R.id.advanced_unlock_view)
|
||||
|
||||
return rootView
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
context?.let {
|
||||
@@ -158,26 +173,6 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
keepConnection = false
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// biometric menu
|
||||
if (mAllowAdvancedUnlockMenu)
|
||||
inflater.inflate(R.menu.advanced_unlock, menu)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_keystore_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
deleteEncryptedDatabaseKey()
|
||||
}
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun onDatabaseLoaded(databaseUri: Uri?) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// To get device credential unlock result, only if same database uri
|
||||
@@ -214,8 +209,6 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
allowOpenBiometricPrompt = true
|
||||
if (PreferencesUtil.isBiometricUnlockEnable(context)) {
|
||||
mAdvancedUnlockInfoView?.setIconResource(R.drawable.fingerprint)
|
||||
|
||||
// biometric not supported (by API level or hardware) so keep option hidden
|
||||
// or manually disable
|
||||
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(context)
|
||||
@@ -234,7 +227,6 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
}
|
||||
}
|
||||
} else if (PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
|
||||
mAdvancedUnlockInfoView?.setIconResource(R.drawable.bolt)
|
||||
if (AdvancedUnlockManager.isDeviceSecure(context)) {
|
||||
selectMode()
|
||||
} else {
|
||||
@@ -290,14 +282,29 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
private fun initNotAvailable() {
|
||||
showViews(false)
|
||||
|
||||
mAdvancedUnlockInfoView?.setIconViewClickListener(false, null)
|
||||
mAdvancedUnlockInfoView?.setIconViewClickListener(null)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun openBiometricSetting() {
|
||||
mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
|
||||
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
|
||||
context?.startActivity(Intent(Settings.ACTION_SETTINGS))
|
||||
mAdvancedUnlockInfoView?.setIconViewClickListener {
|
||||
try {
|
||||
when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
||||
context?.startActivity(Intent(Settings.ACTION_BIOMETRIC_ENROLL))
|
||||
}
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> {
|
||||
@Suppress("DEPRECATION") context
|
||||
?.startActivity(Intent(Settings.ACTION_FINGERPRINT_ENROLL))
|
||||
}
|
||||
else -> {
|
||||
context?.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
|
||||
context?.startActivity(Intent(Settings.ACTION_SETTINGS))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,11 +336,11 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun initWaitData() {
|
||||
showViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.no_credentials_stored)
|
||||
setAdvancedUnlockedTitleView(R.string.unavailable)
|
||||
setAdvancedUnlockedMessageView("")
|
||||
|
||||
context?.let { context ->
|
||||
mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
|
||||
mAdvancedUnlockInfoView?.setIconViewClickListener {
|
||||
onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
|
||||
context.getString(R.string.credential_before_click_advanced_unlock_button))
|
||||
}
|
||||
@@ -360,7 +367,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun initEncryptData() {
|
||||
showViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_store_credential)
|
||||
setAdvancedUnlockedTitleView(R.string.unlock_and_link_biometric)
|
||||
setAdvancedUnlockedMessageView("")
|
||||
|
||||
advancedUnlockManager?.initEncryptData { cryptoPrompt ->
|
||||
@@ -374,7 +381,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun initDecryptData() {
|
||||
showViews(true)
|
||||
setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_unlock_database)
|
||||
setAdvancedUnlockedTitleView(R.string.unlock)
|
||||
setAdvancedUnlockedMessageView("")
|
||||
|
||||
advancedUnlockManager?.let { unlockHelper ->
|
||||
@@ -389,14 +396,15 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
}
|
||||
|
||||
// Auto open the biometric prompt
|
||||
if (mAutoOpenPrompt) {
|
||||
mAutoOpenPrompt = false
|
||||
if (mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt
|
||||
&& mAutoOpenPromptEnabled) {
|
||||
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
|
||||
openAdvancedUnlockPrompt(cryptoPrompt)
|
||||
}
|
||||
}
|
||||
} ?: deleteEncryptedDatabaseKey()
|
||||
}
|
||||
} ?: throw IODatabaseException()
|
||||
} ?: throw UnknownDatabaseLocationException()
|
||||
} ?: throw Exception("AdvancedUnlockManager not initialized")
|
||||
}
|
||||
|
||||
@@ -579,10 +587,13 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
|
||||
private fun showViews(show: Boolean) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
mAdvancedUnlockInfoView?.visibility = if (show)
|
||||
View.VISIBLE
|
||||
if (show) {
|
||||
if (mAdvancedUnlockInfoView?.visibility != View.VISIBLE)
|
||||
mAdvancedUnlockInfoView?.showByFading()
|
||||
}
|
||||
else {
|
||||
View.GONE
|
||||
if (mAdvancedUnlockInfoView?.visibility == View.VISIBLE)
|
||||
mAdvancedUnlockInfoView?.hideByFading()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -604,30 +615,10 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun setAdvancedUnlockedMessageView(text: CharSequence) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
mAdvancedUnlockInfoView?.message = text
|
||||
mAdvancedUnlockInfoView?.setMessage(text)
|
||||
}
|
||||
}
|
||||
|
||||
fun performEducation(passwordActivityEducation: PasswordActivityEducation,
|
||||
readOnlyEducationPerformed: Boolean,
|
||||
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
|
||||
onOuterViewClick: ((TapTargetView?) -> Unit)? = null) {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& !readOnlyEducationPerformed) {
|
||||
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(requireContext())
|
||||
PreferencesUtil.isAdvancedUnlockEnable(requireContext())
|
||||
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
||||
&& mAdvancedUnlockInfoView != null && mAdvancedUnlockInfoView?.visibility == View.VISIBLE
|
||||
&& mAdvancedUnlockInfoView?.unlockIconImageView != null
|
||||
&& passwordActivityEducation.checkAndPerformedBiometricEducation(mAdvancedUnlockInfoView!!.unlockIconImageView!!,
|
||||
onEducationViewClick,
|
||||
onOuterViewClick)
|
||||
}
|
||||
} catch (ignored: Exception) {}
|
||||
}
|
||||
|
||||
enum class Mode {
|
||||
BIOMETRIC_UNAVAILABLE,
|
||||
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
|
||||
|
||||
@@ -124,7 +124,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSecretKey(): SecretKey? {
|
||||
@Synchronized private fun getSecretKey(): SecretKey? {
|
||||
if (!isKeyManagerInitialized) {
|
||||
return null
|
||||
}
|
||||
@@ -141,8 +141,8 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
KeyGenParameterSpec.Builder(
|
||||
ADVANCED_UNLOCK_KEYSTORE_KEY,
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
|
||||
.setBlockModes(ADVANCED_UNLOCK_BLOCKS_MODES)
|
||||
.setEncryptionPaddings(ADVANCED_UNLOCK_ENCRYPTION_PADDING)
|
||||
.apply {
|
||||
// Require the user to authenticate with a fingerprint to authorize every use
|
||||
// of the key, don't use it for device credential because it's the user authentication
|
||||
@@ -173,11 +173,11 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
return null
|
||||
}
|
||||
|
||||
fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,) {
|
||||
@Synchronized fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,) {
|
||||
initEncryptData(actionIfCypherInit, true)
|
||||
}
|
||||
|
||||
private fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
|
||||
@Synchronized private fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
|
||||
firstLaunch: Boolean) {
|
||||
if (!isKeyManagerInitialized) {
|
||||
return
|
||||
@@ -213,7 +213,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
}
|
||||
|
||||
fun encryptData(value: ByteArray) {
|
||||
@Synchronized fun encryptData(value: ByteArray) {
|
||||
if (!isKeyManagerInitialized) {
|
||||
return
|
||||
}
|
||||
@@ -229,12 +229,12 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
}
|
||||
|
||||
fun initDecryptData(ivSpecValue: ByteArray,
|
||||
@Synchronized fun initDecryptData(ivSpecValue: ByteArray,
|
||||
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
|
||||
initDecryptData(ivSpecValue, actionIfCypherInit, true)
|
||||
}
|
||||
|
||||
private fun initDecryptData(ivSpecValue: ByteArray,
|
||||
@Synchronized private fun initDecryptData(ivSpecValue: ByteArray,
|
||||
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
|
||||
firstLaunch: Boolean = true) {
|
||||
if (!isKeyManagerInitialized) {
|
||||
@@ -278,7 +278,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptData(encryptedValue: ByteArray) {
|
||||
@Synchronized fun decryptData(encryptedValue: ByteArray) {
|
||||
if (!isKeyManagerInitialized) {
|
||||
return
|
||||
}
|
||||
@@ -296,7 +296,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteKeystoreKey() {
|
||||
@Synchronized fun deleteKeystoreKey() {
|
||||
try {
|
||||
keyStore?.load(null)
|
||||
keyStore?.deleteEntry(ADVANCED_UNLOCK_KEYSTORE_KEY)
|
||||
@@ -306,7 +306,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
}
|
||||
|
||||
fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt,
|
||||
@Synchronized fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt,
|
||||
deviceCredentialResultLauncher: ActivityResultLauncher<Intent>
|
||||
) {
|
||||
// Init advanced unlock prompt
|
||||
@@ -346,7 +346,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
}
|
||||
|
||||
fun closeBiometricPrompt() {
|
||||
@Synchronized fun closeBiometricPrompt() {
|
||||
biometricPrompt?.cancelAuthentication()
|
||||
}
|
||||
|
||||
@@ -403,13 +403,11 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
fun isDeviceSecure(context: Context): Boolean {
|
||||
val keyguardManager = ContextCompat.getSystemService(context, KeyguardManager::class.java)
|
||||
return keyguardManager?.isDeviceSecure ?: false
|
||||
return ContextCompat.getSystemService(context, KeyguardManager::class.java)
|
||||
?.isDeviceSecure ?: false
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
fun biometricUnlockSupported(context: Context): Boolean {
|
||||
val biometricCanAuthenticate = try {
|
||||
BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG)
|
||||
@@ -430,28 +428,23 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
||||
)
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
fun deviceCredentialUnlockSupported(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL)
|
||||
return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|
||||
(biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
|
||||
)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
ContextCompat.getSystemService(context, KeyguardManager::class.java)?.apply {
|
||||
return isDeviceSecure
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove entry key in keystore
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
fun deleteEntryKeyInKeystoreForBiometric(fragmentActivity: FragmentActivity,
|
||||
advancedCallback: AdvancedUnlockErrorCallback) {
|
||||
AdvancedUnlockManager{ fragmentActivity }.apply {
|
||||
|
||||
@@ -1,63 +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.biometric
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.widget.ImageView
|
||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
class FingerPrintAnimatedVector(context: Context, imageView: ImageView) {
|
||||
|
||||
private val scanFingerprint: AnimatedVectorDrawableCompat? =
|
||||
AnimatedVectorDrawableCompat.create(context, R.drawable.scan_fingerprint)
|
||||
|
||||
init {
|
||||
imageView.setImageDrawable(scanFingerprint)
|
||||
}
|
||||
|
||||
private var animationCallback = object : Animatable2Compat.AnimationCallback() {
|
||||
override fun onAnimationEnd(drawable: Drawable) {
|
||||
imageView.post {
|
||||
scanFingerprint?.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startScan() {
|
||||
scanFingerprint?.registerAnimationCallback(animationCallback)
|
||||
|
||||
if (scanFingerprint?.isRunning != true)
|
||||
scanFingerprint?.start()
|
||||
}
|
||||
|
||||
fun stopScan() {
|
||||
scanFingerprint?.unregisterAnimationCallback(animationCallback)
|
||||
|
||||
if (scanFingerprint?.isRunning == true)
|
||||
scanFingerprint.stop()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.kunzisoft.keepass.database
|
||||
|
||||
import android.net.Uri
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
import com.kunzisoft.keepass.utils.SingletonHolder
|
||||
import java.io.File
|
||||
|
||||
class ContextualDatabase: Database() {
|
||||
|
||||
var fileUri: Uri? = null
|
||||
|
||||
val iconDrawableFactory = IconDrawableFactory(
|
||||
retrieveBinaryCache = { binaryCache },
|
||||
retrieveCustomIconBinary = { iconId -> getBinaryForCustomIcon(iconId) }
|
||||
)
|
||||
|
||||
override fun removeCustomIcon(customIcon: IconImageCustom) {
|
||||
iconDrawableFactory.clearFromCache(customIcon)
|
||||
super.removeCustomIcon(customIcon)
|
||||
}
|
||||
|
||||
override fun clearIndexesAndBinaries(filesDirectory: File?) {
|
||||
iconDrawableFactory.clearCache()
|
||||
super.clearIndexesAndBinaries(filesDirectory)
|
||||
}
|
||||
|
||||
override fun clearAndClose(filesDirectory: File?) {
|
||||
super.clearAndClose(filesDirectory)
|
||||
this.fileUri = null
|
||||
}
|
||||
|
||||
companion object : SingletonHolder<ContextualDatabase>(::ContextualDatabase) {
|
||||
private val TAG = ContextualDatabase::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -17,17 +17,29 @@
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.action
|
||||
package com.kunzisoft.keepass.database
|
||||
|
||||
import android.app.Service
|
||||
import android.content.*
|
||||
import android.content.Context.*
|
||||
import android.Manifest
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Context.BIND_ABOVE_CLIENT
|
||||
import android.content.Context.BIND_AUTO_CREATE
|
||||
import android.content.Context.BIND_IMPORTANT
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.ServiceConnection
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.kunzisoft.keepass.R
|
||||
@@ -35,7 +47,6 @@ import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
@@ -43,10 +54,10 @@ import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_CHALLENGE_RESPONDED
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
||||
@@ -82,26 +93,33 @@ import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
|
||||
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
|
||||
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
||||
import com.kunzisoft.keepass.utils.putParcelableList
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Utility class to connect an activity or a service to the DatabaseTaskNotificationService,
|
||||
* Useful to retrieve a database instance and sending tasks commands
|
||||
*/
|
||||
class DatabaseTaskProvider {
|
||||
class DatabaseTaskProvider(
|
||||
private var context: Context,
|
||||
private var showDialog: Boolean = true
|
||||
) {
|
||||
|
||||
private var activity: FragmentActivity? = null
|
||||
private var service: Service? = null
|
||||
private var context: Context
|
||||
// To show dialog only if context is an activity
|
||||
private var activity: FragmentActivity? = try { context as? FragmentActivity? }
|
||||
catch (_: Exception) { null }
|
||||
|
||||
var onDatabaseRetrieved: ((database: Database?) -> Unit)? = null
|
||||
var onDatabaseRetrieved: ((database: ContextualDatabase?) -> Unit)? = null
|
||||
|
||||
var onActionFinish: ((database: Database,
|
||||
var onActionFinish: ((database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result) -> Unit)? = null
|
||||
|
||||
private var intentDatabaseTask: Intent
|
||||
private var intentDatabaseTask: Intent = Intent(
|
||||
context.applicationContext,
|
||||
DatabaseTaskNotificationService::class.java
|
||||
)
|
||||
|
||||
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
|
||||
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
|
||||
@@ -111,34 +129,49 @@ class DatabaseTaskProvider {
|
||||
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
|
||||
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
|
||||
|
||||
constructor(activity: FragmentActivity) {
|
||||
this.activity = activity
|
||||
this.context = activity
|
||||
this.intentDatabaseTask = Intent(activity.applicationContext,
|
||||
DatabaseTaskNotificationService::class.java)
|
||||
}
|
||||
|
||||
constructor(service: Service) {
|
||||
this.service = service
|
||||
this.context = service
|
||||
this.intentDatabaseTask = Intent(service.applicationContext,
|
||||
DatabaseTaskNotificationService::class.java)
|
||||
fun destroy() {
|
||||
this.activity = null
|
||||
this.onDatabaseRetrieved = null
|
||||
this.onActionFinish = null
|
||||
this.databaseTaskBroadcastReceiver = null
|
||||
this.mBinder = null
|
||||
this.serviceConnection = null
|
||||
this.progressTaskDialogFragment = null
|
||||
this.databaseChangedDialogFragment = null
|
||||
}
|
||||
|
||||
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
|
||||
override fun onStartAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||
startDialog(titleId, messageId, warningId)
|
||||
override fun onActionStarted(
|
||||
database: ContextualDatabase,
|
||||
progressMessage: ProgressMessage
|
||||
) {
|
||||
if (showDialog)
|
||||
startDialog(progressMessage)
|
||||
}
|
||||
|
||||
override fun onUpdateAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||
updateDialog(titleId, messageId, warningId)
|
||||
override fun onActionUpdated(
|
||||
database: ContextualDatabase,
|
||||
progressMessage: ProgressMessage
|
||||
) {
|
||||
if (showDialog)
|
||||
updateDialog(progressMessage)
|
||||
}
|
||||
|
||||
override fun onStopAction(database: Database, actionTask: String, result: ActionRunnable.Result) {
|
||||
onActionFinish?.invoke(database, actionTask, result)
|
||||
override fun onActionStopped(
|
||||
database: ContextualDatabase
|
||||
) {
|
||||
// Remove the progress task
|
||||
stopDialog()
|
||||
}
|
||||
|
||||
override fun onActionFinished(
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
onActionFinish?.invoke(database, actionTask, result)
|
||||
onActionStopped(database)
|
||||
}
|
||||
}
|
||||
|
||||
private val mActionDatabaseListener = object: DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
|
||||
@@ -147,7 +180,8 @@ class DatabaseTaskProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private var databaseInfoListener = object: DatabaseTaskNotificationService.DatabaseInfoListener {
|
||||
private var databaseInfoListener = object:
|
||||
DatabaseTaskNotificationService.DatabaseInfoListener {
|
||||
override fun onDatabaseInfoChanged(previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||
newDatabaseInfo: SnapFileDatabaseInfo) {
|
||||
activity?.let { activity ->
|
||||
@@ -176,14 +210,12 @@ class DatabaseTaskProvider {
|
||||
}
|
||||
|
||||
private var databaseListener = object: DatabaseTaskNotificationService.DatabaseListener {
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||
onDatabaseRetrieved?.invoke(database)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startDialog(titleId: Int? = null,
|
||||
messageId: Int? = null,
|
||||
warningId: Int? = null) {
|
||||
private fun startDialog(progressMessage: ProgressMessage) {
|
||||
activity?.let { activity ->
|
||||
activity.lifecycleScope.launch {
|
||||
if (progressTaskDialogFragment == null) {
|
||||
@@ -197,22 +229,17 @@ class DatabaseTaskProvider {
|
||||
PROGRESS_TASK_DIALOG_TAG
|
||||
)
|
||||
}
|
||||
updateDialog(titleId, messageId, warningId)
|
||||
updateDialog(progressMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDialog(titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||
private fun updateDialog(progressMessage: ProgressMessage) {
|
||||
progressTaskDialogFragment?.apply {
|
||||
titleId?.let {
|
||||
updateTitle(it)
|
||||
}
|
||||
messageId?.let {
|
||||
updateMessage(it)
|
||||
}
|
||||
warningId?.let {
|
||||
updateWarning(it)
|
||||
}
|
||||
updateTitle(progressMessage.titleId)
|
||||
updateMessage(progressMessage.messageId)
|
||||
updateWarning(progressMessage.warningId)
|
||||
setCancellable(progressMessage.cancelable)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,11 +251,17 @@ class DatabaseTaskProvider {
|
||||
private fun initServiceConnection() {
|
||||
if (serviceConnection == null) {
|
||||
serviceConnection = object : ServiceConnection {
|
||||
override fun onBindingDied(name: ComponentName?) {
|
||||
stopDialog()
|
||||
}
|
||||
|
||||
override fun onNullBinding(name: ComponentName?) {
|
||||
stopDialog()
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
|
||||
addDatabaseListener(databaseListener)
|
||||
addDatabaseFileInfoListener(databaseInfoListener)
|
||||
addActionTaskListener(actionTaskListener)
|
||||
addServiceListeners(this)
|
||||
getService().checkDatabase()
|
||||
getService().checkDatabaseInfo()
|
||||
getService().checkAction()
|
||||
@@ -236,19 +269,29 @@ class DatabaseTaskProvider {
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
mBinder?.removeActionTaskListener(actionTaskListener)
|
||||
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
|
||||
mBinder?.removeDatabaseListener(databaseListener)
|
||||
removeServiceListeners(mBinder)
|
||||
mBinder = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
|
||||
service?.addDatabaseListener(databaseListener)
|
||||
service?.addDatabaseFileInfoListener(databaseInfoListener)
|
||||
service?.addActionTaskListener(actionTaskListener)
|
||||
}
|
||||
|
||||
private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
|
||||
service?.removeActionTaskListener(actionTaskListener)
|
||||
service?.removeDatabaseFileInfoListener(databaseInfoListener)
|
||||
service?.removeDatabaseListener(databaseListener)
|
||||
}
|
||||
|
||||
private fun bindService() {
|
||||
initServiceConnection()
|
||||
serviceConnection?.let {
|
||||
context.bindService(intentDatabaseTask, it, BIND_AUTO_CREATE or BIND_NOT_FOREGROUND or BIND_ABOVE_CLIENT)
|
||||
context.bindService(intentDatabaseTask, it, BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,19 +299,18 @@ class DatabaseTaskProvider {
|
||||
* Unbind the service and assign null to the service connection to check if already unbind or not
|
||||
*/
|
||||
private fun unBindService() {
|
||||
serviceConnection?.let {
|
||||
context.unbindService(it)
|
||||
try {
|
||||
serviceConnection?.let {
|
||||
context.unbindService(it)
|
||||
}
|
||||
} catch (e: java.lang.Exception) {
|
||||
Log.e(TAG, "Unable to unbind the database task service", e)
|
||||
} finally {
|
||||
serviceConnection = null
|
||||
}
|
||||
serviceConnection = null
|
||||
}
|
||||
|
||||
fun isBinded(): Boolean {
|
||||
return mBinder != null
|
||||
}
|
||||
|
||||
fun registerProgressTask() {
|
||||
stopDialog()
|
||||
|
||||
// Register a database task receiver to stop loading dialog when service finish the task
|
||||
databaseTaskBroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
@@ -279,17 +321,16 @@ class DatabaseTaskProvider {
|
||||
}
|
||||
DATABASE_STOP_TASK_ACTION -> {
|
||||
// Remove the progress task
|
||||
stopDialog()
|
||||
unBindService()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
context.registerReceiver(databaseTaskBroadcastReceiver,
|
||||
IntentFilter().apply {
|
||||
addAction(DATABASE_START_TASK_ACTION)
|
||||
addAction(DATABASE_STOP_TASK_ACTION)
|
||||
}
|
||||
IntentFilter().apply {
|
||||
addAction(DATABASE_START_TASK_ACTION)
|
||||
addAction(DATABASE_STOP_TASK_ACTION)
|
||||
}
|
||||
)
|
||||
|
||||
// Check if a service is currently running else do nothing
|
||||
@@ -297,11 +338,7 @@ class DatabaseTaskProvider {
|
||||
}
|
||||
|
||||
fun unregisterProgressTask() {
|
||||
stopDialog()
|
||||
|
||||
mBinder?.removeActionTaskListener(actionTaskListener)
|
||||
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
|
||||
mBinder?.removeDatabaseListener(databaseListener)
|
||||
removeServiceListeners(mBinder)
|
||||
mBinder = null
|
||||
|
||||
unBindService()
|
||||
@@ -313,7 +350,50 @@ class DatabaseTaskProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private val tempServiceParameters = mutableListOf<Pair<Bundle?, String>>()
|
||||
private val requestPermissionLauncher = activity?.registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { _ ->
|
||||
// Whether or not the user has accepted, the service can be started,
|
||||
// There just won't be any notification if it's not allowed.
|
||||
tempServiceParameters.removeFirstOrNull()?.let {
|
||||
startService(it.first, it.second)
|
||||
}
|
||||
}
|
||||
|
||||
private fun start(bundle: Bundle? = null, actionTask: String) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val contextActivity = activity
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
|
||||
== PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
startService(bundle, actionTask)
|
||||
} else if (contextActivity != null && shouldShowRequestPermissionRationale(
|
||||
contextActivity,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
)
|
||||
) {
|
||||
// it's not the first time, so the user deliberately chooses not to display the notification
|
||||
startService(bundle, actionTask)
|
||||
} else {
|
||||
AlertDialog.Builder(context)
|
||||
.setMessage(R.string.warning_database_notification_permission)
|
||||
.setNegativeButton(R.string.later) { _, _ ->
|
||||
// Refuses the notification, so start the service
|
||||
startService(bundle, actionTask)
|
||||
}
|
||||
.setPositiveButton(R.string.ask) { _, _ ->
|
||||
// Save the temp parameters to ask the permission
|
||||
tempServiceParameters.add(Pair(bundle, actionTask))
|
||||
requestPermissionLauncher?.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}.create().show()
|
||||
}
|
||||
} else {
|
||||
startService(bundle, actionTask)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startService(bundle: Bundle? = null, actionTask: String) {
|
||||
try {
|
||||
if (bundle != null)
|
||||
intentDatabaseTask.putExtras(bundle)
|
||||
@@ -321,7 +401,7 @@ class DatabaseTaskProvider {
|
||||
context.startService(intentDatabaseTask)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to perform database action", e)
|
||||
Toast.makeText(activity, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(context, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,12 +412,13 @@ class DatabaseTaskProvider {
|
||||
*/
|
||||
|
||||
fun startDatabaseCreate(databaseUri: Uri,
|
||||
mainCredential: MainCredential) {
|
||||
mainCredential: MainCredential
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||
}
|
||||
, ACTION_DATABASE_CREATE_TASK)
|
||||
, ACTION_DATABASE_CREATE_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseLoad(databaseUri: Uri,
|
||||
@@ -352,12 +433,14 @@ class DatabaseTaskProvider {
|
||||
putParcelable(DatabaseTaskNotificationService.CIPHER_DATABASE_KEY, cipherEncryptDatabase)
|
||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||
}
|
||||
, ACTION_DATABASE_LOAD_TASK)
|
||||
, ACTION_DATABASE_LOAD_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseMerge(fromDatabaseUri: Uri? = null,
|
||||
fun startDatabaseMerge(save: Boolean,
|
||||
fromDatabaseUri: Uri? = null,
|
||||
mainCredential: MainCredential? = null) {
|
||||
start(Bundle().apply {
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri)
|
||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||
}
|
||||
@@ -368,7 +451,7 @@ class DatabaseTaskProvider {
|
||||
start(Bundle().apply {
|
||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||
}
|
||||
, ACTION_DATABASE_RELOAD_TASK)
|
||||
, ACTION_DATABASE_RELOAD_TASK)
|
||||
}
|
||||
|
||||
fun askToStartDatabaseReload(conditionToAsk: Boolean, approved: () -> Unit) {
|
||||
@@ -384,14 +467,15 @@ class DatabaseTaskProvider {
|
||||
}
|
||||
}
|
||||
|
||||
fun startDatabaseAssignPassword(databaseUri: Uri,
|
||||
mainCredential: MainCredential) {
|
||||
fun startDatabaseAssignCredential(databaseUri: Uri,
|
||||
mainCredential: MainCredential
|
||||
) {
|
||||
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||
}
|
||||
, ACTION_DATABASE_ASSIGN_PASSWORD_TASK)
|
||||
, ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -408,7 +492,7 @@ class DatabaseTaskProvider {
|
||||
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_CREATE_GROUP_TASK)
|
||||
, ACTION_DATABASE_CREATE_GROUP_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseUpdateGroup(oldGroup: Group,
|
||||
@@ -419,7 +503,7 @@ class DatabaseTaskProvider {
|
||||
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, groupToUpdate)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_GROUP_TASK)
|
||||
, ACTION_DATABASE_UPDATE_GROUP_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseCreateEntry(newEntry: Entry,
|
||||
@@ -430,7 +514,7 @@ class DatabaseTaskProvider {
|
||||
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_CREATE_ENTRY_TASK)
|
||||
, ACTION_DATABASE_CREATE_ENTRY_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseUpdateEntry(oldEntry: Entry,
|
||||
@@ -441,7 +525,7 @@ class DatabaseTaskProvider {
|
||||
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, entryToUpdate)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_ENTRY_TASK)
|
||||
, ACTION_DATABASE_UPDATE_ENTRY_TASK)
|
||||
}
|
||||
|
||||
private fun startDatabaseActionListNodes(actionTask: String,
|
||||
@@ -464,13 +548,13 @@ class DatabaseTaskProvider {
|
||||
|
||||
start(Bundle().apply {
|
||||
putAll(getBundleFromListNodes(nodesPaste))
|
||||
putParcelableArrayList(DatabaseTaskNotificationService.GROUPS_ID_KEY, groupsIdToCopy)
|
||||
putParcelableArrayList(DatabaseTaskNotificationService.ENTRIES_ID_KEY, entriesIdToCopy)
|
||||
putParcelableList(DatabaseTaskNotificationService.GROUPS_ID_KEY, groupsIdToCopy)
|
||||
putParcelableList(DatabaseTaskNotificationService.ENTRIES_ID_KEY, entriesIdToCopy)
|
||||
if (newParentId != null)
|
||||
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, newParentId)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, actionTask)
|
||||
, actionTask)
|
||||
}
|
||||
|
||||
fun startDatabaseCopyNodes(nodesToCopy: List<Node>,
|
||||
@@ -504,7 +588,7 @@ class DatabaseTaskProvider {
|
||||
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
|
||||
, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
|
||||
}
|
||||
|
||||
fun startDatabaseDeleteEntryHistory(mainEntryId: NodeId<UUID>,
|
||||
@@ -515,7 +599,7 @@ class DatabaseTaskProvider {
|
||||
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_DELETE_ENTRY_HISTORY)
|
||||
, ACTION_DATABASE_DELETE_ENTRY_HISTORY)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -532,7 +616,7 @@ class DatabaseTaskProvider {
|
||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newName)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_NAME_TASK)
|
||||
, ACTION_DATABASE_UPDATE_NAME_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveDescription(oldDescription: String,
|
||||
@@ -543,7 +627,7 @@ class DatabaseTaskProvider {
|
||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDescription)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_DESCRIPTION_TASK)
|
||||
, ACTION_DATABASE_UPDATE_DESCRIPTION_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveDefaultUsername(oldDefaultUsername: String,
|
||||
@@ -554,7 +638,7 @@ class DatabaseTaskProvider {
|
||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDefaultUsername)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK)
|
||||
, ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveColor(oldColor: String,
|
||||
@@ -565,7 +649,7 @@ class DatabaseTaskProvider {
|
||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newColor)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_COLOR_TASK)
|
||||
, ACTION_DATABASE_UPDATE_COLOR_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveCompression(oldCompression: CompressionAlgorithm,
|
||||
@@ -576,14 +660,14 @@ class DatabaseTaskProvider {
|
||||
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newCompression)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
|
||||
, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseRemoveUnlinkedData(save: Boolean) {
|
||||
start(Bundle().apply {
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
|
||||
, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveRecycleBin(oldRecycleBin: Group?,
|
||||
@@ -616,7 +700,7 @@ class DatabaseTaskProvider {
|
||||
putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistoryItems)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK)
|
||||
, ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveMaxHistorySize(oldMaxHistorySize: Long,
|
||||
@@ -627,7 +711,7 @@ class DatabaseTaskProvider {
|
||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistorySize)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK)
|
||||
, ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -644,7 +728,7 @@ class DatabaseTaskProvider {
|
||||
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newEncryption)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_ENCRYPTION_TASK)
|
||||
, ACTION_DATABASE_UPDATE_ENCRYPTION_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveKeyDerivation(oldKeyDerivation: KdfEngine,
|
||||
@@ -655,7 +739,7 @@ class DatabaseTaskProvider {
|
||||
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newKeyDerivation)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK)
|
||||
, ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveIterations(oldIterations: Long,
|
||||
@@ -666,7 +750,7 @@ class DatabaseTaskProvider {
|
||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newIterations)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_ITERATIONS_TASK)
|
||||
, ACTION_DATABASE_UPDATE_ITERATIONS_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveMemoryUsage(oldMemoryUsage: Long,
|
||||
@@ -677,7 +761,7 @@ class DatabaseTaskProvider {
|
||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMemoryUsage)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
|
||||
, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
|
||||
}
|
||||
|
||||
fun startDatabaseSaveParallelism(oldParallelism: Long,
|
||||
@@ -688,7 +772,7 @@ class DatabaseTaskProvider {
|
||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newParallelism)
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
}
|
||||
, ACTION_DATABASE_UPDATE_PARALLELISM_TASK)
|
||||
, ACTION_DATABASE_UPDATE_PARALLELISM_TASK)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -699,10 +783,17 @@ class DatabaseTaskProvider {
|
||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, saveToUri)
|
||||
}
|
||||
, ACTION_DATABASE_SAVE)
|
||||
, ACTION_DATABASE_SAVE)
|
||||
}
|
||||
|
||||
fun startChallengeResponded(response: ByteArray?) {
|
||||
start(Bundle().apply {
|
||||
putByteArray(DatabaseTaskNotificationService.DATA_BYTES, response)
|
||||
}
|
||||
, ACTION_CHALLENGE_RESPONDED)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = DatabaseTaskProvider::class.java.name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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.database
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.kunzisoft.keepass.database.element.MasterCredential
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.utils.readParcelableCompat
|
||||
import com.kunzisoft.keepass.utils.getUriInputStream
|
||||
import com.kunzisoft.keepass.utils.readEnum
|
||||
import com.kunzisoft.keepass.utils.writeEnum
|
||||
|
||||
data class MainCredential(var password: String? = null,
|
||||
var keyFileUri: Uri? = null,
|
||||
var hardwareKey: HardwareKey? = null): Parcelable {
|
||||
|
||||
constructor(parcel: Parcel) : this() {
|
||||
password = parcel.readString()
|
||||
keyFileUri = parcel.readParcelableCompat()
|
||||
hardwareKey = parcel.readEnum<HardwareKey>()
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(password)
|
||||
parcel.writeParcelable(keyFileUri, flags)
|
||||
parcel.writeEnum(hardwareKey)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as MainCredential
|
||||
|
||||
if (password != other.password) return false
|
||||
if (keyFileUri != other.keyFileUri) return false
|
||||
if (hardwareKey != other.hardwareKey) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = password?.hashCode() ?: 0
|
||||
result = 31 * result + (keyFileUri?.hashCode() ?: 0)
|
||||
result = 31 * result + (hardwareKey?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
fun toMasterCredential(contentResolver: ContentResolver): MasterCredential {
|
||||
return MasterCredential(
|
||||
this.password,
|
||||
this.keyFileUri?.let {
|
||||
getKeyFileData(contentResolver, it)
|
||||
},
|
||||
this.hardwareKey
|
||||
)
|
||||
}
|
||||
|
||||
private fun getKeyFileData(contentResolver: ContentResolver,
|
||||
keyFileUri: Uri): ByteArray? {
|
||||
contentResolver.getUriInputStream(keyFileUri)?.use { keyFileInputStream ->
|
||||
return keyFileInputStream.readBytes()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<MainCredential> {
|
||||
override fun createFromParcel(parcel: Parcel): MainCredential {
|
||||
return MainCredential(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<MainCredential?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
|
||||
private val TAG = MainCredential::class.java.simpleName
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.kunzisoft.keepass.database
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
data class ProgressMessage(
|
||||
@StringRes
|
||||
var titleId: Int,
|
||||
@StringRes
|
||||
var messageId: Int? = null,
|
||||
@StringRes
|
||||
var warningId: Int? = null,
|
||||
var cancelable: (() -> Unit)? = null
|
||||
)
|
||||
@@ -1,83 +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.database.action
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
open class AssignMainCredentialInDatabaseRunnable (
|
||||
context: Context,
|
||||
database: Database,
|
||||
protected val mDatabaseUri: Uri,
|
||||
protected val mMainCredential: MainCredential)
|
||||
: SaveDatabaseRunnable(context, database, true) {
|
||||
|
||||
private var mBackupKey: ByteArray? = null
|
||||
|
||||
override fun onStartRun() {
|
||||
// Set key
|
||||
try {
|
||||
mBackupKey = ByteArray(database.masterKey.size)
|
||||
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
|
||||
|
||||
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri)
|
||||
database.assignMasterKey(mMainCredential.masterPassword, uriInputStream)
|
||||
} catch (e: Exception) {
|
||||
erase(mBackupKey)
|
||||
setError(e)
|
||||
}
|
||||
|
||||
super.onStartRun()
|
||||
}
|
||||
|
||||
override fun onFinishRun() {
|
||||
super.onFinishRun()
|
||||
|
||||
// Erase the biometric
|
||||
CipherDatabaseAction.getInstance(context)
|
||||
.deleteByDatabaseUri(mDatabaseUri)
|
||||
// Erase the register keyfile
|
||||
FileDatabaseHistoryAction.getInstance(context)
|
||||
.deleteKeyFileByDatabaseUri(mDatabaseUri)
|
||||
|
||||
if (!result.isSuccess) {
|
||||
// Erase the current master key
|
||||
erase(database.masterKey)
|
||||
mBackupKey?.let {
|
||||
database.masterKey = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrite the array as soon as we don't need it to avoid keeping the extra data in memory
|
||||
*/
|
||||
private fun erase(array: ByteArray?) {
|
||||
if (array == null) return
|
||||
for (i in array.indices) {
|
||||
array[i] = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,57 +21,46 @@ package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
|
||||
class CreateDatabaseRunnable(context: Context,
|
||||
private val mDatabase: Database,
|
||||
databaseUri: Uri,
|
||||
private val databaseName: String,
|
||||
private val rootName: String,
|
||||
private val templateGroupName: String?,
|
||||
mainCredential: MainCredential,
|
||||
private val createDatabaseResult: ((Result) -> Unit)?)
|
||||
: AssignMainCredentialInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) {
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.utils.getBinaryDir
|
||||
|
||||
class CreateDatabaseRunnable(
|
||||
context: Context,
|
||||
private val mDatabase: ContextualDatabase,
|
||||
private val databaseUri: Uri,
|
||||
private val databaseName: String,
|
||||
private val rootName: String,
|
||||
private val templateGroupName: String?,
|
||||
val mainCredential: MainCredential,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray
|
||||
) : SaveDatabaseRunnable(
|
||||
context,
|
||||
mDatabase,
|
||||
true,
|
||||
mainCredential,
|
||||
challengeResponseRetriever
|
||||
) {
|
||||
override fun onStartRun() {
|
||||
try {
|
||||
// Create new database record
|
||||
mDatabase.apply {
|
||||
createData(mDatabaseUri, databaseName, rootName, templateGroupName)
|
||||
this.fileUri = databaseUri
|
||||
createData(databaseName, rootName, templateGroupName)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
mDatabase.clearAndClose(context)
|
||||
mDatabase.clearAndClose(context.getBinaryDir())
|
||||
setError(e)
|
||||
}
|
||||
|
||||
super.onStartRun()
|
||||
}
|
||||
|
||||
override fun onActionRun() {
|
||||
super.onActionRun()
|
||||
|
||||
if (result.isSuccess) {
|
||||
// Add database to recent files
|
||||
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
||||
FileDatabaseHistoryAction.getInstance(context.applicationContext)
|
||||
.addOrUpdateDatabaseUri(mDatabaseUri,
|
||||
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null)
|
||||
}
|
||||
|
||||
// Register the current time to init the lock timer
|
||||
PreferencesUtil.saveCurrentTime(context)
|
||||
} else {
|
||||
Log.e("CreateDatabaseRunnable", "Unable to create the database")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinishRun() {
|
||||
if (result.isSuccess) {
|
||||
mDatabase.loaded = true
|
||||
}
|
||||
super.onFinishRun()
|
||||
|
||||
createDatabaseResult?.invoke(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,73 +21,65 @@ package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseInputException
|
||||
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.utils.getBinaryDir
|
||||
import com.kunzisoft.keepass.utils.getUriInputStream
|
||||
|
||||
class LoadDatabaseRunnable(private val context: Context,
|
||||
private val mDatabase: Database,
|
||||
private val mUri: Uri,
|
||||
private val mMainCredential: MainCredential,
|
||||
private val mReadonly: Boolean,
|
||||
private val mCipherEncryptDatabase: CipherEncryptDatabase?,
|
||||
private val mFixDuplicateUUID: Boolean,
|
||||
private val progressTaskUpdater: ProgressTaskUpdater?,
|
||||
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||
: ActionRunnable() {
|
||||
class LoadDatabaseRunnable(
|
||||
private val context: Context,
|
||||
private val mDatabase: ContextualDatabase,
|
||||
private val mDatabaseUri: Uri,
|
||||
private val mMainCredential: MainCredential,
|
||||
private val mChallengeResponseRetriever: (hardwareKey: HardwareKey, seed: ByteArray?) -> ByteArray,
|
||||
private val mReadonly: Boolean,
|
||||
private val mFixDuplicateUUID: Boolean,
|
||||
private val progressTaskUpdater: ProgressTaskUpdater?
|
||||
) : ActionRunnable() {
|
||||
|
||||
var afterLoadDatabase : ((Result) -> Unit)? = null
|
||||
|
||||
private val binaryDir = context.getBinaryDir()
|
||||
|
||||
override fun onStartRun() {
|
||||
// Clear before we load
|
||||
mDatabase.clearAndClose(context)
|
||||
mDatabase.clearAndClose(binaryDir)
|
||||
}
|
||||
|
||||
override fun onActionRun() {
|
||||
try {
|
||||
mDatabase.loadData(mUri,
|
||||
mMainCredential,
|
||||
mReadonly,
|
||||
context.contentResolver,
|
||||
UriUtil.getBinaryDir(context),
|
||||
{ memoryWanted ->
|
||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||
},
|
||||
mFixDuplicateUUID,
|
||||
progressTaskUpdater)
|
||||
}
|
||||
catch (e: LoadDatabaseException) {
|
||||
val contentResolver = context.contentResolver
|
||||
// Save database URI
|
||||
mDatabase.fileUri = mDatabaseUri
|
||||
mDatabase.loadData(
|
||||
contentResolver.getUriInputStream(mDatabaseUri)
|
||||
?: throw UnknownDatabaseLocationException(),
|
||||
mMainCredential.toMasterCredential(contentResolver),
|
||||
mChallengeResponseRetriever,
|
||||
mReadonly,
|
||||
binaryDir,
|
||||
{ memoryWanted ->
|
||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||
},
|
||||
mFixDuplicateUUID,
|
||||
progressTaskUpdater
|
||||
)
|
||||
} catch (e: DatabaseInputException) {
|
||||
setError(e)
|
||||
}
|
||||
|
||||
if (result.isSuccess) {
|
||||
// Save keyFile in app database
|
||||
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
||||
FileDatabaseHistoryAction.getInstance(context)
|
||||
.addOrUpdateDatabaseUri(mUri,
|
||||
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null)
|
||||
}
|
||||
|
||||
// Register the biometric
|
||||
mCipherEncryptDatabase?.let { cipherDatabase ->
|
||||
CipherDatabaseAction.getInstance(context)
|
||||
.addOrUpdateCipherDatabase(cipherDatabase) // return value not called
|
||||
}
|
||||
|
||||
// Register the current time to init the lock timer
|
||||
PreferencesUtil.saveCurrentTime(context)
|
||||
} else {
|
||||
mDatabase.clearAndClose(context)
|
||||
if (!result.isSuccess) {
|
||||
mDatabase.clearAndClose(binaryDir)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinishRun() {
|
||||
mLoadDatabaseResult?.invoke(result)
|
||||
afterLoadDatabase?.invoke(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,47 +21,54 @@ package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.utils.getUriInputStream
|
||||
|
||||
class MergeDatabaseRunnable(private val context: Context,
|
||||
private val mDatabase: Database,
|
||||
private val mDatabaseToMergeUri: Uri?,
|
||||
private val mDatabaseToMergeMainCredential: MainCredential?,
|
||||
private val progressTaskUpdater: ProgressTaskUpdater?,
|
||||
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||
: ActionRunnable() {
|
||||
|
||||
class MergeDatabaseRunnable(
|
||||
context: Context,
|
||||
private val mDatabaseToMergeUri: Uri?,
|
||||
private val mDatabaseToMergeMainCredential: MainCredential?,
|
||||
private val mDatabaseToMergeChallengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
|
||||
database: ContextualDatabase,
|
||||
saveDatabase: Boolean,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
|
||||
private val progressTaskUpdater: ProgressTaskUpdater?,
|
||||
) : SaveDatabaseRunnable(
|
||||
context,
|
||||
database,
|
||||
saveDatabase,
|
||||
null,
|
||||
challengeResponseRetriever
|
||||
) {
|
||||
override fun onStartRun() {
|
||||
mDatabase.wasReloaded = true
|
||||
database.wasReloaded = true
|
||||
super.onStartRun()
|
||||
}
|
||||
|
||||
override fun onActionRun() {
|
||||
try {
|
||||
mDatabase.mergeData(mDatabaseToMergeUri,
|
||||
mDatabaseToMergeMainCredential,
|
||||
context.contentResolver,
|
||||
val contentResolver = context.contentResolver
|
||||
database.mergeData(
|
||||
context.contentResolver.getUriInputStream(
|
||||
mDatabaseToMergeUri ?: database.fileUri
|
||||
) ?: throw UnknownDatabaseLocationException(),
|
||||
mDatabaseToMergeMainCredential?.toMasterCredential(contentResolver),
|
||||
mDatabaseToMergeChallengeResponseRetriever,
|
||||
{ memoryWanted ->
|
||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||
},
|
||||
progressTaskUpdater
|
||||
)
|
||||
} catch (e: LoadDatabaseException) {
|
||||
} catch (e: DatabaseException) {
|
||||
setError(e)
|
||||
}
|
||||
|
||||
if (result.isSuccess) {
|
||||
// Register the current time to init the lock timer
|
||||
PreferencesUtil.saveCurrentTime(context)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinishRun() {
|
||||
mLoadDatabaseResult?.invoke(result)
|
||||
super.onActionRun()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,46 +20,50 @@
|
||||
package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.utils.getBinaryDir
|
||||
import com.kunzisoft.keepass.utils.getUriInputStream
|
||||
|
||||
class ReloadDatabaseRunnable(private val context: Context,
|
||||
private val mDatabase: Database,
|
||||
private val progressTaskUpdater: ProgressTaskUpdater?,
|
||||
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||
: ActionRunnable() {
|
||||
class ReloadDatabaseRunnable(
|
||||
private val context: Context,
|
||||
private val mDatabase: ContextualDatabase,
|
||||
private val progressTaskUpdater: ProgressTaskUpdater?
|
||||
) : ActionRunnable() {
|
||||
|
||||
var afterReloadDatabase : ((Result) -> Unit)? = null
|
||||
|
||||
private val binaryDir = context.getBinaryDir()
|
||||
|
||||
override fun onStartRun() {
|
||||
// Clear before we load
|
||||
mDatabase.clearIndexesAndBinaries(UriUtil.getBinaryDir(context))
|
||||
mDatabase.clearIndexesAndBinaries(binaryDir)
|
||||
mDatabase.wasReloaded = true
|
||||
}
|
||||
|
||||
override fun onActionRun() {
|
||||
try {
|
||||
mDatabase.reloadData(context.contentResolver,
|
||||
{ memoryWanted ->
|
||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||
},
|
||||
progressTaskUpdater)
|
||||
} catch (e: LoadDatabaseException) {
|
||||
mDatabase.reloadData(
|
||||
context.contentResolver.getUriInputStream(mDatabase.fileUri)
|
||||
?: throw UnknownDatabaseLocationException(),
|
||||
{ memoryWanted ->
|
||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||
},
|
||||
progressTaskUpdater)
|
||||
} catch (e: DatabaseException) {
|
||||
setError(e)
|
||||
}
|
||||
|
||||
if (result.isSuccess) {
|
||||
// Register the current time to init the lock timer
|
||||
PreferencesUtil.saveCurrentTime(context)
|
||||
} else {
|
||||
mDatabase.clearAndClose(context)
|
||||
if (!result.isSuccess) {
|
||||
mDatabase.clearAndClose(binaryDir)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinishRun() {
|
||||
mLoadDatabaseResult?.invoke(result)
|
||||
afterReloadDatabase?.invoke(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,13 +20,21 @@
|
||||
package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class RemoveUnlinkedDataDatabaseRunnable (
|
||||
context: Context,
|
||||
database: Database,
|
||||
saveDatabase: Boolean)
|
||||
: SaveDatabaseRunnable(context, database, saveDatabase) {
|
||||
context: Context,
|
||||
database: ContextualDatabase,
|
||||
saveDatabase: Boolean,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray
|
||||
) : SaveDatabaseRunnable(
|
||||
context,
|
||||
database,
|
||||
saveDatabase,
|
||||
null,
|
||||
challengeResponseRetriever
|
||||
) {
|
||||
|
||||
override fun onActionRun() {
|
||||
try {
|
||||
|
||||
@@ -21,17 +21,24 @@ package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseException
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.getUriOutputStream
|
||||
import java.io.File
|
||||
|
||||
open class SaveDatabaseRunnable(protected var context: Context,
|
||||
protected var database: Database,
|
||||
private var saveDatabase: Boolean,
|
||||
private var databaseCopyUri: Uri? = null)
|
||||
: ActionRunnable() {
|
||||
open class SaveDatabaseRunnable(
|
||||
protected var context: Context,
|
||||
protected var database: ContextualDatabase,
|
||||
private var saveDatabase: Boolean,
|
||||
private var mainCredential: MainCredential?, // If null, uses composite Key
|
||||
private var challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
|
||||
private var databaseCopyUri: Uri? = null
|
||||
) : ActionRunnable() {
|
||||
|
||||
var mAfterSaveDatabase: ((Result) -> Unit)? = null
|
||||
var afterSaveDatabase: ((Result) -> Unit)? = null
|
||||
|
||||
override fun onStartRun() {}
|
||||
|
||||
@@ -39,7 +46,15 @@ open class SaveDatabaseRunnable(protected var context: Context,
|
||||
database.checkVersion()
|
||||
if (saveDatabase && result.isSuccess) {
|
||||
try {
|
||||
database.saveData(databaseCopyUri, context.contentResolver)
|
||||
val contentResolver = context.contentResolver
|
||||
// Build temp database file to avoid file corruption if error
|
||||
database.saveData(
|
||||
cacheFile = File(context.cacheDir, databaseCopyUri.hashCode().toString()),
|
||||
databaseOutputStream = contentResolver
|
||||
.getUriOutputStream(databaseCopyUri ?: database.fileUri),
|
||||
isNewLocation = databaseCopyUri == null,
|
||||
mainCredential?.toMasterCredential(contentResolver),
|
||||
challengeResponseRetriever)
|
||||
} catch (e: DatabaseException) {
|
||||
setError(e)
|
||||
}
|
||||
@@ -48,6 +63,6 @@ open class SaveDatabaseRunnable(protected var context: Context,
|
||||
|
||||
override fun onFinishRun() {
|
||||
// Need to call super.onFinishRun() in child class
|
||||
mAfterSaveDatabase?.invoke(result)
|
||||
afterSaveDatabase?.invoke(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,16 +20,24 @@
|
||||
package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class UpdateCompressionBinariesDatabaseRunnable (
|
||||
context: Context,
|
||||
database: Database,
|
||||
private val oldCompressionAlgorithm: CompressionAlgorithm,
|
||||
private val newCompressionAlgorithm: CompressionAlgorithm,
|
||||
saveDatabase: Boolean)
|
||||
: SaveDatabaseRunnable(context, database, saveDatabase) {
|
||||
context: Context,
|
||||
database: ContextualDatabase,
|
||||
private val oldCompressionAlgorithm: CompressionAlgorithm,
|
||||
private val newCompressionAlgorithm: CompressionAlgorithm,
|
||||
saveDatabase: Boolean,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray
|
||||
) : SaveDatabaseRunnable(
|
||||
context,
|
||||
database,
|
||||
saveDatabase,
|
||||
null,
|
||||
challengeResponseRetriever
|
||||
) {
|
||||
|
||||
override fun onStartRun() {
|
||||
// Set new compression
|
||||
|
||||
@@ -20,17 +20,19 @@
|
||||
package com.kunzisoft.keepass.database.action.history
|
||||
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class DeleteEntryHistoryDatabaseRunnable (
|
||||
context: Context,
|
||||
database: Database,
|
||||
private val mainEntry: Entry,
|
||||
private val entryHistoryPosition: Int,
|
||||
saveDatabase: Boolean)
|
||||
: SaveDatabaseRunnable(context, database, saveDatabase) {
|
||||
context: Context,
|
||||
database: ContextualDatabase,
|
||||
private val mainEntry: Entry,
|
||||
private val entryHistoryPosition: Int,
|
||||
saveDatabase: Boolean,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray
|
||||
) : SaveDatabaseRunnable(context, database, saveDatabase, null, challengeResponseRetriever) {
|
||||
|
||||
override fun onStartRun() {
|
||||
try {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user