Compare commits
3302 Commits
2.9.15
...
3ba56677ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ba56677ba | ||
|
|
39b4b4df70 | ||
|
|
4180ca92b0 | ||
|
|
bc9d00a1e1 | ||
|
|
5bdc72aa67 | ||
|
|
2be32e6884 | ||
|
|
612db4a6fc | ||
|
|
e74176f3bc | ||
|
|
af1fba42a0 | ||
|
|
bebf30aec1 | ||
|
|
321bb46df5 | ||
|
|
429f6db93f | ||
|
|
fc5a13160a | ||
|
|
c6eee8d449 | ||
|
|
7d227f372f | ||
|
|
3ac56b974f | ||
|
|
2e85ea401b | ||
|
|
fd080fb952 | ||
|
|
cc8e07366a | ||
|
|
c21bcbdbc2 | ||
|
|
e2ee17dae7 | ||
|
|
e68830fa25 | ||
|
|
9ddd66ce85 | ||
|
|
e3b69789bf | ||
|
|
54f2ed9fab | ||
|
|
2fea019b95 | ||
|
|
9ac7ef2d22 | ||
|
|
6d452fa49c | ||
|
|
d99edb6b4d | ||
|
|
cb679f0d59 | ||
|
|
2e237fba2d | ||
|
|
e68863a154 | ||
|
|
5dd9f75095 | ||
|
|
403021d38b | ||
|
|
fea7b30d6f | ||
|
|
ab5c859db4 | ||
|
|
3fcbc65de0 | ||
|
|
3f1ee6bbea | ||
|
|
37ce2ab781 | ||
|
|
ffaf4a761a | ||
|
|
56b7cc9118 | ||
|
|
987f3f9047 | ||
|
|
3039efc67c | ||
|
|
26daac4637 | ||
|
|
88a93829a9 | ||
|
|
7923a63d36 | ||
|
|
9a5c782d5d | ||
|
|
c39e4ba693 | ||
|
|
7db3d0502f | ||
|
|
d557e8b516 | ||
|
|
d6ae17657b | ||
|
|
3468b0f6f5 | ||
|
|
79777801e8 | ||
|
|
a202f66d48 | ||
|
|
ba58d5d47c | ||
|
|
46685592df | ||
|
|
ba9e2892ef | ||
|
|
a1da3b4fbd | ||
|
|
8bee0ec220 | ||
|
|
aebf6b21de | ||
|
|
0cf9253ea4 | ||
|
|
b63ceb37a4 | ||
|
|
c462dae6f5 | ||
|
|
ddf890b861 | ||
|
|
252eb30b13 | ||
|
|
62ab11cc56 | ||
|
|
e19ad3a8cc | ||
|
|
51fd8a77eb | ||
|
|
5ee0c2eb13 | ||
|
|
6d0ef8265c | ||
|
|
ea69d5acb2 | ||
|
|
1fb9595ec3 | ||
|
|
88e0bd51dc | ||
|
|
67477cc53b | ||
|
|
d2549d61d6 | ||
|
|
d6dc75961b | ||
|
|
f40c83812a | ||
|
|
b29c638d20 | ||
|
|
5bb03c2eef | ||
|
|
a76b1195e5 | ||
|
|
64da26f42c | ||
|
|
ef82552a0f | ||
|
|
d61b27ccd0 | ||
|
|
910ba99056 | ||
|
|
3de2a9acfd | ||
|
|
a48dccf27a | ||
|
|
2a561fb37e | ||
|
|
e27a329ac5 | ||
|
|
8e06a2a7cb | ||
|
|
ace82852af | ||
|
|
73369974b8 | ||
|
|
332eda8a7a | ||
|
|
e5ea1e35aa | ||
|
|
86aae9635a | ||
|
|
db3ccae87d | ||
|
|
4cec26967c | ||
|
|
a0368a4981 | ||
|
|
a1c7fe1e99 | ||
|
|
bf247ddeb7 | ||
|
|
1d2bc0fbfb | ||
|
|
85a12fe4ee | ||
|
|
a443ef996b | ||
|
|
c6995ad403 | ||
|
|
9018807eb8 | ||
|
|
b463106dd5 | ||
|
|
a23d28e1fa | ||
|
|
a0454f42d0 | ||
|
|
1c2ac88f47 | ||
|
|
11eb1bae45 | ||
|
|
089d86165a | ||
|
|
6a7362ad35 | ||
|
|
d2c10e2e4e | ||
|
|
0c20a14e67 | ||
|
|
acccf290de | ||
|
|
6ebe0f78af | ||
|
|
935c09ccd2 | ||
|
|
1eb10ad5bd | ||
|
|
ca4283151e | ||
|
|
8fb98ca4e7 | ||
|
|
be74c9710f | ||
|
|
24fb3c4c30 | ||
|
|
3bdc5fe600 | ||
|
|
c30884d6d0 | ||
|
|
5d26c3bd09 | ||
|
|
02e35cf5b7 | ||
|
|
085aefd2b9 | ||
|
|
1ea5b7a50c | ||
|
|
6cba96dd42 | ||
|
|
46238a76bc | ||
|
|
d5a9b664a1 | ||
|
|
6fdc4504d5 | ||
|
|
8a7c411a35 | ||
|
|
5ff9d5fa2f | ||
|
|
bb0f3c80d3 | ||
|
|
597d9c8274 | ||
|
|
4dd8c06fd2 | ||
|
|
72c66b3cd9 | ||
|
|
c2223afa6f | ||
|
|
d338d1340f | ||
|
|
ed4423666b | ||
|
|
d21fe662ff | ||
|
|
4da1c5bd92 | ||
|
|
18c18605fb | ||
|
|
988cb1a8d0 | ||
|
|
b6e01767e0 | ||
|
|
5414854e9c | ||
|
|
ae7f0732c6 | ||
|
|
d49d33fe3a | ||
|
|
5e7fc2d468 | ||
|
|
0d26e6a870 | ||
|
|
dd92f9ceb6 | ||
|
|
ff9239b9c4 | ||
|
|
319d35e485 | ||
|
|
28e65a4601 | ||
|
|
eb626e5bfe | ||
|
|
e1decf9a23 | ||
|
|
fff0e84b95 | ||
|
|
d73a7004b1 | ||
|
|
f71061e835 | ||
|
|
b2d25cc512 | ||
|
|
4d54b56c1d | ||
|
|
c764c6afff | ||
|
|
87b97a3849 | ||
|
|
5e6db44476 | ||
|
|
8615fa817f | ||
|
|
2f891bacd3 | ||
|
|
0d8a426df4 | ||
|
|
c952eb4415 | ||
|
|
2fd53b9416 | ||
|
|
244ca08890 | ||
|
|
208f1e97d5 | ||
|
|
e4e0628e20 | ||
|
|
f60f31771f | ||
|
|
ff6367bac4 | ||
|
|
540e72812e | ||
|
|
5fe4af8e9d | ||
|
|
ae42ab43b7 | ||
|
|
c463055971 | ||
|
|
1849dca81d | ||
|
|
b3dd3dcfb5 | ||
|
|
fef88ff270 | ||
|
|
f1f7dd1e6c | ||
|
|
409f290e33 | ||
|
|
96c3af097a | ||
|
|
4fe6b2e115 | ||
|
|
cc936b9304 | ||
|
|
e7f2a22583 | ||
|
|
4bf905ecda | ||
|
|
f8d80525d9 | ||
|
|
7ce6092270 | ||
|
|
65857596a6 | ||
|
|
e6253336bd | ||
|
|
e5595a3275 | ||
|
|
366e8bf1d7 | ||
|
|
fa63265599 | ||
|
|
755e0ea9a5 | ||
|
|
a819f2f8a8 | ||
|
|
c92da0a72f | ||
|
|
524963dbd8 | ||
|
|
50b1ac388e | ||
|
|
51c62034df | ||
|
|
e4d0cd89c6 | ||
|
|
bfe50fa985 | ||
|
|
3d798e6585 | ||
|
|
068c59ac98 | ||
|
|
34ec94a0c3 | ||
|
|
576a355342 | ||
|
|
aa19f11699 | ||
|
|
2fb4dff46d | ||
|
|
e6cf3f12a5 | ||
|
|
ca94ce86ba | ||
|
|
dea6b25bb4 | ||
|
|
c48f64d331 | ||
|
|
5e3a504c1f | ||
|
|
b9b7d7b2db | ||
|
|
e085d5d277 | ||
|
|
05336e93a0 | ||
|
|
90b3b56893 | ||
|
|
02c514272e | ||
|
|
989e47ed12 | ||
|
|
1caf132558 | ||
|
|
1b98bd740c | ||
|
|
5adeb5cde0 | ||
|
|
b949d5d861 | ||
|
|
b4264a30a4 | ||
|
|
cf799c0f68 | ||
|
|
97f0ca519b | ||
|
|
cf4047b701 | ||
|
|
40608a3eb5 | ||
|
|
99cb50d031 | ||
|
|
b0d0c35241 | ||
|
|
6044c93a4a | ||
|
|
b544b5d54d | ||
|
|
852378e484 | ||
|
|
711a344860 | ||
|
|
72087c7e5c | ||
|
|
a337de3679 | ||
|
|
75b37f5a9f | ||
|
|
075f54b286 | ||
|
|
e07cbc2e14 | ||
|
|
ac29b7bac7 | ||
|
|
b9129cb941 | ||
|
|
6957fcd81a | ||
|
|
cfe56fc055 | ||
|
|
6f3e065ad1 | ||
|
|
abfa7a3f47 | ||
|
|
dd0d85e54e | ||
|
|
76c20263f7 | ||
|
|
e447388611 | ||
|
|
1bfec67c02 | ||
|
|
45041216d6 | ||
|
|
e075e9018c | ||
|
|
eed304ec40 | ||
|
|
5bcbbac97f | ||
|
|
ea4750fc11 | ||
|
|
5037821529 | ||
|
|
3a4c88f19a | ||
|
|
e960a8e169 | ||
|
|
1d4e1687cf | ||
|
|
033fa95285 | ||
|
|
f17d211fbd | ||
|
|
cae69e7572 | ||
|
|
01d778650c | ||
|
|
dd389dbab1 | ||
|
|
272ebd0c3f | ||
|
|
0aecc21f43 | ||
|
|
1e7e464e65 | ||
|
|
ae903ad236 | ||
|
|
d5c378ac85 | ||
|
|
672f1ca37d | ||
|
|
2f9e1e4bf2 | ||
|
|
25d97e4f2e | ||
|
|
f49dcbd654 | ||
|
|
bf2d56b4fd | ||
|
|
7c3a15ce79 | ||
|
|
5893541dd2 | ||
|
|
2230fe66ab | ||
|
|
84a62a32ff | ||
|
|
da8ef9340c | ||
|
|
af068349e4 | ||
|
|
56cb5953dd | ||
|
|
2fc2a9c7c1 | ||
|
|
69e7cdbc47 | ||
|
|
39d9a74a73 | ||
|
|
b609d4e182 | ||
|
|
7212c73481 | ||
|
|
3ee4caa153 | ||
|
|
28e4d929bb | ||
|
|
803d637510 | ||
|
|
ccd5da0962 | ||
|
|
e8ecf28f7c | ||
|
|
3d5adbfc01 | ||
|
|
72bfc50703 | ||
|
|
36e3b85400 | ||
|
|
cd73880e21 | ||
|
|
a60e2e780d | ||
|
|
9210851765 | ||
|
|
8337f98f3a | ||
|
|
47fbb562b7 | ||
|
|
a46251be7b | ||
|
|
ef98e8a2db | ||
|
|
e8ec27dc38 | ||
|
|
30dd7c567c | ||
|
|
e562694606 | ||
|
|
464bc1442d | ||
|
|
c1730353d0 | ||
|
|
55e32e4ac5 | ||
|
|
96ed9fc7a6 | ||
|
|
5fda628c9c | ||
|
|
17742e25a9 | ||
|
|
8086289e4b | ||
|
|
65157f661f | ||
|
|
5df637d01f | ||
|
|
8084920b9e | ||
|
|
b196145578 | ||
|
|
ac347db2d1 | ||
|
|
013c437cf7 | ||
|
|
1f600d60e3 | ||
|
|
a6af9976fc | ||
|
|
05c480b6d3 | ||
|
|
d5ecaeb331 | ||
|
|
db8b0100de | ||
|
|
5f41177a1f | ||
|
|
fb909dac52 | ||
|
|
a8130d67be | ||
|
|
754d195e26 | ||
|
|
074910ea19 | ||
|
|
988b18b515 | ||
|
|
8924254c25 | ||
|
|
0db2b7023e | ||
|
|
a2c2a21dde | ||
|
|
d7a3e7fedd | ||
|
|
2bedbf8a6c | ||
|
|
437a704bc8 | ||
|
|
a3bd5e1593 | ||
|
|
3feb177afc | ||
|
|
821f35fe05 | ||
|
|
d36f675da7 | ||
|
|
b7f9690a38 | ||
|
|
5e4ee167fc | ||
|
|
c911b7c511 | ||
|
|
c79d1f1b81 | ||
|
|
daf717becd | ||
|
|
48d4483484 | ||
|
|
c861fe790c | ||
|
|
1a717bda03 | ||
|
|
b80acd5a2d | ||
|
|
7e41527cfe | ||
|
|
200881278c | ||
|
|
0d133ffdb0 | ||
|
|
c6b0ee27df | ||
|
|
0053726d0b | ||
|
|
1395af88d1 | ||
|
|
2e3ade1b4a | ||
|
|
90c43acfbf | ||
|
|
90b68fd972 | ||
|
|
f8787ba03d | ||
|
|
4f10d13691 | ||
|
|
ef6aeceb20 | ||
|
|
ef8685f0e7 | ||
|
|
3021ed158b | ||
|
|
a57043f496 | ||
|
|
fdfd124fee | ||
|
|
71739de91a | ||
|
|
041b1fbf53 | ||
|
|
3a72b32b4a | ||
|
|
994f174300 | ||
|
|
c0f32254bb | ||
|
|
fd98dbeebe | ||
|
|
69ac6e6698 | ||
|
|
35e224d227 | ||
|
|
2da8552a53 | ||
|
|
a9a5047949 | ||
|
|
17c98f7fea | ||
|
|
c3bc890665 | ||
|
|
7a295c2541 | ||
|
|
01b1b74c6a | ||
|
|
fd25d21c72 | ||
|
|
6b1d8d24dd | ||
|
|
5d002f5128 | ||
|
|
98314c466f | ||
|
|
4f7afd7c97 | ||
|
|
a9e139ff7e | ||
|
|
4ff483a8d2 | ||
|
|
1916b79df1 | ||
|
|
98e15a7717 | ||
|
|
dfd18e3c7f | ||
|
|
8fbbaae05b | ||
|
|
98007c962d | ||
|
|
5f27f161a5 | ||
|
|
fcf723849b | ||
|
|
8a60056866 | ||
|
|
e9d20a51a5 | ||
|
|
a28d77ba32 | ||
|
|
5bd866e104 | ||
|
|
9985c6065d | ||
|
|
1f2e4a3719 | ||
|
|
fa2555a3f7 | ||
|
|
b4de7afe77 | ||
|
|
736cafbcc2 | ||
|
|
d143605a40 | ||
|
|
f2f4c1e63d | ||
|
|
bc86ee87a0 | ||
|
|
5cbd60c024 | ||
|
|
15972efb4f | ||
|
|
dae5f65c0d | ||
|
|
564b5f10ea | ||
|
|
e6e40f9bd4 | ||
|
|
bd15e36b52 | ||
|
|
43faca3061 | ||
|
|
82af9bada2 | ||
|
|
5817273872 | ||
|
|
32d6a11353 | ||
|
|
9477fba704 | ||
|
|
80b16bccf1 | ||
|
|
2befa68c93 | ||
|
|
6672085d84 | ||
|
|
05a39f6922 | ||
|
|
40e8dea485 | ||
|
|
7e09532d5d | ||
|
|
4034a2bfc4 | ||
|
|
0d93e867cf | ||
|
|
44e8f4f406 | ||
|
|
e3083c7773 | ||
|
|
d0c0c4a4d6 | ||
|
|
a9e8de26f8 | ||
|
|
c7a256ebf1 | ||
|
|
8cac4eb51c | ||
|
|
933d34ff1d | ||
|
|
d34f460b98 | ||
|
|
7632face63 | ||
|
|
c4cbd2e587 | ||
|
|
ad454c2e4a | ||
|
|
fbb03c3ecf | ||
|
|
1a4d963d53 | ||
|
|
0c58992c21 | ||
|
|
0eaeb3b90b | ||
|
|
fba8443d0a | ||
|
|
601874442c | ||
|
|
fa34618d67 | ||
|
|
a60fc83379 | ||
|
|
c2c9ebe4c7 | ||
|
|
fdd86e2b9e | ||
|
|
34f0f45862 | ||
|
|
c0345d4dc4 | ||
|
|
24e859c4ce | ||
|
|
1fff0c526c | ||
|
|
1ee1bb8d95 | ||
|
|
b8e996a5af | ||
|
|
df999002af | ||
|
|
64f5a4152b | ||
|
|
0af99d1830 | ||
|
|
733f337312 | ||
|
|
1f99b1f884 | ||
|
|
c3b5598a09 | ||
|
|
d0ab5267cf | ||
|
|
88b701fd39 | ||
|
|
4a1cee619c | ||
|
|
26cb36ccd0 | ||
|
|
67e0496efc | ||
|
|
6c4d040564 | ||
|
|
c7741115ff | ||
|
|
cb5a725d50 | ||
|
|
1c30d533ad | ||
|
|
845d1a581b | ||
|
|
23bebf9597 | ||
|
|
536e038306 | ||
|
|
9e1f6d29a5 | ||
|
|
7a9469e59d | ||
|
|
c12eb3d643 | ||
|
|
da0f02e536 | ||
|
|
04bcc6631c | ||
|
|
698e3b7fb1 | ||
|
|
6de02384c1 | ||
|
|
df3bd7e0a1 | ||
|
|
c8c232639f | ||
|
|
192d6eedd0 | ||
|
|
9cae3f0794 | ||
|
|
a680db9707 | ||
|
|
fe526089d7 | ||
|
|
dfd7ade416 | ||
|
|
3cd65345c5 | ||
|
|
2d398908de | ||
|
|
756454abc3 | ||
|
|
b7619b45b1 | ||
|
|
1369a3cad9 | ||
|
|
f46c062c4e | ||
|
|
0a0abef4d4 | ||
|
|
3a8245ee74 | ||
|
|
7be554a378 | ||
|
|
7007efa627 | ||
|
|
6c37f7b12c | ||
|
|
05defff5ef | ||
|
|
e4569662ba | ||
|
|
4727f7a761 | ||
|
|
89b15e715d | ||
|
|
ef72df02e3 | ||
|
|
19c987abc3 | ||
|
|
b6201262f1 | ||
|
|
b99fa9ffcf | ||
|
|
1497ab85b2 | ||
|
|
2ade463974 | ||
|
|
3a67ec09d5 | ||
|
|
dca800b1bb | ||
|
|
70665f110d | ||
|
|
3b39cafb99 | ||
|
|
0c1b94468d | ||
|
|
7a841bbf57 | ||
|
|
0066f1c77a | ||
|
|
14dbff603d | ||
|
|
42afa93293 | ||
|
|
2fa25a51ad | ||
|
|
2b7f41477f | ||
|
|
be0d5f80c3 | ||
|
|
3844188fcc | ||
|
|
ef81ba5a8f | ||
|
|
2871668d8f | ||
|
|
d03693341e | ||
|
|
2b5ecb2f84 | ||
|
|
e397b92c36 | ||
|
|
e273eb6e03 | ||
|
|
28b624afa3 | ||
|
|
fb1e6cdc3f | ||
|
|
8b6499d040 | ||
|
|
054af507ad | ||
|
|
bf496333eb | ||
|
|
ac9bb9b666 | ||
|
|
809e1929e5 | ||
|
|
a1b1338d67 | ||
|
|
bd4cacfab1 | ||
|
|
e0343bdc55 | ||
|
|
b743d004e2 | ||
|
|
4b20e035b2 | ||
|
|
afe5fddc50 | ||
|
|
d68ca1b51f | ||
|
|
061b087229 | ||
|
|
bb3a379965 | ||
|
|
593b5c6338 | ||
|
|
56f8a1bf9f | ||
|
|
962b547b36 | ||
|
|
c6b01947b3 | ||
|
|
6df8ff4310 | ||
|
|
52f17140b8 | ||
|
|
75c2bb4a87 | ||
|
|
f36f6c3155 | ||
|
|
b88b92c5b0 | ||
|
|
d2c569c4f0 | ||
|
|
91781f36ac | ||
|
|
3fbdf78ba1 | ||
|
|
d1f463d497 | ||
|
|
cb1316564e | ||
|
|
245d3f7df2 | ||
|
|
3729b3c5a0 | ||
|
|
7ce5eb3c27 | ||
|
|
43defea85e | ||
|
|
8470c4e39b | ||
|
|
1f678fc975 | ||
|
|
082c839639 | ||
|
|
600d548fce | ||
|
|
3035f9b686 | ||
|
|
6eae0f02d3 | ||
|
|
87be2f4b9e | ||
|
|
3b054504a1 | ||
|
|
a88f6b968a | ||
|
|
1fc4f150bf | ||
|
|
1f4e59cbdc | ||
|
|
b5dc8d9adf | ||
|
|
43f7e08548 | ||
|
|
05fc6f87ec | ||
|
|
daae535fa1 | ||
|
|
90c8cb3455 | ||
|
|
daeee10de9 | ||
|
|
6c1c401a71 | ||
|
|
fd7f0fceb2 | ||
|
|
26b8a616be | ||
|
|
d88882f439 | ||
|
|
09dc1d6baa | ||
|
|
f4f5e86979 | ||
|
|
488fd60d5d | ||
|
|
41025f64c0 | ||
|
|
a2eac2ff76 | ||
|
|
34f2a2391a | ||
|
|
a41afb7f1e | ||
|
|
32d9cfbe29 | ||
|
|
7210652567 | ||
|
|
ab15967ad7 | ||
|
|
44df4ec181 | ||
|
|
7afe356082 | ||
|
|
87597553b8 | ||
|
|
27e5f58d5e | ||
|
|
762c946d35 | ||
|
|
21a927e3e9 | ||
|
|
f93bb7436a | ||
|
|
6294fddbba | ||
|
|
c5719dfaf2 | ||
|
|
673fd67f15 | ||
|
|
25524c48e9 | ||
|
|
631b924c33 | ||
|
|
fba12bc278 | ||
|
|
e809109bb2 | ||
|
|
0e31890624 | ||
|
|
0124021ce5 | ||
|
|
74db6bf77f | ||
|
|
efde33182e | ||
|
|
ec68b22330 | ||
|
|
bf7e014f8c | ||
|
|
40e1607698 | ||
|
|
4a132f06fe | ||
|
|
0396dd975d | ||
|
|
80a38c0c54 | ||
|
|
2aa6461094 | ||
|
|
258433b3b8 | ||
|
|
79e723545c | ||
|
|
a6b20455ef | ||
|
|
9659b55bf3 | ||
|
|
ca148ef546 | ||
|
|
322b21d645 | ||
|
|
ed2ba65ecf | ||
|
|
defc8b1c57 | ||
|
|
a90ecc56d8 | ||
|
|
2faa0ac320 | ||
|
|
e391fd59fe | ||
|
|
25df86606c | ||
|
|
811f33eb3f | ||
|
|
ca7e2ed89d | ||
|
|
6f4cd79e2c | ||
|
|
da1caf4b8b | ||
|
|
4a0a8e44ca | ||
|
|
6bc2c3481b | ||
|
|
0aa89ea9ff | ||
|
|
f31a30bf47 | ||
|
|
dc75837ac7 | ||
|
|
9849b0a1da | ||
|
|
2c15a1ddd6 | ||
|
|
98eb9976cf | ||
|
|
0d9a5810b1 | ||
|
|
1adaa137a5 | ||
|
|
44a428d15a | ||
|
|
5416a7942a | ||
|
|
9e0024baf5 | ||
|
|
8d47ce38c2 | ||
|
|
80af43c0ca | ||
|
|
225f8243c2 | ||
|
|
68bc118add | ||
|
|
abbc584402 | ||
|
|
6635594639 | ||
|
|
10db77d402 | ||
|
|
000fd7e520 | ||
|
|
c8ced4ae59 | ||
|
|
9209ca9af7 | ||
|
|
c7bd90c610 | ||
|
|
fceb9c3547 | ||
|
|
030c49b571 | ||
|
|
f2d6a6a536 | ||
|
|
8f61521f05 | ||
|
|
89af7ec5d0 | ||
|
|
362f1aebed | ||
|
|
5226527cec | ||
|
|
b8464cd0e5 | ||
|
|
46e7b04d66 | ||
|
|
73111b770f | ||
|
|
995d485700 | ||
|
|
5ebbbef667 | ||
|
|
c79144400f | ||
|
|
b56556f5a2 | ||
|
|
35d5f01b8e | ||
|
|
501c647236 | ||
|
|
e77c7b84a3 | ||
|
|
d9f4e9b6ab | ||
|
|
1d7f7d2a5b | ||
|
|
df408e862b | ||
|
|
66845926d5 | ||
|
|
0a06acbf1d | ||
|
|
5d3c6798c0 | ||
|
|
b9d2b9ddc9 | ||
|
|
8a6525f45e | ||
|
|
41d89b590d | ||
|
|
8354d08ff5 | ||
|
|
16725b21f3 | ||
|
|
93ba17792f | ||
|
|
940b96cf21 | ||
|
|
157253ce24 | ||
|
|
46b3810f34 | ||
|
|
05c030dbbb | ||
|
|
5b0fd99351 | ||
|
|
2946a6e231 | ||
|
|
47896fcdc9 | ||
|
|
bc51345f0d | ||
|
|
750e1b6c43 | ||
|
|
4a2106837c | ||
|
|
39b9fc350a | ||
|
|
53560dbe29 | ||
|
|
5172dbe114 | ||
|
|
4f0ff67fdf | ||
|
|
bac971ced8 | ||
|
|
64bf5ba165 | ||
|
|
90c4a5e1b8 | ||
|
|
b08a5d9cda | ||
|
|
d5be79948d | ||
|
|
6b64be4925 | ||
|
|
e8e6eb6ca5 | ||
|
|
ee9383dd0b | ||
|
|
aee0b82cff | ||
|
|
ba5913da57 | ||
|
|
3238b9b2ce | ||
|
|
5215181c0f | ||
|
|
d3676a1454 | ||
|
|
792ce6f86e | ||
|
|
ddbd0376fc | ||
|
|
496655093c | ||
|
|
755293eff7 | ||
|
|
b6e51a1f32 | ||
|
|
08f17f3f19 | ||
|
|
14440725fc | ||
|
|
b4f84c5cd6 | ||
|
|
ad6b1cead1 | ||
|
|
e06398ff19 | ||
|
|
919ad5cfd4 | ||
|
|
b3fb721588 | ||
|
|
590497852d | ||
|
|
ebd7a9c7cf | ||
|
|
939fb2fa54 | ||
|
|
2245daffe9 | ||
|
|
784b25ada8 | ||
|
|
146d631794 | ||
|
|
cc062d7f0e | ||
|
|
298dd4af61 | ||
|
|
739ba3b14d | ||
|
|
2864ea9868 | ||
|
|
ed8d3247ca | ||
|
|
ff5de7b327 | ||
|
|
aa77552ff4 | ||
|
|
12456c0ea2 | ||
|
|
a7a93fa2a2 | ||
|
|
ff7cd29b77 | ||
|
|
f7065acc40 | ||
|
|
e4e2e5c43c | ||
|
|
48d240b010 | ||
|
|
9f2deb56b9 | ||
|
|
951257bed8 | ||
|
|
f27ce804fb | ||
|
|
1d896e83b3 | ||
|
|
a2c2925610 | ||
|
|
97e5f90603 | ||
|
|
593b7188dc | ||
|
|
4c4e61a711 | ||
|
|
140f09c77e | ||
|
|
c0fe4faf8a | ||
|
|
066fff7aca | ||
|
|
6dcbdffed4 | ||
|
|
3a07dea6d7 | ||
|
|
72ed07ef17 | ||
|
|
51512b4588 | ||
|
|
aed49e19e8 | ||
|
|
113601d09a | ||
|
|
b7d0b65715 | ||
|
|
3075591885 | ||
|
|
e9596f56db | ||
|
|
3559830738 | ||
|
|
e42beccb22 | ||
|
|
d05b7394e8 | ||
|
|
00b11ea659 | ||
|
|
99f0f096d1 | ||
|
|
416329d50d | ||
|
|
cb0d1b05d7 | ||
|
|
a2845c33f8 | ||
|
|
6d06265d94 | ||
|
|
49d03efe56 | ||
|
|
7f13a3ca76 | ||
|
|
cec9d168e3 | ||
|
|
ecf253451d | ||
|
|
89a6219659 | ||
|
|
9dec308caa | ||
|
|
fb1459de9b | ||
|
|
f3bef64461 | ||
|
|
2509caff6b | ||
|
|
232aafe2c0 | ||
|
|
ed26cb4891 | ||
|
|
0b966a6cd1 | ||
|
|
c84afd2281 | ||
|
|
1711f09547 | ||
|
|
4460a44e99 | ||
|
|
c8f7bcfb52 | ||
|
|
bd2bd842af | ||
|
|
2b0f4fe46b | ||
|
|
55c2f41c71 | ||
|
|
94985422ca | ||
|
|
eaff1aa58f | ||
|
|
c7cb9d0990 | ||
|
|
a9fdf30421 | ||
|
|
d0f45f6dfb | ||
|
|
8cdb6a3c9f | ||
|
|
b369a46431 | ||
|
|
f40ca2d5e0 | ||
|
|
400393c677 | ||
|
|
5710e3be55 | ||
|
|
af1312a92b | ||
|
|
d2a63c48b1 | ||
|
|
8f10ea7ed6 | ||
|
|
7bd701368a | ||
|
|
894e846a62 | ||
|
|
504ef5a7ab | ||
|
|
af6436da77 | ||
|
|
d031420ed3 | ||
|
|
4b6b7478de | ||
|
|
bb1b5eab96 | ||
|
|
9486b50342 | ||
|
|
fc9a5b3545 | ||
|
|
8b4d0e2541 | ||
|
|
0ef07f615c | ||
|
|
836413cff2 | ||
|
|
c71e34fee9 | ||
|
|
b389a4db92 | ||
|
|
0a1a54cb33 | ||
|
|
630228675c | ||
|
|
f7955d00fc | ||
|
|
68b08f9b9a | ||
|
|
612f136ced | ||
|
|
8c313c3d48 | ||
|
|
517a6c0062 | ||
|
|
46c61b10de | ||
|
|
edc8d27577 | ||
|
|
05354777fe | ||
|
|
98d3d2a39b | ||
|
|
f91f75912e | ||
|
|
9f21f67035 | ||
|
|
699e4da112 | ||
|
|
6458285e75 | ||
|
|
e0ddf3711f | ||
|
|
68d415375d | ||
|
|
0e11afdd8b | ||
|
|
ff2c01584f | ||
|
|
a59e20b864 | ||
|
|
d7d898896d | ||
|
|
2dde78d5e7 | ||
|
|
2ca39ff399 | ||
|
|
1f6fdaf9b3 | ||
|
|
edaf58135f | ||
|
|
724698fc51 | ||
|
|
6aabe9e12c | ||
|
|
2b5a1bb893 | ||
|
|
7403305f3c | ||
|
|
e780f8a3f0 | ||
|
|
67b09014aa | ||
|
|
7f2fda0327 | ||
|
|
ada8f74e2c | ||
|
|
1ddfeaf950 | ||
|
|
2ed2cc1499 | ||
|
|
be0f90f12a | ||
|
|
4b362df23b | ||
|
|
b953a1c2f6 | ||
|
|
4c30fa43d3 | ||
|
|
6866b1a3bb | ||
|
|
328030f152 | ||
|
|
fd195bd926 | ||
|
|
87e07366cd | ||
|
|
8133977e09 | ||
|
|
11199b996c | ||
|
|
eee61db189 | ||
|
|
c7c5130030 | ||
|
|
6de14a3840 | ||
|
|
55206b3dde | ||
|
|
e1a44477af | ||
|
|
23afee453e | ||
|
|
8e05309021 | ||
|
|
26fdf87070 | ||
|
|
1c95a0edc4 | ||
|
|
4723fb39e9 | ||
|
|
13d667d81c | ||
|
|
dce255dc58 | ||
|
|
f72c9704d9 | ||
|
|
e623010e91 | ||
|
|
587bfdc162 | ||
|
|
7c1c299282 | ||
|
|
f2a5c0b04b | ||
|
|
4ba77b76ec | ||
|
|
0bfce44317 | ||
|
|
13b6d6384c | ||
|
|
80838bbef0 | ||
|
|
8a557ff2fb | ||
|
|
16e394087d | ||
|
|
ee1b67b36e | ||
|
|
36b9fa2387 | ||
|
|
378169e939 | ||
|
|
70a01e559d | ||
|
|
c09e8196f3 | ||
|
|
0382c05152 | ||
|
|
75cf6e2a56 | ||
|
|
9fb4754430 | ||
|
|
0312b504a9 | ||
|
|
1d6a9651bf | ||
|
|
e36b18e85e | ||
|
|
4ae4951e0d | ||
|
|
a65f52ffba | ||
|
|
6de25ffa65 | ||
|
|
d8429bdd99 | ||
|
|
c907750446 | ||
|
|
26976ae6cf | ||
|
|
53beaca563 | ||
|
|
77628e2fb9 | ||
|
|
40d2f2de96 | ||
|
|
84cdb2483f | ||
|
|
c7866bfbbf | ||
|
|
5de1d6b343 | ||
|
|
7bde363704 | ||
|
|
1f9b2ce7b9 | ||
|
|
ac2e47776a | ||
|
|
e7de5ca263 | ||
|
|
f7f079e653 | ||
|
|
f7cccb33de | ||
|
|
69114c3cc0 | ||
|
|
8177c9c34b | ||
|
|
850c46f881 | ||
|
|
ffcfe966d2 | ||
|
|
800badd2a4 | ||
|
|
0a7ffbcc8f | ||
|
|
019ec4de9a | ||
|
|
78d1e4a12a | ||
|
|
6c99fefad0 | ||
|
|
24bc1424b9 | ||
|
|
94a0e17cfc | ||
|
|
2f012d8cf2 | ||
|
|
75b800054f | ||
|
|
d7c7733315 | ||
|
|
99600ad8d8 | ||
|
|
4a4c7b8b6b | ||
|
|
8c3267b345 | ||
|
|
c0240b047b | ||
|
|
c52957ccfe | ||
|
|
fb3f057adf | ||
|
|
e333bd08a4 | ||
|
|
1844a269cb | ||
|
|
859882d24f | ||
|
|
c95543b8b0 | ||
|
|
d874125dc1 | ||
|
|
795cd099f4 | ||
|
|
66ef6fd9d8 | ||
|
|
3b0655354d | ||
|
|
76acec93fd | ||
|
|
3b60068369 | ||
|
|
a0fd0a71a2 | ||
|
|
907accbcc9 | ||
|
|
ee284abf8d | ||
|
|
8239275770 | ||
|
|
029485bace | ||
|
|
cef9f6ae3b | ||
|
|
8d88c94956 | ||
|
|
692e155117 | ||
|
|
9a9410de2b | ||
|
|
a27f1181ea | ||
|
|
ac65cadb1b | ||
|
|
4345e75b20 | ||
|
|
f64d085e7b | ||
|
|
325b878f0a | ||
|
|
330f375a30 | ||
|
|
7d6c211de1 | ||
|
|
af5b36752c | ||
|
|
e0f563befb | ||
|
|
bf1b84dfea | ||
|
|
b3f8ce9c16 | ||
|
|
499152d066 | ||
|
|
c47e7edc9e | ||
|
|
afc034b495 | ||
|
|
e1d19741af | ||
|
|
8d2de40df6 | ||
|
|
f4b7db667f | ||
|
|
4032e52317 | ||
|
|
f9db4325d8 | ||
|
|
815824f76d | ||
|
|
8534672c33 | ||
|
|
b9bb9a166a | ||
|
|
185c886472 | ||
|
|
acc565d021 | ||
|
|
64db137c6c | ||
|
|
4f2bdeb2c9 | ||
|
|
527994084b | ||
|
|
ffaf5c9475 | ||
|
|
d1e24bfcd8 | ||
|
|
aab8612c63 | ||
|
|
79c1e2d21c | ||
|
|
7ff44f4839 | ||
|
|
2de0272e3e | ||
|
|
1d22fe72d0 | ||
|
|
7e9821551c | ||
|
|
8fb3cd71b9 | ||
|
|
b0f6f1f2ba | ||
|
|
df56dc4a4a | ||
|
|
b572ec4901 | ||
|
|
75759171c1 | ||
|
|
322fe185dd | ||
|
|
effe514ecb | ||
|
|
4ff9f69548 | ||
|
|
e1c7cb17da | ||
|
|
e6defd7770 | ||
|
|
bba9f45075 | ||
|
|
de1a2d1557 | ||
|
|
e995d06a26 | ||
|
|
63f54a1318 | ||
|
|
0828c9f901 | ||
|
|
51cd38450a | ||
|
|
0a4a720fea | ||
|
|
7f7ce171e0 | ||
|
|
87622cc6ec | ||
|
|
aaf2e64fdb | ||
|
|
c6f22e7ce5 | ||
|
|
749a2d19aa | ||
|
|
ed3491fbba | ||
|
|
f5e4b7cd6a | ||
|
|
0b614e81ee | ||
|
|
0a93b660cf | ||
|
|
71e104e4bd | ||
|
|
189c5de7ea | ||
|
|
a5e141c361 | ||
|
|
e76d8d4df7 | ||
|
|
d54c093985 | ||
|
|
086709e40f | ||
|
|
9477e32ec8 | ||
|
|
a66a0628ad | ||
|
|
ea018bd4c1 | ||
|
|
8e81ee3b75 | ||
|
|
9c326f9be9 | ||
|
|
7d2a0aa793 | ||
|
|
22d1442033 | ||
|
|
ffa32a5501 | ||
|
|
f50a6a8416 | ||
|
|
7a82b75ee2 | ||
|
|
350c0585b8 | ||
|
|
5c4454d3ed | ||
|
|
dfd1ae7a50 | ||
|
|
d16fdd062f | ||
|
|
d6432698fc | ||
|
|
1a9c1e8bc1 | ||
|
|
2d05c6cb13 | ||
|
|
05f689913f | ||
|
|
9e714c4192 | ||
|
|
e1ad1e31f8 | ||
|
|
b473bf1da8 | ||
|
|
dfdc6eecb6 | ||
|
|
e9145df77a | ||
|
|
4e824990d1 | ||
|
|
5a053378e1 | ||
|
|
e09306b6f3 | ||
|
|
21e577c1d3 | ||
|
|
9d85d0979c | ||
|
|
e4fec7ea1f | ||
|
|
c998e513fc | ||
|
|
855b06c4b2 | ||
|
|
776012f02f | ||
|
|
9bff531618 | ||
|
|
4d64818da1 | ||
|
|
837c8773a8 | ||
|
|
97a199b504 | ||
|
|
9cd37be5b1 | ||
|
|
90895fed52 | ||
|
|
df4b73abbb | ||
|
|
d2bed08ae0 | ||
|
|
fd5e1472e1 | ||
|
|
6a22126006 | ||
|
|
ff5e9049a0 | ||
|
|
7570aa9f49 | ||
|
|
4fcdd3da23 | ||
|
|
f8a65b4d13 | ||
|
|
5bb70a5043 | ||
|
|
c4788159e4 | ||
|
|
403b0aedca | ||
|
|
cae12b84df | ||
|
|
40dbbbf0a0 | ||
|
|
9e0da27484 | ||
|
|
7e6c2463d6 | ||
|
|
69a76db166 | ||
|
|
efd282e326 | ||
|
|
6471fef28c | ||
|
|
5a0691b70c | ||
|
|
27ea357348 | ||
|
|
e12de29cce | ||
|
|
a236be4e38 | ||
|
|
5300643f62 | ||
|
|
469b837daf | ||
|
|
84ca3e8982 | ||
|
|
bfb8be159a | ||
|
|
13d7af9c76 | ||
|
|
66d78497b9 | ||
|
|
32743f7e19 | ||
|
|
c2cc4451ff | ||
|
|
67370bbc3d | ||
|
|
1201ae1956 | ||
|
|
0eb2786ce5 | ||
|
|
bbdeb66f8e | ||
|
|
641698a652 | ||
|
|
15880e995a | ||
|
|
1e849b106b | ||
|
|
31bc405e86 | ||
|
|
6e69eee2a3 | ||
|
|
279b095f30 | ||
|
|
9821499cdb | ||
|
|
b76c39862a | ||
|
|
7948d234f5 | ||
|
|
50fc3aa9fc | ||
|
|
79a7adb05b | ||
|
|
c02770ec2a | ||
|
|
bf24033513 | ||
|
|
d97c87e90c | ||
|
|
6d29fdc448 | ||
|
|
d16f0dd3f6 | ||
|
|
d023c97049 | ||
|
|
73c8d63f0e | ||
|
|
30709de7e2 | ||
|
|
c3e560766e | ||
|
|
7c7889e87e | ||
|
|
6547ccecdd | ||
|
|
d467a591b0 | ||
|
|
42613ff480 | ||
|
|
3a3d4b9e54 | ||
|
|
0b5c0bcf74 | ||
|
|
5937f009e1 | ||
|
|
c7f3d2a64e | ||
|
|
41263307d7 | ||
|
|
f2ff52477e | ||
|
|
6baa5a0730 | ||
|
|
5a5cb8c866 | ||
|
|
a3a78366f7 | ||
|
|
d936292ed9 | ||
|
|
a3ded18f59 | ||
|
|
0969135f7f | ||
|
|
7e1c1a6324 | ||
|
|
aa43dd2a49 | ||
|
|
81fe9b6fc2 | ||
|
|
6807fdc1cc | ||
|
|
ac1590810f | ||
|
|
9dd4d77535 | ||
|
|
c7aa6a9d96 | ||
|
|
fc56db2f83 | ||
|
|
c58e56257d | ||
|
|
b27eb280a8 | ||
|
|
78c7f4078c | ||
|
|
28ccaffcf5 | ||
|
|
78739558d6 | ||
|
|
cd195d30de | ||
|
|
03bf752284 | ||
|
|
238fab3e1d | ||
|
|
fcd9af8f84 | ||
|
|
b44491ebbe | ||
|
|
1f8018fd5b | ||
|
|
1d67656fa0 | ||
|
|
64b8023d1a | ||
|
|
cc1697e7ec | ||
|
|
28943e77e8 | ||
|
|
575109da9f | ||
|
|
a99667d471 | ||
|
|
6a7420bd3a | ||
|
|
e8dbe05615 | ||
|
|
6b6566cd29 | ||
|
|
0001d31c2c | ||
|
|
974686e698 | ||
|
|
7b7063b9be | ||
|
|
55061a9469 | ||
|
|
c433fb643c | ||
|
|
02306385b6 | ||
|
|
432ac1bcec | ||
|
|
d9480e0c9a | ||
|
|
815fb911d6 | ||
|
|
68cbdae8e0 | ||
|
|
2d8f8aeef3 | ||
|
|
479bc7be71 | ||
|
|
cbc6df2e62 | ||
|
|
d16b4cfadb | ||
|
|
a97bad1f86 | ||
|
|
7198ffff43 | ||
|
|
e173159d13 | ||
|
|
1b54b79e88 | ||
|
|
bb3436615e | ||
|
|
809db61c35 | ||
|
|
88e53fcba8 | ||
|
|
fe68b7e294 | ||
|
|
b7b76d6da7 | ||
|
|
e2eae43fc9 | ||
|
|
f41ecec09c | ||
|
|
15ad4d11ef | ||
|
|
0cf9d98f14 | ||
|
|
612e642523 | ||
|
|
0ff77eb157 | ||
|
|
2b8935a5d7 | ||
|
|
afdc5c8460 | ||
|
|
91ba2dff2d | ||
|
|
2d26079c49 | ||
|
|
f13d99e0d1 | ||
|
|
798c95d8a8 | ||
|
|
ef77c2acfb | ||
|
|
11a98267a2 | ||
|
|
b2aa1155d0 | ||
|
|
d3182b8d2a | ||
|
|
f52d139acc | ||
|
|
87e9a38548 | ||
|
|
faa70c57b3 | ||
|
|
5172c07c18 | ||
|
|
a80fa03db4 | ||
|
|
d73e02948e | ||
|
|
283657e1b7 | ||
|
|
d3c4a3a17e | ||
|
|
9184bc40e5 | ||
|
|
84bd98ebf4 | ||
|
|
4ef2cbcaeb | ||
|
|
35f8b45bf4 | ||
|
|
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 | ||
|
|
0474e5b1fe | ||
|
|
15b84739e7 | ||
|
|
4bfe296e1a | ||
|
|
3fcc0db4f8 | ||
|
|
a8238565f3 | ||
|
|
53114462b3 | ||
|
|
094da79cea | ||
|
|
f093206a1c | ||
|
|
5b4940d017 | ||
|
|
aee36eeec6 | ||
|
|
cc783f8be1 | ||
|
|
a20f491cf1 | ||
|
|
f5ff9bf263 | ||
|
|
dcba54b499 | ||
|
|
d1e103c1d7 | ||
|
|
542daba206 | ||
|
|
94b61b1bbd | ||
|
|
718e590bfd | ||
|
|
9ce5e184c0 | ||
|
|
dbc477765f | ||
|
|
92fcadf3f3 | ||
|
|
c437fd96a8 | ||
|
|
10d33ecb82 | ||
|
|
d506c0cb27 | ||
|
|
acac7f7540 | ||
|
|
c853bd282a | ||
|
|
ecc4550261 | ||
|
|
c26ece7166 | ||
|
|
88b50c7902 | ||
|
|
7ba2cdd6ff | ||
|
|
589d9a2f1d | ||
|
|
a92411b95b | ||
|
|
472051bd24 | ||
|
|
e5109a1f43 | ||
|
|
f7ae9e3574 | ||
|
|
170ec3c636 | ||
|
|
1ab3fa8b3b | ||
|
|
8b046512e3 | ||
|
|
228a10c8e0 | ||
|
|
9c53bea190 | ||
|
|
11cf991498 | ||
|
|
a88c3721b2 | ||
|
|
0b4b6d4d91 | ||
|
|
941f9bcd48 | ||
|
|
09988a858d | ||
|
|
f1bf9fb25c | ||
|
|
1751fa49c0 | ||
|
|
6b4fc9a4fa | ||
|
|
7c8d85e428 | ||
|
|
e335140f23 | ||
|
|
d85f398b5f | ||
|
|
a16082a59d | ||
|
|
456269a343 | ||
|
|
eb8e1e20eb | ||
|
|
ed3c84fec0 | ||
|
|
be40416a2d | ||
|
|
5b5476a513 | ||
|
|
dc64dd6400 | ||
|
|
eca02d3bde | ||
|
|
176b6c2936 | ||
|
|
5b22350bdf | ||
|
|
6e1e011234 | ||
|
|
ac65ef6a5c | ||
|
|
fc198dde74 | ||
|
|
15ac51b2fc | ||
|
|
34214432e1 | ||
|
|
361ca92493 | ||
|
|
e367051b80 | ||
|
|
a2a4a50c5e | ||
|
|
afc74b2f2a | ||
|
|
fc756d1eaf | ||
|
|
eb8a4b1e49 | ||
|
|
8d258b3538 | ||
|
|
a59cfa3477 | ||
|
|
f1e513006e | ||
|
|
9df5c8f439 | ||
|
|
3ae099accf | ||
|
|
bb3e9396f2 | ||
|
|
1628749bde | ||
|
|
f2006b5e42 | ||
|
|
80d387d9e7 | ||
|
|
4452b4d599 | ||
|
|
dfeaeb9888 | ||
|
|
7e45a20ee7 | ||
|
|
f3fe92e4de | ||
|
|
b606909c65 | ||
|
|
2882bb30d7 | ||
|
|
5b62227e3f | ||
|
|
8b6af6fd8a | ||
|
|
99e9a92953 | ||
|
|
9f626309c3 | ||
|
|
3fe7cf2bfd | ||
|
|
9b5c274b49 | ||
|
|
46b350e7ac | ||
|
|
22a4aeb108 | ||
|
|
332e116ba7 | ||
|
|
8b594a1a1f | ||
|
|
ab23ec6d4d | ||
|
|
0ef574d675 | ||
|
|
6d15a2462d | ||
|
|
24fcdeb7aa | ||
|
|
13905db732 | ||
|
|
6e1c8e5bec | ||
|
|
9aa1d11b94 | ||
|
|
6c9f359fae | ||
|
|
531ebcae85 | ||
|
|
fe9601b510 | ||
|
|
bdf7cc6ea0 | ||
|
|
1cfe02af6f | ||
|
|
647e3f9383 | ||
|
|
597f52799d | ||
|
|
a59e052ed8 | ||
|
|
11da0a4500 | ||
|
|
fd736bd1c2 | ||
|
|
ca6a4bfeef | ||
|
|
02e9debc42 | ||
|
|
9bc9bd8b95 | ||
|
|
d810f79b7a | ||
|
|
f3468951f1 | ||
|
|
7ec5badabb | ||
|
|
1ff2f501ca | ||
|
|
cfcb49e233 | ||
|
|
467df2020e | ||
|
|
a961b41de0 | ||
|
|
40e8d5225e | ||
|
|
bc755ae1df | ||
|
|
b1cb0c3786 | ||
|
|
090d0fa2db | ||
|
|
27918a12b0 | ||
|
|
ba1498b0b2 | ||
|
|
cbde96dd82 | ||
|
|
344118a755 | ||
|
|
259c8a4bd9 | ||
|
|
fe92e41e91 | ||
|
|
e58c2f2a99 | ||
|
|
f4d5bd1bea | ||
|
|
20b352cabe | ||
|
|
20e35f1a69 | ||
|
|
d963f56d0f | ||
|
|
aecfbc7728 | ||
|
|
5734df89f0 | ||
|
|
bdf9b864d4 | ||
|
|
1c0f1a036b | ||
|
|
327c9de464 | ||
|
|
8b2f994769 | ||
|
|
a5e53d872b | ||
|
|
1868d90693 | ||
|
|
da0c19e068 | ||
|
|
d1103d8db4 | ||
|
|
b2e92646a1 | ||
|
|
19bc2444bc | ||
|
|
831b649cbb | ||
|
|
ded3c204b9 | ||
|
|
23eec5f066 | ||
|
|
6c167090e1 | ||
|
|
7d9eca0d46 | ||
|
|
c551aff474 | ||
|
|
e627745358 | ||
|
|
5a30d9d2b5 | ||
|
|
0a46817bbc | ||
|
|
a4134fa8c8 | ||
|
|
683535a5a6 | ||
|
|
edb53112c2 | ||
|
|
83a77af520 | ||
|
|
df3ae17c7b | ||
|
|
4a1624a443 | ||
|
|
a8de9f9f9f | ||
|
|
3aa5b40acd | ||
|
|
8400f3e874 | ||
|
|
b40bca1913 | ||
|
|
7100257f31 | ||
|
|
17df1a4d8a | ||
|
|
d7a5209c68 | ||
|
|
076220eacd | ||
|
|
99a50f271a | ||
|
|
63d265da06 | ||
|
|
30e3624eb1 | ||
|
|
88f3713e28 | ||
|
|
90f0c22545 | ||
|
|
8deed8468d | ||
|
|
923ad26b1b | ||
|
|
3bc858e4c2 | ||
|
|
f5a7fa41a7 | ||
|
|
bf71d5508b | ||
|
|
b44c9cfc51 | ||
|
|
5b4338abae | ||
|
|
aa5adc28cb | ||
|
|
2dad013cc0 | ||
|
|
7ade66f3ac | ||
|
|
ed75a64b46 | ||
|
|
e156b80d91 | ||
|
|
e8f79ae467 | ||
|
|
90e4862280 | ||
|
|
438080d3d6 | ||
|
|
3c17605764 | ||
|
|
3f68bc0eda | ||
|
|
ecbee73eae | ||
|
|
3e4452da00 | ||
|
|
549c690b56 | ||
|
|
aabe06f29b | ||
|
|
82693c5cd3 | ||
|
|
37a4f26d2f | ||
|
|
ca94063c7b | ||
|
|
eadc4bf6c2 | ||
|
|
b1c307c86b | ||
|
|
1874a0056d | ||
|
|
48331f9552 | ||
|
|
f907aa578a | ||
|
|
41e2620cc1 | ||
|
|
e7a82b167a | ||
|
|
088c556b00 | ||
|
|
c80343b6d4 | ||
|
|
4e52a8cf60 | ||
|
|
1ed1d4233f | ||
|
|
6e4626bc02 | ||
|
|
2608ae247f | ||
|
|
785586bfe9 | ||
|
|
bdcbb177ae | ||
|
|
15ac365d79 | ||
|
|
debbcb753b | ||
|
|
69d73aeaa4 | ||
|
|
dffe53370f | ||
|
|
4334e6dcdf | ||
|
|
c2c6c093d5 | ||
|
|
77e539eec2 | ||
|
|
a57970210e | ||
|
|
1b31a46fb7 | ||
|
|
87f19c74fc | ||
|
|
bd157a9724 | ||
|
|
5a327eb0db | ||
|
|
4b9c0b0109 | ||
|
|
df6b75cdbb | ||
|
|
0b4f8c122b | ||
|
|
2a87eaf3e5 | ||
|
|
c52266f5cf | ||
|
|
3b21f8add2 | ||
|
|
6574bd10a0 | ||
|
|
23f3335988 | ||
|
|
a5d7f33c82 | ||
|
|
3782c4dac0 | ||
|
|
1fc02fd2fe | ||
|
|
cc347c1dbe | ||
|
|
79ff20eb18 | ||
|
|
e6e8a447da | ||
|
|
233f0c5bdb | ||
|
|
9ed4271a14 | ||
|
|
470c0b6b43 | ||
|
|
afa8ae42b9 | ||
|
|
63d426503f | ||
|
|
ffb7f80b26 | ||
|
|
63f8826fd8 | ||
|
|
ef836e8b84 | ||
|
|
abc1c43a51 | ||
|
|
6b54dd9e0d | ||
|
|
1f54e7752d | ||
|
|
6ac941f276 | ||
|
|
a4fe92562f | ||
|
|
b9bd1d9d4b | ||
|
|
3b6c28488a | ||
|
|
875eb3500d | ||
|
|
3a88a2451c | ||
|
|
6800b73a4f | ||
|
|
983404e6d8 | ||
|
|
b95c0a18a7 | ||
|
|
36b317cad8 | ||
|
|
35d74888fb | ||
|
|
6c308483f7 | ||
|
|
9d25fb74ec | ||
|
|
d217b52744 | ||
|
|
319da4b174 | ||
|
|
9bee467942 | ||
|
|
44ac70fc97 | ||
|
|
d897611d62 | ||
|
|
c2ae251e73 | ||
|
|
35ad285864 | ||
|
|
97bdae21eb | ||
|
|
d6dc6e43c7 | ||
|
|
01e6e530d5 | ||
|
|
9ec0178beb | ||
|
|
d66f2f6d24 | ||
|
|
79cd4004cc | ||
|
|
991243e2df | ||
|
|
b91cf11d86 | ||
|
|
d182ec09fa | ||
|
|
8641822358 | ||
|
|
9665cbb428 | ||
|
|
a280dfaf3b | ||
|
|
3e56521ea8 | ||
|
|
b205230ea9 | ||
|
|
51645ab126 | ||
|
|
5d04897e75 | ||
|
|
1ac0ea5cc6 | ||
|
|
a07e8b51e5 | ||
|
|
a81f0238f4 | ||
|
|
2b81eb8ec7 | ||
|
|
e5eb642781 | ||
|
|
a4cbe25733 | ||
|
|
2042c85b22 | ||
|
|
3149f8745c | ||
|
|
15b9f1616f | ||
|
|
94c02b7288 | ||
|
|
7e70b59a59 | ||
|
|
2c7f5e41ed | ||
|
|
108b8df280 | ||
|
|
553098f9be | ||
|
|
131eb78407 | ||
|
|
f956a279a5 | ||
|
|
7150686b92 | ||
|
|
4b1fb2c173 | ||
|
|
94464bf608 | ||
|
|
2faa88784a | ||
|
|
e6607b53d8 | ||
|
|
3f6a6c864a | ||
|
|
30a578257d | ||
|
|
8411134adf | ||
|
|
f86a5d1a19 | ||
|
|
be72492537 | ||
|
|
76f9e8ec6e | ||
|
|
8fb1c44e58 | ||
|
|
f607b35cf3 | ||
|
|
0e56bec35a | ||
|
|
c890d10114 | ||
|
|
dee2fe5ce7 | ||
|
|
4a4d767bce | ||
|
|
d57e0cf601 | ||
|
|
7fa141dd1b | ||
|
|
c261a0cbca | ||
|
|
66661cbd49 | ||
|
|
100c126c3d | ||
|
|
d466e3077d | ||
|
|
24587dc34e | ||
|
|
32cc57dd03 | ||
|
|
a55488846b | ||
|
|
dcf61fd4e2 | ||
|
|
5bf998468a | ||
|
|
01c9625c59 | ||
|
|
772c378922 | ||
|
|
ee50a91379 | ||
|
|
9cfda3bad8 | ||
|
|
aa19b08bd9 | ||
|
|
87f69bb7e2 | ||
|
|
41c0aeedbe | ||
|
|
3cbe53d76f | ||
|
|
aed60d6c1e | ||
|
|
be7d35490d | ||
|
|
d0ea997c63 | ||
|
|
1fecffeba2 | ||
|
|
76319a56a2 | ||
|
|
1d71de7031 | ||
|
|
e9a1cfea11 | ||
|
|
9115856d19 | ||
|
|
8c45266c18 | ||
|
|
6b4130df89 | ||
|
|
e3e10e7dfa | ||
|
|
cbf900004d | ||
|
|
51b36dc460 | ||
|
|
9f8016afe2 | ||
|
|
d5a36db50a | ||
|
|
ecd458d8d0 | ||
|
|
9088297c41 | ||
|
|
5282deb088 | ||
|
|
75d661f12b | ||
|
|
83bc769d9e | ||
|
|
8324acadc8 | ||
|
|
6e42db41be | ||
|
|
3917bfc9e6 | ||
|
|
d11febb1ce | ||
|
|
360eb6f9cc | ||
|
|
e4ac5d01d0 | ||
|
|
6a51fc0668 | ||
|
|
ba03f07fbe | ||
|
|
7bf7d63f64 | ||
|
|
d3efaabc24 | ||
|
|
b4283ed98b | ||
|
|
de407e4cf9 | ||
|
|
60ed3a9836 | ||
|
|
7948358d85 | ||
|
|
96b82bb9b2 | ||
|
|
699ccf13f0 | ||
|
|
ae88aa4e42 | ||
|
|
bcce13b12f | ||
|
|
4fd4f660a7 | ||
|
|
518e59b33c | ||
|
|
165cdcc00d | ||
|
|
48c2115fdf | ||
|
|
d7d68ccdeb | ||
|
|
5cf6362db4 | ||
|
|
4efcc48160 | ||
|
|
383274ce0f | ||
|
|
c9dec3a2f7 | ||
|
|
2d4bf2903b | ||
|
|
2b88cfbda0 | ||
|
|
eb6ab7a156 | ||
|
|
9bed7bf213 | ||
|
|
99c2796014 | ||
|
|
9ee9bf12ae | ||
|
|
688cbe50f2 | ||
|
|
e0577d1628 | ||
|
|
de70925f8a | ||
|
|
0f8dd17fde | ||
|
|
4bc8a08606 | ||
|
|
cf34433186 | ||
|
|
21113d6fc8 | ||
|
|
d7decba3f5 | ||
|
|
671364f395 | ||
|
|
d0072237e7 | ||
|
|
d11c001e0d | ||
|
|
a197453ce0 | ||
|
|
d1586a8d80 | ||
|
|
6e959e415f | ||
|
|
8a8e14701b | ||
|
|
60bb144020 | ||
|
|
b561db3a67 | ||
|
|
b0e722acce | ||
|
|
279bd16b74 | ||
|
|
2e0081b66c | ||
|
|
f79d32b22b | ||
|
|
3579606d1e | ||
|
|
b93c6266a6 | ||
|
|
ea9c530667 | ||
|
|
6ea95c050a | ||
|
|
5a99a28195 | ||
|
|
0cd5351c07 | ||
|
|
148bed801b | ||
|
|
87a1a3ba1b | ||
|
|
20d2c10bba | ||
|
|
3eed5e395e | ||
|
|
870fbaa05c | ||
|
|
55868f68da | ||
|
|
e6f0fbeab5 | ||
|
|
8a554349b5 | ||
|
|
ab87d4e564 | ||
|
|
8400a83b70 | ||
|
|
5d2caa37a9 | ||
|
|
a6b7cfc2d3 | ||
|
|
74107b90bb | ||
|
|
9f4a915d43 | ||
|
|
3f1f22e1c3 | ||
|
|
1d98eb2b95 | ||
|
|
f1b56fed16 | ||
|
|
23e75c56ed | ||
|
|
eafc9cc1fa | ||
|
|
3e3dbb22df | ||
|
|
127b8ba0bd | ||
|
|
8a8e588a01 | ||
|
|
3c1b010821 | ||
|
|
739c938576 | ||
|
|
69fed0c347 | ||
|
|
d040258296 | ||
|
|
abee18839b | ||
|
|
9c0eb4e27e | ||
|
|
10598ef5e5 | ||
|
|
fa09e2d21d | ||
|
|
11ddfe3445 | ||
|
|
9abbc8876b | ||
|
|
c5cf99e13d | ||
|
|
82ac1c8f5e | ||
|
|
837f8146e4 | ||
|
|
9698c3f2ee | ||
|
|
85290b8f9d | ||
|
|
1ccf48d7a5 | ||
|
|
2cbec9f2b6 | ||
|
|
692be415fa | ||
|
|
83d5218f92 | ||
|
|
324b016324 | ||
|
|
005c9673f7 | ||
|
|
b8762dc6e0 | ||
|
|
93abff0768 | ||
|
|
ac1cfb43d8 | ||
|
|
30ec712782 | ||
|
|
bbd93fbc5b | ||
|
|
c46cbaff4b | ||
|
|
1e979b256a | ||
|
|
50429b419f | ||
|
|
2c540be5a3 | ||
|
|
155d3d222f | ||
|
|
3fc3bf1302 | ||
|
|
f0fafbbc6e | ||
|
|
5f7d980d89 | ||
|
|
c3799ed7fe | ||
|
|
f7533af5d8 | ||
|
|
2c9504fb93 | ||
|
|
c22b3f5335 | ||
|
|
2aea7fd46d | ||
|
|
bb9dda0cf1 | ||
|
|
ced6f05e59 | ||
|
|
cc14b5d4d4 | ||
|
|
2922f7110d | ||
|
|
762a877c5e | ||
|
|
aace7a8260 | ||
|
|
ac91abcb45 | ||
|
|
5d6ccd84c9 | ||
|
|
bd2f980f8e | ||
|
|
69af330ba1 | ||
|
|
b7bc3bfa8f | ||
|
|
678663ad66 | ||
|
|
ec3aa14112 | ||
|
|
3cdd98dc1a | ||
|
|
fc3270efd2 | ||
|
|
0dd871d3b5 | ||
|
|
41108ff407 | ||
|
|
99de8cf220 | ||
|
|
a60e887eb3 | ||
|
|
5133cb5c8f | ||
|
|
1607d84c21 | ||
|
|
cc0a990af8 | ||
|
|
fec5bdcfc8 | ||
|
|
648514d150 | ||
|
|
37b9745b6b | ||
|
|
f858b1144c | ||
|
|
c7cd7b83fa | ||
|
|
3cef086b69 | ||
|
|
ca51f591de | ||
|
|
da63404a84 | ||
|
|
de2160f992 | ||
|
|
8b7bb36e66 | ||
|
|
6ade91dafa | ||
|
|
2ef66f3011 | ||
|
|
5b2fd0bfbb | ||
|
|
ab392e2cf3 | ||
|
|
8eb922e93f | ||
|
|
20900dce07 | ||
|
|
c2705ff5d2 | ||
|
|
8af70fa7b5 | ||
|
|
e7eb8099ac | ||
|
|
cedf2eafbb | ||
|
|
e7553c68a0 | ||
|
|
cea7f1c2d1 | ||
|
|
45937f9c9c | ||
|
|
ee2a1ea924 | ||
|
|
1851c205e9 | ||
|
|
c4a3947cb3 | ||
|
|
d3e76bcf21 | ||
|
|
cf4e64d6c5 | ||
|
|
4cac571f1b | ||
|
|
445ed92ff7 | ||
|
|
6686ce15c1 | ||
|
|
59f134e0cd | ||
|
|
aab3f8c56f | ||
|
|
17c26e2a96 | ||
|
|
aec9124ef5 | ||
|
|
2df1e9bc2e | ||
|
|
6ceecbfd0f | ||
|
|
460726cadb | ||
|
|
32baa7b9f1 | ||
|
|
f44e648ccc | ||
|
|
2ad385acb6 | ||
|
|
ebaec2eaf0 | ||
|
|
dac8c2c15d | ||
|
|
60965491b0 | ||
|
|
19a3faa85a | ||
|
|
3779346eed | ||
|
|
185e263ddd | ||
|
|
8c0d984955 | ||
|
|
8d1012eda0 | ||
|
|
03f147e3d0 | ||
|
|
13ef5d640c | ||
|
|
2e8eed5afe | ||
|
|
54653a5bea | ||
|
|
e29082eba3 | ||
|
|
b635e9bb0d | ||
|
|
179bfdc3d2 | ||
|
|
631cb104cd | ||
|
|
1aeaf6855e | ||
|
|
6bac1ace30 | ||
|
|
488bab7c4d | ||
|
|
69fbfd7723 | ||
|
|
7697713c44 | ||
|
|
ee9b072f45 | ||
|
|
741defd31e | ||
|
|
e259b37f74 | ||
|
|
9a87de797c | ||
|
|
5d44ba658e | ||
|
|
00518b8231 | ||
|
|
6a3a99d2ac | ||
|
|
819434968c | ||
|
|
9f55ca2fdb | ||
|
|
7f69563edb | ||
|
|
b5cf0f987e | ||
|
|
28ad0b39c3 | ||
|
|
b4e9040d5c | ||
|
|
18a6ff0aa5 | ||
|
|
66e8c25265 | ||
|
|
aba1f2d35b | ||
|
|
5f29bcea8f | ||
|
|
fac6fd7926 | ||
|
|
5f4ab201af | ||
|
|
9591d36f9d | ||
|
|
a38f34995d | ||
|
|
f72564e48d | ||
|
|
39ae743c64 | ||
|
|
9cdb355878 | ||
|
|
6bdabbc96b | ||
|
|
d9e7d5ff6f | ||
|
|
9f41da7868 | ||
|
|
d4f7258ed1 | ||
|
|
aa34d78052 | ||
|
|
72712e8e0e | ||
|
|
bf25054ef2 | ||
|
|
32efafb404 | ||
|
|
3748ba1afa | ||
|
|
5baf91dc77 | ||
|
|
a152a48402 | ||
|
|
5fb4c4c20c | ||
|
|
80cf4f05f8 | ||
|
|
ce8e532f61 | ||
|
|
7e04fcbb6c | ||
|
|
71c37d0b8b | ||
|
|
518375e29a | ||
|
|
25c845c1c7 | ||
|
|
6a468b339f | ||
|
|
05a52dd482 | ||
|
|
5f1413ea1f | ||
|
|
84e45482a4 | ||
|
|
32bfdca562 | ||
|
|
547971545e | ||
|
|
c12b16faf9 | ||
|
|
858d6c8723 | ||
|
|
0f021fae9f | ||
|
|
5cb42264c5 | ||
|
|
c17af4098f | ||
|
|
e6c094b433 | ||
|
|
b15a86618b | ||
|
|
2e66cee551 | ||
|
|
c6e3d0125a | ||
|
|
f60f7d32dc | ||
|
|
dd7175d0c6 | ||
|
|
ba0e87f32a | ||
|
|
cebba5a09d | ||
|
|
7b0c31c641 | ||
|
|
487be74e83 | ||
|
|
3c7c88e0f5 | ||
|
|
e876e89b41 | ||
|
|
065fd9632c | ||
|
|
f7d8641e31 | ||
|
|
ed6456599b | ||
|
|
649538846a | ||
|
|
7468db5269 | ||
|
|
8291e94de4 | ||
|
|
94d30fc7ec | ||
|
|
36c6f371c5 | ||
|
|
94b3f66e14 | ||
|
|
c9527ddacd | ||
|
|
805c728e92 | ||
|
|
7812a86adb | ||
|
|
d22defcd83 | ||
|
|
d06829ff7b | ||
|
|
9110a1aca6 | ||
|
|
d7c90601d0 | ||
|
|
7b5fc600f5 | ||
|
|
0e699a1918 | ||
|
|
49f9964bc7 | ||
|
|
ee3ec4b14d | ||
|
|
cdcc9f0aff | ||
|
|
d56a01998f | ||
|
|
4e7f7f7fa0 | ||
|
|
201d8f8aee | ||
|
|
3847cb4d2d | ||
|
|
89a338ac33 | ||
|
|
0084d113d3 | ||
|
|
6d49cc3577 | ||
|
|
910c45a6be | ||
|
|
1d16ad764f | ||
|
|
142021c849 | ||
|
|
c87b9768b1 | ||
|
|
fbc3c1a9a4 | ||
|
|
05d1656a9e | ||
|
|
98804db478 | ||
|
|
c90f18c45a | ||
|
|
553783bfce | ||
|
|
fbf67f28f5 | ||
|
|
b8005466cd | ||
|
|
82177a386e | ||
|
|
176b276835 | ||
|
|
a4732194ef | ||
|
|
4a815daf4d | ||
|
|
8aa7804e8d | ||
|
|
3b7086627d | ||
|
|
20fb4036d1 | ||
|
|
3679ad3d70 | ||
|
|
63f23c571f | ||
|
|
7e96303c7d | ||
|
|
4f5879179e | ||
|
|
5a81f54a1b | ||
|
|
d8ccaf3578 | ||
|
|
7d0848f22b | ||
|
|
b02cffab56 | ||
|
|
59b6deb7be | ||
|
|
1b734051d5 | ||
|
|
037fe41961 | ||
|
|
7bf1263c04 | ||
|
|
0e4afd4681 | ||
|
|
21930021ed | ||
|
|
748b5c54c9 | ||
|
|
82d305008b | ||
|
|
1f24955533 | ||
|
|
ccfa28b895 | ||
|
|
2e3562d87e | ||
|
|
f1be109c15 | ||
|
|
b09bd39ad4 | ||
|
|
d812c3d61b | ||
|
|
0d7772a4c6 | ||
|
|
a5abf4d186 | ||
|
|
c938a13482 | ||
|
|
0bfc395986 | ||
|
|
60c536f444 | ||
|
|
8129e6e0c1 | ||
|
|
223a8e9a5e | ||
|
|
0e21c75007 | ||
|
|
6ca031e8fd | ||
|
|
f02e86fb50 | ||
|
|
b17f8244da | ||
|
|
4c02ce138b | ||
|
|
5eba208b00 | ||
|
|
2a4fbbfb35 | ||
|
|
5dd8bdcae6 | ||
|
|
c3e82eea5d | ||
|
|
2cfb96be33 | ||
|
|
715eedfab1 | ||
|
|
7f3a1c9a7d | ||
|
|
8413d2b31a | ||
|
|
04a03da382 | ||
|
|
e3274657ea | ||
|
|
f3b25cb792 | ||
|
|
d181f886fe | ||
|
|
616d073395 | ||
|
|
d36fc19585 | ||
|
|
95d9e07e2f | ||
|
|
25077a7b9a | ||
|
|
6971cd1a6b | ||
|
|
1e43a65743 | ||
|
|
2f2cbf343e | ||
|
|
3426b3cdeb | ||
|
|
e1d4c172f4 | ||
|
|
4c4a67afaf | ||
|
|
d33f210940 | ||
|
|
ead59bd410 | ||
|
|
f9445de71f | ||
|
|
c26995779a | ||
|
|
c6277453a3 | ||
|
|
91ebf2ba6f | ||
|
|
0f87bdd5a3 | ||
|
|
9e97042dd1 | ||
|
|
b7c4a99e71 | ||
|
|
48e315453e | ||
|
|
a8a6d14ca3 | ||
|
|
e895dd3430 | ||
|
|
f59859137a | ||
|
|
dee92e9e40 | ||
|
|
6701f4f95e | ||
|
|
e20f769854 | ||
|
|
4f762a9432 | ||
|
|
3c49eb1635 | ||
|
|
bdc6a282e2 | ||
|
|
8392ab2cc4 | ||
|
|
93c7c09f8c | ||
|
|
120116414f | ||
|
|
11794e5819 | ||
|
|
edce3d7bec | ||
|
|
e133e32e7c | ||
|
|
471859e448 | ||
|
|
d8de66eb14 | ||
|
|
cfdc0237d7 | ||
|
|
05fad24eda | ||
|
|
d4818c5567 | ||
|
|
8e8e6a7b93 | ||
|
|
6547f0ffad | ||
|
|
6f172fffa8 | ||
|
|
ed16e06676 | ||
|
|
1874f06f42 | ||
|
|
e9db24429a | ||
|
|
a59f4d45ca | ||
|
|
c7b3e0926c | ||
|
|
f0f5258bc9 | ||
|
|
12c07cf793 | ||
|
|
d2b8c85015 | ||
|
|
b9652291bd | ||
|
|
b0d1f93bfc | ||
|
|
a6d6c247a8 | ||
|
|
553416c927 | ||
|
|
b83696bc60 | ||
|
|
23ce320d75 | ||
|
|
dd170aafee | ||
|
|
27d5733dbc | ||
|
|
9ba769c53e | ||
|
|
8ff57e3004 | ||
|
|
5c75c6c7d3 | ||
|
|
56efb20ffa | ||
|
|
699578bb59 | ||
|
|
8d7d01bf88 | ||
|
|
0bc37d2fc2 | ||
|
|
aaa1655af1 | ||
|
|
f1bd4e1bba | ||
|
|
f74147070a | ||
|
|
20f4ea93e4 | ||
|
|
15a28e7c83 | ||
|
|
c550e1de54 | ||
|
|
ab26e561fd | ||
|
|
66968a28a3 | ||
|
|
37d1f91224 | ||
|
|
9e69068d42 | ||
|
|
8e2a9fcd01 | ||
|
|
1753887916 | ||
|
|
21bcffcc87 | ||
|
|
1caed49c75 | ||
|
|
619ea35168 | ||
|
|
877f913e8f | ||
|
|
25c47390c0 | ||
|
|
b3c46348a1 | ||
|
|
6f154194f1 | ||
|
|
c3ab08ce17 | ||
|
|
004fffa992 | ||
|
|
d6bd80c9c0 | ||
|
|
318bcdd011 | ||
|
|
3076f2af68 | ||
|
|
3dd9ef5564 | ||
|
|
367e5fa84e | ||
|
|
97cd61fd13 | ||
|
|
869bf7a345 | ||
|
|
9e15ac242d | ||
|
|
84943e58f1 | ||
|
|
2289bf0a27 | ||
|
|
7fda40c983 | ||
|
|
7eeed8f670 | ||
|
|
4d3f4ed5c2 | ||
|
|
145a4f5c20 | ||
|
|
9afe3d26e9 | ||
|
|
b73a7f1ed8 | ||
|
|
91a2bc3862 | ||
|
|
78a8a840b0 | ||
|
|
f4d54b6ca3 | ||
|
|
bc7a1c332c | ||
|
|
0e75cb9095 | ||
|
|
41b6fb6dcd | ||
|
|
2ca3cbc88f | ||
|
|
d05641a3d6 | ||
|
|
28bf84e05c | ||
|
|
ff51b53660 | ||
|
|
8b8e034b18 | ||
|
|
39927b06e3 | ||
|
|
66db2e7d16 | ||
|
|
a927c33ef1 | ||
|
|
17bc18b881 | ||
|
|
aa643c4a82 | ||
|
|
836fbea676 | ||
|
|
045049243c | ||
|
|
b9813a3494 | ||
|
|
9b42a93ce1 | ||
|
|
8502bceef1 | ||
|
|
663387476f | ||
|
|
daafd83df9 | ||
|
|
f780f2725b | ||
|
|
483aca871a | ||
|
|
352e709c3b | ||
|
|
629057b2c1 | ||
|
|
0e5f53596d | ||
|
|
0d91f07646 | ||
|
|
db882a26ab | ||
|
|
35c8ea22b1 | ||
|
|
7f01619358 | ||
|
|
ee109b4ceb | ||
|
|
7a398e5453 | ||
|
|
23a548f9b4 | ||
|
|
d4655d7034 | ||
|
|
9feb96b541 | ||
|
|
e939278193 | ||
|
|
d4ef1a2617 | ||
|
|
5f8746ced3 | ||
|
|
40a063e94f | ||
|
|
8f5439b958 | ||
|
|
e347f57d8b | ||
|
|
7169b15fd8 | ||
|
|
f9def8c96f | ||
|
|
ef43837af1 | ||
|
|
2efb8e8b8c | ||
|
|
e5bb69ea5f | ||
|
|
0979ca607d | ||
|
|
6ae186b2af | ||
|
|
98fb27f77d | ||
|
|
d68510bbaa | ||
|
|
71fdd2d92d | ||
|
|
3656689ff3 | ||
|
|
7d78406db6 | ||
|
|
ac47748e41 | ||
|
|
80f9b46479 | ||
|
|
999f1bf47a | ||
|
|
9e114eb2b8 | ||
|
|
4177d34b00 | ||
|
|
3ec5c04bf6 | ||
|
|
a877c068b6 | ||
|
|
6a3db90c1e | ||
|
|
a079e0d864 | ||
|
|
719776d66e | ||
|
|
c5af1241e9 | ||
|
|
27e4d7b563 | ||
|
|
450ab34721 | ||
|
|
3e2d4eae2c | ||
|
|
d89b6529ef | ||
|
|
5caf11556a | ||
|
|
78cc6f0f40 | ||
|
|
0007cd4668 | ||
|
|
05195e41de | ||
|
|
66f44ef87d | ||
|
|
a0585d9b11 | ||
|
|
5067946b13 | ||
|
|
f52241d5a8 | ||
|
|
04ccb25fa3 | ||
|
|
5a3f4b60b8 | ||
|
|
4408b2e488 | ||
|
|
9a26acee35 | ||
|
|
6ac377348b | ||
|
|
daeb88d4f4 | ||
|
|
47bf199f52 | ||
|
|
91540b022d | ||
|
|
505a51b6b5 | ||
|
|
28400488aa | ||
|
|
45ae600289 | ||
|
|
8be6874651 | ||
|
|
f694a500e0 | ||
|
|
c415fa01fc | ||
|
|
5225a9459c | ||
|
|
2974b150af | ||
|
|
cf353c8067 | ||
|
|
3aacb5d8b3 | ||
|
|
114fbdbe01 | ||
|
|
f1f83cbec4 | ||
|
|
335c28c2c9 | ||
|
|
daf12cbcce | ||
|
|
bf56eca003 | ||
|
|
c3542224ae | ||
|
|
a12b7fd58a | ||
|
|
98d004edbf | ||
|
|
67586a98b3 | ||
|
|
429faf44db | ||
|
|
c6d8911883 | ||
|
|
69ad7979ae | ||
|
|
12e398ce9b | ||
|
|
5c4a202616 | ||
|
|
adc1ec8444 | ||
|
|
0227d2fcb4 | ||
|
|
e87caac723 | ||
|
|
95abe3b5ac | ||
|
|
133a902c54 | ||
|
|
494544a4c2 | ||
|
|
aacb03d9ef | ||
|
|
fbeaa1781f | ||
|
|
ca73aad538 | ||
|
|
94b6118bf7 | ||
|
|
4a4bfefd17 | ||
|
|
7d6d86c6ff | ||
|
|
6796b0cd2a | ||
|
|
29e1f824b0 | ||
|
|
51263a2911 | ||
|
|
dd7f857475 | ||
|
|
bedc327e65 | ||
|
|
f0fdd4a537 | ||
|
|
9b847a0561 | ||
|
|
469e76b80a | ||
|
|
9c6994b476 | ||
|
|
52ba487617 | ||
|
|
21c57c9484 | ||
|
|
9b1ea6a07a | ||
|
|
f7d7bb0ea3 | ||
|
|
dd96b9ef53 | ||
|
|
b6b01893ba | ||
|
|
ad531d793d | ||
|
|
a9dd11e24a | ||
|
|
115983830b | ||
|
|
442cece081 | ||
|
|
14e08457b9 | ||
|
|
fabcc08cd5 | ||
|
|
7750843b04 | ||
|
|
40a893a8c9 | ||
|
|
37ebd30a4d | ||
|
|
c03188e976 | ||
|
|
d7da1ce333 | ||
|
|
dd9ee8c3f8 | ||
|
|
34bbd8f439 | ||
|
|
053f57cff5 | ||
|
|
365d2e2844 | ||
|
|
c0ac01a34a | ||
|
|
22c0bc0adb | ||
|
|
1b88f2ddf0 | ||
|
|
b4f2a1eb89 | ||
|
|
e9fc9cbc2a | ||
|
|
b809180a1b | ||
|
|
ecc75df3a1 | ||
|
|
d1b6863143 | ||
|
|
faf27143aa | ||
|
|
aee58a4475 | ||
|
|
c4cbf07d78 | ||
|
|
c5e07f643f | ||
|
|
818f5820d5 | ||
|
|
77c6e28876 | ||
|
|
fb7f66012d | ||
|
|
0f7f7bbe6c | ||
|
|
8dedb8deb4 | ||
|
|
dbb2c10bba | ||
|
|
67a5eef7d6 | ||
|
|
979f651251 | ||
|
|
3b93cbb009 | ||
|
|
30c63bfc4b | ||
|
|
bed40324a1 | ||
|
|
bc035de377 | ||
|
|
4db3cb6936 | ||
|
|
ed4b91f4bd | ||
|
|
24c7151276 | ||
|
|
804a9c07b8 | ||
|
|
528ea56821 | ||
|
|
7ba9c69ff8 | ||
|
|
0fc34da08a | ||
|
|
3fbf8cdbc8 | ||
|
|
e21f20d818 | ||
|
|
9fd9a60ca3 | ||
|
|
78b683d724 | ||
|
|
ad2f5036e1 | ||
|
|
4afbad8faa | ||
|
|
d284db4d3c | ||
|
|
82450c0ae8 | ||
|
|
8dd6c33901 | ||
|
|
f920d40db5 | ||
|
|
19be6c1acc | ||
|
|
7d9d8ad0e4 | ||
|
|
85f8237d5f | ||
|
|
c542894734 | ||
|
|
d348987077 | ||
|
|
3718610595 | ||
|
|
9c36ec0623 | ||
|
|
c6917b5d74 | ||
|
|
ae8b1c0c29 | ||
|
|
27978c459c | ||
|
|
1dc7f5c666 | ||
|
|
12ac870d3a | ||
|
|
dd1baa0224 | ||
|
|
4eaa179789 | ||
|
|
9008cd4549 | ||
|
|
bb27ef41cc | ||
|
|
2d35ac1df8 | ||
|
|
589ffc0c06 | ||
|
|
1f7f38c7d3 | ||
|
|
83817a2dc0 | ||
|
|
11af9da66f | ||
|
|
af3926acf3 | ||
|
|
ab40c2b3fd | ||
|
|
cc3204453e | ||
|
|
5ef8d3b7b9 | ||
|
|
2a9de97a19 | ||
|
|
9cecfed417 | ||
|
|
319715918a | ||
|
|
a3bf6e8b6d | ||
|
|
c4062658ce | ||
|
|
01a5de413e | ||
|
|
e4c22b1f29 | ||
|
|
fd05670dbc | ||
|
|
1ac094bfae | ||
|
|
b10e60126f | ||
|
|
ef1f27f421 | ||
|
|
0ed208675c | ||
|
|
00f7a0a194 | ||
|
|
935d4f4a64 | ||
|
|
dc4d88260d | ||
|
|
18934601da | ||
|
|
4ea811aeda | ||
|
|
f8fdecdc8f | ||
|
|
5467c61137 | ||
|
|
9c72b4cc56 | ||
|
|
9102217bc3 | ||
|
|
0e8fd7b2c4 | ||
|
|
fdf052cddb | ||
|
|
9a8d50ba6f | ||
|
|
136c97c312 | ||
|
|
bf00b88ef3 | ||
|
|
bafd1ea549 | ||
|
|
982618511b | ||
|
|
a4ad7ca3b1 | ||
|
|
99d71b57a4 | ||
|
|
1b2d8502e0 | ||
|
|
53e4ea9334 | ||
|
|
3ce704155c | ||
|
|
a06ea8fe55 | ||
|
|
31eb0fb48a | ||
|
|
d6a012e85f | ||
|
|
11c1cc7c72 | ||
|
|
6b7acb7bd5 | ||
|
|
bdebf19d7b | ||
|
|
cb1973ffb5 | ||
|
|
c6e2342ab4 | ||
|
|
2447599364 | ||
|
|
6a2cda74f1 | ||
|
|
8385d55d69 | ||
|
|
85e3464a15 | ||
|
|
6680039de7 | ||
|
|
9935826877 | ||
|
|
b977792168 | ||
|
|
2595cf87d8 | ||
|
|
f4342f1448 | ||
|
|
84c26b7c40 | ||
|
|
1cd7940a17 | ||
|
|
9514032f25 | ||
|
|
c7d6da2373 | ||
|
|
41b822fb6c | ||
|
|
69bf098c84 | ||
|
|
b4283ed4dc | ||
|
|
0fa0cac9e6 | ||
|
|
c71ef24052 | ||
|
|
cf0f665b14 | ||
|
|
2034e3ab78 | ||
|
|
89cfeec1b3 | ||
|
|
d8ae212df0 | ||
|
|
39b817bc69 | ||
|
|
09d79d52ae | ||
|
|
5c4b98d0e9 | ||
|
|
e5d6fc0604 | ||
|
|
5e656ebfba | ||
|
|
58fb75e55d | ||
|
|
e01621e658 | ||
|
|
b4aee17f53 | ||
|
|
dc70918648 | ||
|
|
69772edfa3 | ||
|
|
dd224cab05 | ||
|
|
b62873129e | ||
|
|
5052a1f564 | ||
|
|
2c36163e7a | ||
|
|
1bf912d6f0 | ||
|
|
d290259075 | ||
|
|
1689672faf | ||
|
|
8196e05679 | ||
|
|
fe0235da43 | ||
|
|
0895a73546 | ||
|
|
f06821e35b | ||
|
|
9cce5f645f | ||
|
|
c1a46408e9 | ||
|
|
21c3ccd637 | ||
|
|
3b9b034d80 | ||
|
|
348a5c3eb7 | ||
|
|
e3adaba3b3 | ||
|
|
4c5be658c3 | ||
|
|
b6517a449b | ||
|
|
ae0b8db0b0 | ||
|
|
56f0f8a299 | ||
|
|
c9af786b79 | ||
|
|
f34f615b80 | ||
|
|
aef2ef8479 | ||
|
|
7afbc9f5a4 | ||
|
|
df51b62041 | ||
|
|
045abc54fb | ||
|
|
9b2d9683eb | ||
|
|
3b0dd4a36c | ||
|
|
5e15f82313 | ||
|
|
d841c25bd3 | ||
|
|
8d3f1fe179 | ||
|
|
130ec130cc | ||
|
|
5e7a95eac0 | ||
|
|
a8cb49d12d | ||
|
|
c179ac626a | ||
|
|
041583bf96 | ||
|
|
ed710335b3 | ||
|
|
b556581a87 | ||
|
|
77a1b7918c | ||
|
|
45149e1b28 | ||
|
|
932338a25a | ||
|
|
925509e5a0 | ||
|
|
25646fbad7 | ||
|
|
e1733512c4 | ||
|
|
8379ffe1ce | ||
|
|
c77537ecee | ||
|
|
2192d97c69 | ||
|
|
a0dc76bda8 | ||
|
|
7fe177edc6 | ||
|
|
1f5e6f1e17 | ||
|
|
bf0aa295b0 | ||
|
|
649dffc3e0 | ||
|
|
a0f5ed66e2 | ||
|
|
7df3b95c22 | ||
|
|
0756474d40 | ||
|
|
60747db945 | ||
|
|
afcfad162e | ||
|
|
63f15bdc9e | ||
|
|
3b826869e9 | ||
|
|
af0256add0 | ||
|
|
b8d8cba12c | ||
|
|
616e9a0ec2 | ||
|
|
366434cbd7 | ||
|
|
f6d4046af6 | ||
|
|
82932f002e | ||
|
|
7593a05953 | ||
|
|
3026a9e3e4 | ||
|
|
362939eab9 | ||
|
|
61d52731a5 | ||
|
|
6aecc6521c | ||
|
|
ef5829593e | ||
|
|
4a8f67093f | ||
|
|
9cbe0664f6 | ||
|
|
965d6e4e8e | ||
|
|
eefdeb0bb7 | ||
|
|
a904a51293 | ||
|
|
cce377d70d | ||
|
|
5721bca5a3 | ||
|
|
bcd5b024f0 | ||
|
|
571f257c17 | ||
|
|
3451135800 | ||
|
|
f426a78a94 | ||
|
|
3d65236e63 | ||
|
|
7b51b5005a | ||
|
|
3d9cf16960 | ||
|
|
35def53666 | ||
|
|
5c46a89ddc | ||
|
|
4e429025bf | ||
|
|
95fae11eee | ||
|
|
9a22a9fb8b | ||
|
|
f60e2e2ca6 | ||
|
|
deb9101335 | ||
|
|
407f93ac43 | ||
|
|
78c39edceb | ||
|
|
c8445fb711 | ||
|
|
7c0e7347c8 | ||
|
|
12f37d0931 | ||
|
|
9a5086d9ba | ||
|
|
3222c7e677 | ||
|
|
632e0648d4 | ||
|
|
e3198031e3 | ||
|
|
0ced9c8e26 | ||
|
|
65f4a708cd | ||
|
|
36e7b00d9a | ||
|
|
8b2c48f5ca | ||
|
|
9f7a0d4f17 | ||
|
|
fa5ae17621 | ||
|
|
7a2536c559 | ||
|
|
96d2edb641 | ||
|
|
8a2bd23c32 | ||
|
|
d3b935ea7f | ||
|
|
e53bc3b048 | ||
|
|
5f1cfc9dda | ||
|
|
43207b316f | ||
|
|
96ed4c419a | ||
|
|
840a2253e2 | ||
|
|
18db9b0a77 | ||
|
|
6c7a5292a4 | ||
|
|
bef1c74226 | ||
|
|
176ec8bace | ||
|
|
c62064002f | ||
|
|
45b7800a68 | ||
|
|
fa761ac69b | ||
|
|
cc11e98aa6 | ||
|
|
8f1c71137a | ||
|
|
8fdf2dcb7a | ||
|
|
d4cd5b73bd | ||
|
|
77975aed2a | ||
|
|
a7700ce27e | ||
|
|
726ff1a126 | ||
|
|
e24269c452 | ||
|
|
686f4656ec | ||
|
|
55fe10d2dc | ||
|
|
422984ac41 | ||
|
|
706d117d80 | ||
|
|
13f8df4e0d | ||
|
|
263d433193 | ||
|
|
c15c11f3b1 | ||
|
|
524c8ccfc5 | ||
|
|
902392ea30 | ||
|
|
bef179187f | ||
|
|
ea7221c39a | ||
|
|
edaf9f6296 | ||
|
|
0d83725b77 | ||
|
|
6ce31305c6 | ||
|
|
90935c033d | ||
|
|
b4c3f831a7 | ||
|
|
f0e25e8198 | ||
|
|
d800082621 | ||
|
|
653d3da718 | ||
|
|
0f39409386 | ||
|
|
ccae0d1a57 | ||
|
|
257992d314 | ||
|
|
5eb843b63d | ||
|
|
3929b478a7 | ||
|
|
18734ed822 | ||
|
|
876e749b31 | ||
|
|
32b8c505d9 | ||
|
|
37a0dce7c5 | ||
|
|
2332f36b56 | ||
|
|
21cc9cc026 | ||
|
|
db4d76502e | ||
|
|
1578ea7590 | ||
|
|
7405de01fe | ||
|
|
77dc5943e5 | ||
|
|
f8a2748ede | ||
|
|
a99ca00bb3 | ||
|
|
6eb80eea2f | ||
|
|
82828f7f82 | ||
|
|
23c9a5963a | ||
|
|
7595f113ec | ||
|
|
e9e5a4ee0d | ||
|
|
1947fc3e83 | ||
|
|
8844689482 | ||
|
|
0d2ba54c10 | ||
|
|
a4bb5137ea | ||
|
|
b8aea1f97a | ||
|
|
120e1893bd | ||
|
|
836df52a50 | ||
|
|
00498aaeac | ||
|
|
f4f47cff75 | ||
|
|
40dc3d45fc | ||
|
|
b89d2a6da1 | ||
|
|
55a4af9f00 | ||
|
|
719d45e75e | ||
|
|
8703684740 | ||
|
|
e95e7218f6 | ||
|
|
e9d4711978 | ||
|
|
9309506e97 | ||
|
|
00d2a80e95 | ||
|
|
c1e62b7d90 | ||
|
|
84775d36dc | ||
|
|
fc4eb11fd8 | ||
|
|
ce70ce6c76 | ||
|
|
ffd404ec1b | ||
|
|
6f943db012 | ||
|
|
12342ac426 | ||
|
|
489ddc3f56 | ||
|
|
02a266cbea | ||
|
|
bdc9facd41 | ||
|
|
7d679aac0b | ||
|
|
ae1719e795 | ||
|
|
6cea05e9f4 | ||
|
|
cda84d4e64 | ||
|
|
b3f63a85d5 | ||
|
|
196903cb12 | ||
|
|
e6f082adfd | ||
|
|
bb2f641073 | ||
|
|
de6312d317 | ||
|
|
887b0f3119 | ||
|
|
f13c8d7884 | ||
|
|
a10fdb260d | ||
|
|
0f8b1790f3 | ||
|
|
531e345dd9 | ||
|
|
c1942759d4 | ||
|
|
eca9e573de | ||
|
|
61376f8d68 | ||
|
|
292e2c60e2 | ||
|
|
2ffaf81109 | ||
|
|
da37381678 | ||
|
|
3236bf6122 | ||
|
|
a352ae6922 | ||
|
|
f3631a6a09 | ||
|
|
0d376bd5b7 | ||
|
|
fd57bd0378 | ||
|
|
5a11882d1b | ||
|
|
0f041da548 | ||
|
|
00a23119c5 | ||
|
|
cdeb49473b | ||
|
|
b55b3731a5 | ||
|
|
4afbb688ba | ||
|
|
f289a921f1 | ||
|
|
f2165bc4c1 | ||
|
|
73b20bfe4a | ||
|
|
fbf2006e3f | ||
|
|
358b701396 | ||
|
|
d3caae3a2d | ||
|
|
300062d3ac | ||
|
|
bb0aaad383 | ||
|
|
e9aa8609f1 | ||
|
|
37cbb18626 | ||
|
|
3dad6daa2e | ||
|
|
f49485e161 | ||
|
|
f3f268742f | ||
|
|
99e76f2254 | ||
|
|
a7801e376b | ||
|
|
90000aa1fb | ||
|
|
a2a26cd058 | ||
|
|
e3cbefc5b6 | ||
|
|
c6ceb25ee8 | ||
|
|
022777888c | ||
|
|
ad5e644362 | ||
|
|
bf6cb04fe8 | ||
|
|
dbde23fb7b | ||
|
|
c9cb469d65 | ||
|
|
2e37c20e55 | ||
|
|
ade665d228 | ||
|
|
a62f0cfd3b | ||
|
|
8b867f78fe | ||
|
|
d6a43fd8e5 | ||
|
|
9887b8142d | ||
|
|
4d92d6dc2b | ||
|
|
d8cd84ed9e | ||
|
|
6bc740e881 | ||
|
|
db348cc368 | ||
|
|
6ebf59d7ff | ||
|
|
d35e31d128 | ||
|
|
b9b6d3d2cb | ||
|
|
728b111ac9 | ||
|
|
5afbfbfd43 | ||
|
|
9e5ce589ae | ||
|
|
f486150a0f | ||
|
|
075ee815f0 | ||
|
|
d321283b13 | ||
|
|
0ec72bb013 | ||
|
|
cdfbcd873c | ||
|
|
8be382fa7e | ||
|
|
82f5ab1446 | ||
|
|
c97f8c31ce | ||
|
|
92e5b5e9c3 | ||
|
|
a70fca493d | ||
|
|
42c8f0c345 | ||
|
|
d3d5a1745d | ||
|
|
8392d8b684 | ||
|
|
1abd13c6e0 | ||
|
|
e126b66e19 | ||
|
|
47449d93db | ||
|
|
13002f96f1 | ||
|
|
1985a035be | ||
|
|
cdfc7e4158 | ||
|
|
5050052710 | ||
|
|
48e39d2ffa | ||
|
|
a8d053e82a | ||
|
|
2154945c3c | ||
|
|
8d83a0a86a | ||
|
|
c196cdf405 | ||
|
|
b6a5f43176 | ||
|
|
008ded4a5c | ||
|
|
d476574d05 | ||
|
|
371b3813d4 | ||
|
|
e4c3c224d9 | ||
|
|
69bc697568 | ||
|
|
fe08d034bb | ||
|
|
18f4714410 | ||
|
|
1b6c416893 | ||
|
|
6153a28b4b | ||
|
|
9574cf16fb | ||
|
|
d309a67416 | ||
|
|
fb865af088 | ||
|
|
c1e7039357 | ||
|
|
0fd3b37641 | ||
|
|
cea91f7b2f | ||
|
|
3959896832 | ||
|
|
d55dccdeb1 | ||
|
|
c46c286b51 | ||
|
|
36b37b3b8f | ||
|
|
ce9931d8a3 | ||
|
|
66f5ff35d3 | ||
|
|
aa15d261f3 | ||
|
|
00a32463c7 | ||
|
|
dd60ff8b74 | ||
|
|
4588611cbf | ||
|
|
80c0152d46 | ||
|
|
fac727cd3d | ||
|
|
9c30183068 | ||
|
|
9c28d5c5c5 | ||
|
|
3c55e3a3f0 | ||
|
|
ba6e3b801d | ||
|
|
4064ab47ac | ||
|
|
b6005fcb56 | ||
|
|
71de526366 | ||
|
|
a3e5d8448b | ||
|
|
f65b6e5484 | ||
|
|
05d0d9e501 | ||
|
|
d107beadf2 | ||
|
|
9c2fd26579 | ||
|
|
d796ea6324 | ||
|
|
8386c9e729 | ||
|
|
00ca4524b5 | ||
|
|
f4d4853319 | ||
|
|
00678cc9ca | ||
|
|
2845486a3f | ||
|
|
a815e6447d | ||
|
|
5b71a30ee9 | ||
|
|
ac2d94420b | ||
|
|
1460c1364a | ||
|
|
37f38fe988 | ||
|
|
cf025b9135 | ||
|
|
283ff7a280 | ||
|
|
e668f016b4 | ||
|
|
256c2c955a | ||
|
|
d560c3e8de | ||
|
|
4f8e8e6669 | ||
|
|
7cdc2e0915 | ||
|
|
74b236b317 | ||
|
|
e344cafd9b | ||
|
|
4b0d16cad1 | ||
|
|
da4d8629bd | ||
|
|
68ae3b79ab | ||
|
|
78e0336c1b | ||
|
|
89ffeaf03b | ||
|
|
e439f4d643 | ||
|
|
8b779a0fca | ||
|
|
4b71dc8445 | ||
|
|
780875d5f2 | ||
|
|
7356d4b0e2 | ||
|
|
654dea6b7e | ||
|
|
1204374637 | ||
|
|
880fde2148 | ||
|
|
d776d76100 | ||
|
|
39d95105e1 | ||
|
|
2a7af826a8 | ||
|
|
9d2fd53073 | ||
|
|
4c99923467 | ||
|
|
edc5985a7e | ||
|
|
96fc79103b | ||
|
|
76879f3a73 | ||
|
|
38dac3803b | ||
|
|
c0d4ad2042 | ||
|
|
c669d5657a | ||
|
|
98c44fa578 | ||
|
|
328629fe88 | ||
|
|
9754535055 | ||
|
|
26f701d890 | ||
|
|
f3df8024e6 | ||
|
|
bc9fdfc7a4 | ||
|
|
e1e37989e4 | ||
|
|
dc67cb1807 | ||
|
|
2e8a2457bc | ||
|
|
6d4398c6fd | ||
|
|
9f67ad872d | ||
|
|
aba396f274 | ||
|
|
4315f34398 | ||
|
|
47b456e1ee | ||
|
|
529810b2dc | ||
|
|
6f67b8e788 | ||
|
|
a9c9d12444 | ||
|
|
f8428cec61 | ||
|
|
ac46bce807 | ||
|
|
23e899042d | ||
|
|
80c4a3c06d | ||
|
|
d2a31601ba | ||
|
|
7ddb4f3486 | ||
|
|
e56da87e0e | ||
|
|
2e4ebecf67 | ||
|
|
1b4ccaed91 | ||
|
|
1e2d41c7fb | ||
|
|
1b2ead054a | ||
|
|
468c1b95b7 | ||
|
|
60d8eff71f | ||
|
|
8baae8b801 | ||
|
|
839375fcf1 | ||
|
|
9f16f26347 | ||
|
|
69b4cacab4 | ||
|
|
b74b5040b1 | ||
|
|
a28decc854 | ||
|
|
ed82c36628 | ||
|
|
3536629dd9 | ||
|
|
c87696696e | ||
|
|
cb59cef1b8 | ||
|
|
b5ba03df4d | ||
|
|
d9b600466c | ||
|
|
0d37a59a5c | ||
|
|
4edf2f8cd1 | ||
|
|
c60dfdf0d7 | ||
|
|
006afc6841 | ||
|
|
f70879581d | ||
|
|
7a2d2b0376 | ||
|
|
b5368fa239 | ||
|
|
db1d71af9f | ||
|
|
6b03ef35a6 | ||
|
|
2afd02d86f | ||
|
|
b32c00f455 | ||
|
|
fbe9fb41ed | ||
|
|
cc0a7f7d76 | ||
|
|
db33fc60b9 | ||
|
|
6feaee4f86 | ||
|
|
1a27a31a32 | ||
|
|
5b611e71d5 | ||
|
|
6de88bfe11 | ||
|
|
6d7236249f | ||
|
|
69fbaba8a6 | ||
|
|
6d88737505 | ||
|
|
9869cfc736 | ||
|
|
8505326a68 | ||
|
|
3a4af88384 | ||
|
|
5b2e7d0f70 | ||
|
|
ddeea6bee3 | ||
|
|
0cfe3a7634 | ||
|
|
727463e4d1 | ||
|
|
d42abfdc56 | ||
|
|
e01ea1df4c | ||
|
|
078bfac5f5 | ||
|
|
111b07b9e6 | ||
|
|
dfbc89addc | ||
|
|
bf44da9a14 | ||
|
|
d75d13965b | ||
|
|
8aedebdc94 | ||
|
|
9388c4bb0d | ||
|
|
77d4f601af | ||
|
|
7fae590848 | ||
|
|
bc41558a26 | ||
|
|
f6651face4 | ||
|
|
345f00f7f2 | ||
|
|
e876d02118 | ||
|
|
5b7018f71b | ||
|
|
f45b3fc50a | ||
|
|
01196be30d | ||
|
|
0a2999bffb | ||
|
|
8f097096e7 | ||
|
|
cd97fc046a | ||
|
|
eeb10f31a6 | ||
|
|
9df5e116e8 | ||
|
|
1228a03d39 | ||
|
|
a5e1b3096e | ||
|
|
b41ae67128 | ||
|
|
ddfbe20125 | ||
|
|
0bfe9291dd | ||
|
|
622b2e1edc | ||
|
|
4a40719534 | ||
|
|
384993d363 | ||
|
|
01b7d28154 | ||
|
|
d7c4f5577f | ||
|
|
a69d23ca64 | ||
|
|
e2f8b7a6e3 | ||
|
|
171a0b012f | ||
|
|
5c04b15433 | ||
|
|
6397feffff | ||
|
|
e73b9b7f1c | ||
|
|
2d528db054 | ||
|
|
80c4ba6723 | ||
|
|
0d82e40c67 | ||
|
|
b75d6d02fa | ||
|
|
76d4542716 | ||
|
|
87955de849 | ||
|
|
6df60cf5da | ||
|
|
3c23a314f0 | ||
|
|
8fda6b04a4 | ||
|
|
9fa98e6b76 | ||
|
|
deb685f39b | ||
|
|
d7851d3a18 | ||
|
|
44946fc54a | ||
|
|
a033d10adc | ||
|
|
5a3e599fe0 | ||
|
|
a9f645f389 | ||
|
|
d662f0903a | ||
|
|
beaa947eb7 | ||
|
|
48006b64d6 | ||
|
|
8f195ba66f | ||
|
|
123288e745 | ||
|
|
5866e95d49 | ||
|
|
e79f395424 | ||
|
|
999ca87fec | ||
|
|
1217266d88 | ||
|
|
bb262198be | ||
|
|
11aae77caf | ||
|
|
8212cede6e | ||
|
|
a3c51884f4 | ||
|
|
b8890aca7f | ||
|
|
014b0cce14 | ||
|
|
6d860c5cb7 | ||
|
|
d8be832858 | ||
|
|
afcb9fcf41 | ||
|
|
3c7ae0aaf0 | ||
|
|
6b7f93dbfe | ||
|
|
c40b255022 | ||
|
|
1742d265f3 | ||
|
|
3240e0bcae | ||
|
|
ff185f6505 | ||
|
|
346b517c9d | ||
|
|
80f00aba0a | ||
|
|
949905f6e2 | ||
|
|
b9e26fecfd | ||
|
|
232682f4a8 | ||
|
|
de3b690d60 | ||
|
|
de69a78a98 | ||
|
|
1c341c34a3 | ||
|
|
33beb57e9d | ||
|
|
66eeadca0b | ||
|
|
a10d1c98a8 | ||
|
|
59ead4986f | ||
|
|
09f6c18189 | ||
|
|
a5cd6d5ac0 | ||
|
|
0f3ad7c8b1 | ||
|
|
0487dea7fc | ||
|
|
a6803bf0e3 | ||
|
|
8cac1ee284 | ||
|
|
196620e1bd | ||
|
|
43d6c76873 | ||
|
|
b864c39a0d | ||
|
|
818b975111 | ||
|
|
d5fbc8393f | ||
|
|
df9a71a63d | ||
|
|
7b5e9d2344 | ||
|
|
7fc2d95886 | ||
|
|
2ba8702787 | ||
|
|
78d3b369bb | ||
|
|
bb3620680b | ||
|
|
d4a45655ca |
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']
|
||||
41
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,41 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
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 '....'
|
||||
3. Scroll down to '....'
|
||||
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]
|
||||
- Size: [e.g. 150Mo]
|
||||
- Contains attachment: [e.g. Yes]
|
||||
|
||||
**KeePassDX (please complete the following information):**
|
||||
- Version: [e.g. 2.5.0.0beta23]
|
||||
- Build: [e.g. Free]
|
||||
- Language: [e.g. French]
|
||||
|
||||
**Android (please complete the following information):**
|
||||
- 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]
|
||||
62
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: Bug report
|
||||
description: Report a bug.
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please check out the [Wiki](https://github.com/Kunzisoft/KeePassDX/wiki) and [existing issues](https://github.com/Kunzisoft/KeePassDX/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug) to see if your problem has already been reported.
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: Checks
|
||||
options:
|
||||
- label: I have read the Wiki, searched the open issues, and still think this is a new bug.
|
||||
required: true
|
||||
- type: textarea
|
||||
id: bug
|
||||
attributes:
|
||||
label: "Explain the problem clearly and succinctly:"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: "Describe what you expected to happen:"
|
||||
- type: input
|
||||
id: app-version
|
||||
attributes:
|
||||
label: "KeePassDX version:"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: app-build
|
||||
attributes:
|
||||
label: "Build:"
|
||||
multiple: true
|
||||
options:
|
||||
- Free
|
||||
- Libre
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: database-version
|
||||
attributes:
|
||||
label: "Database version:"
|
||||
- type: input
|
||||
id: file-provider
|
||||
attributes:
|
||||
label: "File provider (`content://` URI)"
|
||||
- type: input
|
||||
id: android-version
|
||||
attributes:
|
||||
label: "Android version:"
|
||||
- type: input
|
||||
id: android-device
|
||||
attributes:
|
||||
label: "Android device:"
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: "Additional context:"
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: feature
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
33
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Feature request
|
||||
description: Suggest an idea.
|
||||
labels: ["feature"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please check out the [Wiki](https://github.com/Kunzisoft/KeePassDX/wiki) and [existing issues](https://github.com/Kunzisoft/KeePassDX/issues?q=is%3Aissue%20state%3Aopen%20label%3Afeature) to see if your feature has already been reported.
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: Checks
|
||||
options:
|
||||
- label: I have read the Wiki, searched the open issues, and still think this is a new feature.
|
||||
required: true
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: "Explain the problem clearly and succinctly:"
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: "Describe the solution you'd like:"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: "Describe alternatives you've considered:"
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: "Additional context:"
|
||||
6
.gitignore
vendored
@@ -19,6 +19,9 @@ bin/
|
||||
gen/
|
||||
out/
|
||||
|
||||
# Kotlin folder
|
||||
.kotlin/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
@@ -80,6 +83,9 @@ art/screen*.png
|
||||
art/logo_512.png
|
||||
art/store_screens/
|
||||
|
||||
# Release
|
||||
releases/*
|
||||
|
||||
# Dir linux
|
||||
.directory
|
||||
*/.directory
|
||||
|
||||
328
CHANGELOG
@@ -1,3 +1,331 @@
|
||||
KeePassDX(4.2.4)
|
||||
* Fix remembering database location #2262
|
||||
|
||||
KeePassDX(4.2.3)
|
||||
* Fix multiple Passkey selection #2253
|
||||
* Fix database dialog subtitle #2254
|
||||
* Fix save search info if URL present #2255
|
||||
* Small fixes
|
||||
|
||||
KeePassDX(4.2.2)
|
||||
* Fix database merge algorithm #2223
|
||||
* Fix save search info #2243
|
||||
* Fix Play Service as privileged app for Passkey Cross Device Authentication #2244
|
||||
* Small fixes
|
||||
|
||||
KeePassDX(4.2.1)
|
||||
* Fix Magikeyboard autosearch #2233
|
||||
* Fix database merge #2223
|
||||
* Fix dialog database action #2234
|
||||
* Fix autofill selection #2238 #2235
|
||||
* Small fixes
|
||||
|
||||
KeePassDX(4.2.0)
|
||||
* Passkeys management #1421 #2097 (@cali-95)
|
||||
* Confirm usage of passkey #2165 #2124
|
||||
* Dialog to manage missing signature #2152 #2155 #2161 #2160
|
||||
* Capture error #2159 #2215
|
||||
* Change Passkey Backup Eligibility & Backup State #2135 #2150 #2212
|
||||
* Search settings #2112 #2181 #2187 #2204
|
||||
* Autofill refactoring #765 #2196
|
||||
* Small fixes #2157 #2164 #2171 #2122 #2180 #2209 #2214
|
||||
|
||||
KeePassDX(4.1.9)
|
||||
* Fix landscape UI #2198 #2200 (@chenxiaolong)
|
||||
* Fix start loop and flash screen #2201
|
||||
* Small fixes
|
||||
|
||||
KeePassDX(4.1.8)
|
||||
* Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP)
|
||||
* Remember last read-only state #2099 #2100 (Thx @rmacklin)
|
||||
* Fix merge deletion #1516
|
||||
* Fix space in search #175
|
||||
* Fix deletable recycle bin #2163
|
||||
* Small fixes
|
||||
|
||||
KeePassDX(4.1.7)
|
||||
* Fix CipherDatabase for biometric states #2119
|
||||
|
||||
KeePassDX(4.1.6)
|
||||
* Auto open biometric prompt from database list #2113
|
||||
* Fix Keystore errors #2114 #2115
|
||||
* Complete biometric refactoring for better compatibility
|
||||
|
||||
KeePassDX(4.1.5)
|
||||
* Fix auto prompt #2111
|
||||
|
||||
KeePassDX(4.1.4)
|
||||
* Fix UnlockManager #2098 #2101
|
||||
* Auto device unlock prompt #2105
|
||||
* Small fixes ##2066
|
||||
|
||||
KeePassDX(4.1.3)
|
||||
* Fix Autofill Registration #2089
|
||||
* Fix Biometric errors #2081
|
||||
* Fixed timestamp in copy file #1981 #1983
|
||||
* Fix Template Email #1986
|
||||
* Fix Search #2096
|
||||
|
||||
KeePassDX(4.1.2)
|
||||
* Fix URL search #1940 #1946 #2003 #2040 #2044
|
||||
* Fix Autofill popup #2054
|
||||
* Fix Group notes #2053
|
||||
* Fix Dialog background #2005 #2004 (Thx @codokie)
|
||||
* Fix OTP configuration #2042 #2065 (Thx @Dev-ClayP)
|
||||
* Fix small UI elements #1987 #2007 (Thx @ymcx)
|
||||
* RTL layout support #2021 (Thx @codokie)
|
||||
* App Metadata to translation #1823
|
||||
|
||||
KeePassDX(4.1.1)
|
||||
* Fix date parser #1933
|
||||
* Fix domain search #1820 #1936
|
||||
|
||||
KeePassDX(4.1.0)
|
||||
* Generate keyfile #1290
|
||||
* Hide template group #1894
|
||||
* Group count sum recursively #421
|
||||
* Fix date fields #1695 #1710
|
||||
* Fix distinct domain names #1105 #1820
|
||||
* Resets the advanced unlock expiration #1600
|
||||
* Password entropy #1490 #1355
|
||||
* Upgrade to API 34 (Android 14) #1730
|
||||
* Small fixes #1711 #1831 #1780 #1821 #1863 #1889 #1289 #1600 #1467 #1870
|
||||
|
||||
KeePassDX(4.0.8)
|
||||
* Fix graphical bug that prevented databases from being opened on some versions of Android #1848 #1850
|
||||
|
||||
KeePassDX(4.0.7)
|
||||
* Prevent 0 Byte file with cache during a save exception #1620 #1594 #1680
|
||||
* Fix inline suggestions in keyboard #1840
|
||||
* Fix broken links by default #1755
|
||||
* Fix UX by allowing validation in entry edition #1770
|
||||
* Fix small bugs #1709
|
||||
|
||||
KeePassDX(4.0.6)
|
||||
* Fix form filled recognition #1508 #1735 #1508 #1790 #1783 #1797 #1801 #1802 #1804 #1665
|
||||
* Fix translations #1707 #1683 #1712
|
||||
* Update APK verifier #1810
|
||||
|
||||
KeePassDX(4.0.5)
|
||||
* Fix form filled recognition #1572 #1508
|
||||
* Rollback password color #1686 #1490
|
||||
|
||||
KeePassDX(4.0.4)
|
||||
* Fix form filled recognition #1572 #1677
|
||||
* Fix device unlock #1682
|
||||
* Fix password color #1490
|
||||
|
||||
KeePassDX(4.0.3)
|
||||
* Fix "Save as" in Read Only mode #1666
|
||||
* Fix username autofill #1665 #530 #1572 #1426 #1523 #1556 #1653 #1658 #1508 #1667
|
||||
* Fix regex OTP recognition #1596
|
||||
* Change password color dynamically #1490
|
||||
* Small fixes #1641 #1656 #1649 #1400 #1674
|
||||
|
||||
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
|
||||
|
||||
KeePassDX(3.3.1)
|
||||
* Fix Japanese keyboard in search #1248
|
||||
* Better OOM management #256
|
||||
* Fix filters #1249
|
||||
* Fix temp advanced unlocking #1245
|
||||
* Best autofill recognition #1250
|
||||
* Workaround to fill OTP token in multiple fields with Magikeyboard (long press) #1158
|
||||
|
||||
KeePassDX(3.3.0)
|
||||
* Quick search and dynamic filters #163 #462 #521
|
||||
* Keep search context #1141
|
||||
* Add searchable groups #905 #1006
|
||||
* Search with regular expression #175
|
||||
* Merge from file and save as copy #1221 #1204 #840
|
||||
* Fix custom data #1236
|
||||
* Fix education hints #1192
|
||||
* Fix save and app instance in selection mode
|
||||
* New UI and fix styles
|
||||
* Add "Simple" and "Reply" themes
|
||||
|
||||
KeePassDX(3.2.0)
|
||||
* Manage data merge #840 #977
|
||||
* Manage Tags #633
|
||||
* Inherit colors and icon from template #1213 #1130
|
||||
* Entry colors setting #1207
|
||||
* Setting to keep the screen on when watching the entry #1119
|
||||
* Add path in quick search
|
||||
* Small fixes
|
||||
|
||||
KeePassDX(3.1.0)
|
||||
* Add breadcrumb
|
||||
* Add path in search results #1148
|
||||
* Add group info dialog #1177
|
||||
* Manage colors #64 #913
|
||||
* Fix UI in Android 8 #509
|
||||
* Upgrade libs and SDK to 31 #833
|
||||
* Fix parser of database v1 #1201
|
||||
* Stop asking WRITE_EXTERNAL_STORAGE permission
|
||||
|
||||
KeePassDX(3.0.4)
|
||||
* Fix autofill inline bugs #1173 #1165
|
||||
* Small UI change
|
||||
|
||||
KeePassDX(3.0.3)
|
||||
* Change default Argon2 parameters #1098
|
||||
* Add & edit custom icon name #976
|
||||
* Fix templates #1128 #1133 #1138
|
||||
* Update Autofill compatibility list #725 #1154
|
||||
* Improve fingerprint usage #1137 #1145
|
||||
* Change backup configuration #1144
|
||||
* Add lock button in database notification
|
||||
|
||||
KeePassDX(3.0.2)
|
||||
* Samsung DeX mode #1114 #245 (Thx @chenxiaolong)
|
||||
|
||||
KeePassDX(3.0.1)
|
||||
* Fix text size and smallest margin #1085
|
||||
* Fix number of lines during an edition #1073
|
||||
* Fix Magikeyboard URL auto action #1100
|
||||
* Fix exception after group name change and save #1112
|
||||
* Fix timeout reset #1107
|
||||
* Fix search actions #1091 #1092
|
||||
* Small changes #1106 #1085
|
||||
|
||||
KeePassDX(3.0.0)
|
||||
* Add / Manage dynamic templates #191
|
||||
* Manually select RecycleBin group and Templates group #191
|
||||
* Setting to display OTP Token in list #655
|
||||
* Fix timeout in dialogs #716
|
||||
* Check URI permissions #626
|
||||
* Better autofill implementation #943 #946 #984 #1070 (Thx @uduerholz)
|
||||
* Improvements #680 #1035 #1043 #942 #1021 #1027 #1046 #1082 #1083 (Thx @chenxiaolong)
|
||||
|
||||
KeePassDX(2.10.5)
|
||||
* Increase the saving speed of database #1028
|
||||
* Fix advanced unlocking by device credential #1029
|
||||
|
||||
KeePassDX(2.10.4)
|
||||
* Hot fix to increase the opening speed of database #1028
|
||||
|
||||
KeePassDX(2.10.3)
|
||||
* Improve Magikeyboard options description #1022 #1023 (Thx @djibux)
|
||||
* Fix database opened without notification (database is now closed when screen is killed in background #1025)
|
||||
* Fix biometric prompt #1018
|
||||
|
||||
KeePassDX(2.10.2)
|
||||
* Fix search fields references #987
|
||||
* Fix Auto-Types with same key #997
|
||||
|
||||
KeePassDX(2.10.1)
|
||||
* Fix parcelable with custom data #986
|
||||
|
||||
KeePassDX(2.10.0)
|
||||
* Manage new database format 4.1 #956
|
||||
* Fix show button consistency #980
|
||||
* Fix persistent notification #979
|
||||
|
||||
KeePassDX(2.9.20)
|
||||
* Fix search with non-latin chars #971
|
||||
* Fix action mode with search #972 (rollback ignore accents #945)
|
||||
* Fix timeout with 0s #974
|
||||
|
||||
KeePassDX(2.9.19)
|
||||
* Fix search slowdown #964
|
||||
* Fix closing notification after lock request #965
|
||||
* Better temp advanced unlocking code implementation #965
|
||||
* Fix OTP token generation #967
|
||||
|
||||
KeePassDX(2.9.18)
|
||||
* Move groups #658
|
||||
* Improve autofill recognition #960
|
||||
* Remove diacritical marks in search string #945
|
||||
* Fix search in references #962
|
||||
* Fix themes in Libre version
|
||||
|
||||
KeePassDX(2.9.17)
|
||||
* Import / Export app properties #839
|
||||
* Force twofish padding compatibility #955
|
||||
* Better timeout preference #579
|
||||
|
||||
KeePassDX(2.9.16)
|
||||
* Fix small bugs #948
|
||||
|
||||
KeePassDX(2.9.15)
|
||||
* Fix themes #935 #926
|
||||
* Decrease default clipboard time #934
|
||||
|
||||
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)
|
||||
230
Gemfile.lock
Normal file
@@ -0,0 +1,230 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.7)
|
||||
base64
|
||||
nkf
|
||||
rexml
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1146.0)
|
||||
aws-sdk-core (3.229.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.110.0)
|
||||
aws-sdk-core (~> 3, >= 3.228.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.196.1)
|
||||
aws-sdk-core (~> 3, >= 3.228.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.3.0)
|
||||
bigdecimal (3.2.2)
|
||||
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.7.0)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.6.20240107)
|
||||
dotenv (2.8.1)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.112.0)
|
||||
faraday (1.10.4)
|
||||
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.1)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.1.1)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
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.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.4.0)
|
||||
fastlane (2.228.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 (~> 1.2)
|
||||
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)
|
||||
fastlane-sirp (>= 1.0.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-apis-androidpublisher_v3 (~> 0.3)
|
||||
google-apis-playcustomapp_v1 (~> 0.1)
|
||||
google-cloud-env (>= 1.6.0, < 2.0.0)
|
||||
google-cloud-storage (~> 1.31)
|
||||
highline (~> 2.0)
|
||||
http-cookie (~> 1.0.5)
|
||||
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, < 1.0.0)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
rubyzip (>= 2.0.0, < 3.0.0)
|
||||
security (= 0.1.5)
|
||||
simctl (~> 1.6.3)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (~> 3)
|
||||
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.4.1)
|
||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||
fastlane-plugin-versioning_android (0.1.1)
|
||||
fastlane-sirp (1.0.0)
|
||||
sysrandom (~> 1.0)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.54.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-core (0.11.3)
|
||||
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
|
||||
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.31.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-cloud-core (1.8.0)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.5.0)
|
||||
google-cloud-storage (1.47.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.31.0)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.8.1)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.8)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
jmespath (1.6.2)
|
||||
json (2.13.2)
|
||||
jwt (2.10.2)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.17.0)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
nanaimo (0.4.0)
|
||||
naturally (2.3.0)
|
||||
nkf (0.2.0)
|
||||
optparse (0.6.0)
|
||||
os (1.1.4)
|
||||
plist (3.7.2)
|
||||
public_suffix (6.0.2)
|
||||
rake (13.3.0)
|
||||
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.4.1)
|
||||
rouge (3.28.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
security (0.1.5)
|
||||
signet (0.20.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
|
||||
sysrandom (1.0.5)
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
trailblazer-option (0.1.2)
|
||||
tty-cursor (0.7.1)
|
||||
tty-screen (0.8.2)
|
||||
tty-spinner (0.9.3)
|
||||
tty-cursor (~> 0.7)
|
||||
uber (0.1.0)
|
||||
unicode-display_width (2.6.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.27.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.4.0)
|
||||
rexml (>= 3.3.6, < 4.0)
|
||||
xcpretty (0.4.1)
|
||||
rouge (~> 3.28.0)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
fastlane
|
||||
fastlane-plugin-versioning_android
|
||||
|
||||
BUNDLED WITH
|
||||
2.6.9
|
||||
67
README.md
@@ -1,23 +1,26 @@
|
||||
# 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 alt="KeePassDX Icon" src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> **Lightweight password safe and manager for Android**, KeePassDX allows editing encrypted data in a single file in KeePass format and fill in the forms in a secure way.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
|
||||
<img alt="KeePassDX Screenshot" src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
|
||||
|
||||
### Features
|
||||
|
||||
- Create database files / entries and groups.
|
||||
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
|
||||
- **Compatible** with the majority of alternative programs (KeePass, KeePassX, KeePassXC, …).
|
||||
- **Passkeys** for authentication and **local storage of private keys**.
|
||||
- **Biometric recognition** for fast unlocking (fingerprint / face unlock / …).
|
||||
- **One-Time Password** management (HOTP / TOTP) for two-factor authentication (2FA).
|
||||
- **Autofill** for easy form filling with passwords.
|
||||
- **Magikeyboard** to efficiently fill in any field.
|
||||
- Create **encrypted database files**.
|
||||
- Organisation of credentials by **entry** and in **group** trees.
|
||||
- Allows opening and **copying URI / URL fields quickly**.
|
||||
- **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*.
|
||||
- **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA).
|
||||
- Material design with **themes**.
|
||||
- **Auto-Fill** and Integration.
|
||||
- Field filling **keyboard**.
|
||||
- Dynamic **templates** for each type of entry.
|
||||
- **History** of each entry.
|
||||
- Precise management of **settings**.
|
||||
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
|
||||
- Material design with **themes**.
|
||||
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
|
||||
- **Compatible** with the majority of alternative programs (KeePass, KeePassXC, KeeWeb, …).
|
||||
- Code written in **native languages** (Kotlin / Java / JNI / C).
|
||||
|
||||
KeePassDX is **open source** and **ad-free**.
|
||||
|
||||
@@ -42,20 +45,44 @@ Optional visual styles are accessible after a contribution (and a congratulatory
|
||||
|
||||
* Add features by making a **[pull request](https://help.github.com/articles/about-pull-requests/)**.
|
||||
* Help to **[translate](https://hosted.weblate.org/projects/keepass-dx/strings/)** KeePassDX to your language (on [Weblate](https://hosted.weblate.org/projects/keepass-dx/) or by sending a [pull request](https://help.github.com/articles/about-pull-requests/)).
|
||||
* **[Donate](https://www.kunzisoft.com/donation)** 人◕ ‿‿ ◕人Y for a better service and a quick development of your features.
|
||||
* **[Donate](https://www.keepassdx.com/#donation)** 人◕ ‿‿ ◕人Y for a better service and a quick development of your features.
|
||||
* Buy the **[Pro version](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro)** of KeePassDX.
|
||||
|
||||
## Download
|
||||
|
||||
*[F-Droid](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/) is the recommended way of installing, a libre software project that verifies that all the libraries and app code is libre software.*
|
||||
*[F-Droid](https://f-droid.org/packages/com.kunzisoft.keepass.libre/) is the recommended way of installing, a libre software project that verifies all the libraries and app code is libre software.*
|
||||
|
||||
[<img src="https://f-droid.org/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/)
|
||||
| Source | Status | [Version](https://github.com/Kunzisoft/KeePassDX/wiki/FAQ#why-a-libre-and-free-version) |
|
||||
|--------|--------|---------|
|
||||
| [Google Play](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free) |  | Free + [Pro](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro) |
|
||||
| [F-Droid](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/) |  | Libre |
|
||||
| [IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/com.kunzisoft.keepass.free) |  | Free & [Libre](https://apt.izzysoft.de/fdroid/index/apk/com.kunzisoft.keepass.libre) |
|
||||
| [GitHub](https://github.com/Kunzisoft/KeePassDX/releases) / [Obtainium](https://github.com/ImranR98/Obtainium) |  | Free & 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)
|
||||
## Package authenticity from GitHub
|
||||
- Download the app from [GitHub releases](https://github.com/Kunzisoft/KeePassDX/releases/latest)
|
||||
- Install [`apksigner`](https://developer.android.com/tools/apksigner) from [Android Studio](https://developer.android.com/studio)
|
||||
- Open the directory where you saved the downloaded file in the Terminal
|
||||
- Make sure that you have `apksigner` installed by running:
|
||||
```shell
|
||||
apksigner --version
|
||||
```
|
||||
- Depending on the APK file you downloaded, run:
|
||||
|
||||
```shell
|
||||
apksigner verify --verbose --print-certs -min-sdk-version 24 KeePassDX-*.apk
|
||||
```
|
||||
|
||||
You should get this output :
|
||||
```shell
|
||||
Verified using v2 scheme (APK Signature Scheme v2): true
|
||||
...
|
||||
Number of signers: 1
|
||||
Signer #1 certificate SHA-256 digest: 7d55b8af210381aabf960f07e17cf7857b6d2a642ca2da6bf0bdf1b200362f04
|
||||
...
|
||||
Signer #1 public key SHA-256 digest: 5d261d3176db1e077b80112824d9390167f3be0561827e42112ed6b71192db81
|
||||
```
|
||||
If it's the case, this means that the APK was well built by the author of KeePassDX.
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
@@ -71,7 +98,7 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2020 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||
Copyright © 2025 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||
|
||||
This file is part of KeePassDX.
|
||||
|
||||
|
||||
113
app/build.gradle
@@ -1,18 +1,18 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion "30.0.3"
|
||||
ndkVersion "21.4.7075529"
|
||||
namespace 'com.kunzisoft.keepass'
|
||||
compileSdkVersion 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 30
|
||||
versionCode = 68
|
||||
versionName = "2.9.15"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 35
|
||||
versionCode = 149
|
||||
versionName = "4.2.4"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
@@ -32,48 +32,54 @@ android {
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled = false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
// Disables dependency metadata when building APKs.
|
||||
includeInApk = false
|
||||
// Disables dependency metadata when building Android App Bundles.
|
||||
includeInBundle = false
|
||||
}
|
||||
|
||||
flavorDimensions "version"
|
||||
productFlavors {
|
||||
libre {
|
||||
dimension "version"
|
||||
applicationIdSuffix = ".libre"
|
||||
buildConfigField "String", "BUILD_VERSION", "\"libre\""
|
||||
buildConfigField "boolean", "FULL_VERSION", "true"
|
||||
buildConfigField "boolean", "CLOSED_STORE", "false"
|
||||
buildConfigField "String[]", "STYLES_DISABLED",
|
||||
"{\"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", "{}"
|
||||
}
|
||||
pro {
|
||||
dimension "version"
|
||||
applicationIdSuffix = ".pro"
|
||||
buildConfigField "String", "BUILD_VERSION", "\"pro\""
|
||||
buildConfigField "boolean", "FULL_VERSION", "true"
|
||||
buildConfigField "boolean", "CLOSED_STORE", "true"
|
||||
buildConfigField "String[]", "STYLES_DISABLED", "{}"
|
||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIZiXvrQCzSV9LNI6-p7cjTKENZLHIrz_zaqZuQQ" ]
|
||||
}
|
||||
free {
|
||||
dimension "version"
|
||||
applicationIdSuffix = ".free"
|
||||
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
||||
buildConfigField "boolean", "FULL_VERSION", "false"
|
||||
buildConfigField "boolean", "CLOSED_STORE", "true"
|
||||
buildConfigField "String[]", "STYLES_DISABLED",
|
||||
"{\"KeepassDXStyle_Blue\"," +
|
||||
"\"KeepassDXStyle_Blue_Night\"," +
|
||||
"\"KeepassDXStyle_Red\"," +
|
||||
"\"KeepassDXStyle_Red_Night\"," +
|
||||
"\"KeepassDXStyle_Reply\"," +
|
||||
"\"KeepassDXStyle_Reply_Night\"," +
|
||||
"\"KeepassDXStyle_Purple\"," +
|
||||
"\"KeepassDXStyle_Purple_Dark\"}"
|
||||
"\"KeepassDXStyle_Purple_Dark\"," +
|
||||
"\"KeepassDXStyle_Dynamic_Light\"," +
|
||||
"\"KeepassDXStyle_Dynamic_Night\"}"
|
||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
|
||||
}
|
||||
@@ -81,7 +87,6 @@ android {
|
||||
|
||||
sourceSets {
|
||||
libre.res.srcDir 'src/libre/res'
|
||||
pro.res.srcDir 'src/pro/res'
|
||||
free.res.srcDir 'src/free/res'
|
||||
}
|
||||
|
||||
@@ -90,52 +95,68 @@ android {
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
jvmTarget = "17"
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
packaging {
|
||||
// Bouncy castle bug https://github.com/bcgit/bc-java/issues/1685
|
||||
resources.pickFirsts.add('META-INF/versions/9/OSGI-INF/MANIFEST.MF')
|
||||
}
|
||||
}
|
||||
|
||||
def room_version = "2.2.6"
|
||||
def room_version = "2.5.1"
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||
implementation "com.android.support:multidex:1.0.3"
|
||||
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.0.4'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
|
||||
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-rc01'
|
||||
implementation 'androidx.biometric:biometric:1.1.0'
|
||||
implementation 'androidx.media:media:1.6.0'
|
||||
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||
implementation "androidx.core:core-ktx:1.3.2"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||
// WARNING: Don't upgrade because slowdown https://github.com/Kunzisoft/KeePassDX/issues/923
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
implementation "androidx.core:core-ktx:$android_core_version"
|
||||
implementation "androidx.lifecycle:lifecycle-process:2.6.2"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.0'
|
||||
implementation "com.google.android.material:material:$android_material_version"
|
||||
// Token auto complete
|
||||
// From sources until https://github.com/splitwise/TokenAutoComplete/pull/422 fixed
|
||||
implementation "com.splitwise:tokenautocomplete:4.0.0-beta05"
|
||||
// Database
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
// Autofill
|
||||
implementation "androidx.autofill:autofill:1.1.0"
|
||||
// Time
|
||||
implementation 'joda-time:joda-time:2.10.6'
|
||||
implementation 'joda-time:joda-time:2.13.0'
|
||||
// Color
|
||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4'
|
||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.6'
|
||||
// Education
|
||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
|
||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3'
|
||||
// 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'
|
||||
|
||||
// Credentials Provider
|
||||
implementation "androidx.credentials:credentials:1.2.2"
|
||||
|
||||
// Modules import
|
||||
implementation project(path: ':database')
|
||||
implementation project(path: ':icon-pack')
|
||||
|
||||
// Tests
|
||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||
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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 3,
|
||||
"identityHash": "a20aec7cf09664b1102ec659fa51160a",
|
||||
"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, `read_only` INTEGER, `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": "readOnly",
|
||||
"columnName": "read_only",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "updated",
|
||||
"columnName": "updated",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"database_uri"
|
||||
]
|
||||
},
|
||||
"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": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"database_uri"
|
||||
]
|
||||
},
|
||||
"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, 'a20aec7cf09664b1102ec659fa51160a')"
|
||||
]
|
||||
}
|
||||
}
|
||||
76
app/src/free/assets/passkeys_privileged_apps_community.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "io.github.forkmaintainers.iceraven",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "9C:0D:22:37:9F:48:7B:70:A4:F9:F8:BE:C0:17:3C:F9:1A:16:44:F0:8F:93:38:5B:5B:78:2C:E3:76:60:BA:81"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.chromium.chrome",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "A8:56:48:50:79:BC:B3:57:BF:BE:69:BA:19:A9:BA:43:CD:0A:D9:AB:22:67:52:C7:80:B6:88:8A:FD:48:21:6B"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.cromite.cromite",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "63:3F:A4:1D:82:11:D6:D0:91:6A:81:9B:89:66:8C:6D:E9:2E:64:23:2D:A6:7F:9D:16:FD:81:C3:B7:E9:23:FF"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.ironfoxoss.ironfox",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "C5:E2:91:B5:A5:71:F9:C8:CD:9A:97:99:C2:C9:4E:02:EC:97:03:94:88:93:F2:CA:75:6D:67:B9:42:04:F9:04"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.ironfoxoss.ironfox.nightly",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "C5:E2:91:B5:A5:71:F9:C8:CD:9A:97:99:C2:C9:4E:02:EC:97:03:94:88:93:F2:CA:75:6D:67:B9:42:04:F9:04"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.mozilla.fennec_fdroid",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "06:66:53:58:EF:D8:BA:05:BE:23:6A:47:A1:2C:B0:95:8D:7D:75:DD:93:9D:77:C2:B3:1F:53:98:53:7E:BD:C5"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
820
app/src/free/assets/passkeys_privileged_apps_google.json
Normal file
@@ -0,0 +1,820 @@
|
||||
{
|
||||
"apps": [
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.android.chrome",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
|
||||
},
|
||||
{
|
||||
"build": "userdebug",
|
||||
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.chrome.beta",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "DA:63:3D:34:B6:9E:63:AE:21:03:B4:9D:53:CE:05:2F:C5:F7:F3:C5:3A:AB:94:FD:C2:A2:08:BD:FD:14:24:9C"
|
||||
},
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.chrome.dev",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "90:44:EE:5F:EE:4B:BC:5E:21:DD:44:66:54:31:C4:EB:1F:1F:71:A3:27:16:A0:BC:92:7B:CB:B3:92:33:CA:BF"
|
||||
},
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.chrome.canary",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "20:19:DF:A1:FB:23:EF:BF:70:C5:BC:D1:44:3C:5B:EA:B0:4F:3F:2F:F4:36:6E:9A:C1:E3:45:76:39:A2:4C:FC"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.chromium.chrome",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "C6:AD:B8:B8:3C:6D:4C:17:D2:92:AF:DE:56:FD:48:8A:51:D3:16:FF:8F:2C:11:C5:41:02:23:BF:F8:A7:DB:B3"
|
||||
},
|
||||
{
|
||||
"build": "userdebug",
|
||||
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.google.android.apps.chrome",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "userdebug",
|
||||
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.mozilla.fennec_webauthndebug",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "userdebug",
|
||||
"cert_fingerprint_sha256": "BD:AE:82:02:80:D2:AF:B7:74:94:EF:22:58:AA:78:A9:AE:A1:36:41:7E:8B:C2:3D:C9:87:75:2E:6F:48:E8:48"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.mozilla.firefox",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.mozilla.firefox_beta",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.mozilla.focus",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.mozilla.fennec_aurora",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "BC:04:88:83:8D:06:F4:CA:6B:F3:23:86:DA:AB:0D:D8:EB:CF:3E:77:30:78:74:59:F6:2F:B3:CD:14:A1:BA:AA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.mozilla.rocket",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "86:3A:46:F0:97:39:32:B7:D0:19:9B:54:91:12:74:1C:2D:27:31:AC:72:EA:11:B7:52:3A:A9:0A:11:BF:56:91"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.mozilla.fenix",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "50:04:77:90:88:E7:F9:88:D5:BC:5C:C5:F8:79:8F:EB:F4:F8:CD:08:4A:1B:2A:46:EF:D4:C8:EE:4A:EA:F2:11"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.mozilla.fenix.debug",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "userdebug",
|
||||
"cert_fingerprint_sha256": "BD:AE:82:02:80:D2:AF:B7:74:94:EF:22:58:AA:78:A9:AE:A1:36:41:7E:8B:C2:3D:C9:87:75:2E:6F:48:E8:48"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.mozilla.focus.beta",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.mozilla.focus.nightly",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.mozilla.klar",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "org.mozilla.reference.browser",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "B0:09:90:E3:0F:9D:81:5D:2E:BC:7B:9B:B2:21:CE:47:E5:C9:D5:17:AA:C7:0E:7F:D5:95:B1:E5:3E:9A:4B:14"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.microsoft.emmx.canary",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.microsoft.emmx.dev",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.microsoft.emmx.beta",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.microsoft.emmx",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.microsoft.emmx.rolling",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "userdebug",
|
||||
"cert_fingerprint_sha256": "32:A2:FC:74:D7:31:10:58:59:E5:A8:5D:F1:6D:95:F1:02:D8:5B:22:09:9B:80:64:C5:D8:91:5C:61:DA:D1:E0"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.microsoft.emmx.local",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "userdebug",
|
||||
"cert_fingerprint_sha256": "32:A2:FC:74:D7:31:10:58:59:E5:A8:5D:F1:6D:95:F1:02:D8:5B:22:09:9B:80:64:C5:D8:91:5C:61:DA:D1:E0"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.brave.browser",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.brave.browser_beta",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.brave.browser_nightly",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "app.vanadium.browser",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "C6:AD:B8:B8:3C:6D:4C:17:D2:92:AF:DE:56:FD:48:8A:51:D3:16:FF:8F:2C:11:C5:41:02:23:BF:F8:A7:DB:B3"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.vivaldi.browser",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.vivaldi.browser.snapshot",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.vivaldi.browser.sopranos",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.citrix.Receiver",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "3D:D1:12:67:10:69:AB:36:4E:F9:BE:73:9A:B7:B5:EE:15:E1:CD:E9:D8:75:7B:1B:F0:64:F5:0C:55:68:9A:49"
|
||||
},
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "CE:B2:23:D7:77:09:F2:B6:BC:0B:3A:78:36:F5:A5:AF:4C:E1:D3:55:F4:A7:28:86:F7:9D:F8:0D:C9:D6:12:2E"
|
||||
},
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "AA:D0:D4:57:E6:33:C3:78:25:77:30:5B:C1:B2:D9:E3:81:41:C7:21:DF:0D:AA:6E:29:07:2F:C4:1D:34:F0:AB"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.android.browser",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "C9:00:9D:01:EB:F9:F5:D0:30:2B:C7:1B:2F:E9:AA:9A:47:A4:32:BB:A1:73:08:A3:11:1B:75:D7:B2:14:90:25"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.sec.android.app.sbrowser",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "C8:A2:E9:BC:CF:59:7C:2F:B6:DC:66:BE:E2:93:FC:13:F2:FC:47:EC:77:BC:6B:2B:0D:52:C1:1F:51:19:2A:B8"
|
||||
},
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "34:DF:0E:7A:9F:1C:F1:89:2E:45:C0:56:B4:97:3C:D8:1C:CF:14:8A:40:50:D1:1A:EA:4A:C5:A6:5F:90:0A:42"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.sec.android.app.sbrowser.beta",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "C8:A2:E9:BC:CF:59:7C:2F:B6:DC:66:BE:E2:93:FC:13:F2:FC:47:EC:77:BC:6B:2B:0D:52:C1:1F:51:19:2A:B8"
|
||||
},
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "34:DF:0E:7A:9F:1C:F1:89:2E:45:C0:56:B4:97:3C:D8:1C:CF:14:8A:40:50:D1:1A:EA:4A:C5:A6:5F:90:0A:42"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.google.android.gms",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "7C:E8:3C:1B:71:F3:D5:72:FE:D0:4C:8D:40:C5:CB:10:FF:75:E6:D8:7D:9D:F6:FB:D5:3F:04:68:C2:90:50:53"
|
||||
},
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "D2:2C:C5:00:29:9F:B2:28:73:A0:1A:01:0D:E1:C8:2F:BE:4D:06:11:19:B9:48:14:DD:30:1D:AB:50:CB:76:78"
|
||||
},
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
|
||||
},
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.yandex.browser",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.yandex.browser.beta",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.yandex.browser.alpha",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.yandex.browser.corp",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.yandex.browser.canary",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "1D:A9:CB:AE:2D:CC:C6:A5:8D:6C:94:7B:E9:4C:DB:B7:33:D6:5D:A4:D1:77:0F:A1:4A:53:64:CB:4A:28:EB:49"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.yandex.browser.broteam",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "1D:A9:CB:AE:2D:CC:C6:A5:8D:6C:94:7B:E9:4C:DB:B7:33:D6:5D:A4:D1:77:0F:A1:4A:53:64:CB:4A:28:EB:49"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.talonsec.talon",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "A3:66:03:44:A6:F6:AF:CA:81:8C:BF:43:96:A2:3C:CF:D5:ED:7A:78:1B:B4:A3:D1:85:03:01:E2:F4:6D:23:83"
|
||||
},
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "E2:A5:64:74:EA:23:7B:06:67:B6:F5:2C:DC:E9:04:5E:24:88:3B:AE:D0:82:59:9A:A2:DF:0B:60:3A:CF:6A:3B"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.talonsec.talon_beta",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "F5:86:62:7A:32:C8:9F:E6:7E:00:6D:B1:8C:34:31:9E:01:7F:B3:B2:BE:D6:9D:01:01:B7:F9:43:E7:7C:48:AE"
|
||||
},
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "9A:A1:25:D5:E5:5E:3F:B0:DE:96:72:D9:A9:5D:04:65:3F:49:4A:1E:C3:EE:76:1E:94:C4:4E:5D:2F:65:8E:2F"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.duckduckgo.mobile.android.debug",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "C4:F0:9E:2B:D7:25:AD:F5:AD:92:0B:A2:80:27:66:AC:16:4A:C1:53:B3:EA:9E:08:48:B0:57:98:37:F7:6A:29"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.duckduckgo.mobile.android",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "BB:7B:B3:1C:57:3C:46:A1:DA:7F:C5:C5:28:A6:AC:F4:32:10:84:56:FE:EC:50:81:0C:7F:33:69:4E:B3:D2:D4"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.naver.whale",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "0B:8B:85:23:BB:4A:EF:FA:34:6E:4B:DD:4F:BF:7D:19:34:50:56:9A:A1:4A:AA:D4:AD:FD:94:A3:F7:B2:27:BB"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.fido.fido2client",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "FC:98:DA:E6:3A:D3:96:26:C8:C6:7F:BE:83:F2:F0:6F:74:93:2A:9C:D1:46:B9:2C:EC:FC:6A:04:7A:90:43:86"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.heytap.browser",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "AF:F8:A7:49:CF:0E:7D:75:44:65:D0:FB:FA:7B:8D:0C:64:5E:22:5C:10:C6:E2:32:AD:A0:D9:74:88:36:B8:E5"
|
||||
},
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "A8:FE:A4:CA:FB:93:32:DA:26:B8:E6:81:08:17:C1:DA:90:A5:03:0E:35:A6:0A:79:E0:6C:90:97:AA:C6:A4:42"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "io.island.Island",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "D9:C3:39:AC:9C:3A:EE:E1:75:1D:85:8C:35:D9:BA:C5:CC:87:B3:CE:76:30:93:F0:F5:10:64:F5:A2:F6:9B:04"
|
||||
},
|
||||
{
|
||||
"build": "userdebug",
|
||||
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "io.island.IslandCanary",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "90:17:13:23:45:6E:6F:39:CB:FD:CF:B2:56:BE:1D:CF:F3:BC:1C:59:8A:15:93:30:E4:97:73:D0:4C:B9:C9:05"
|
||||
},
|
||||
{
|
||||
"build": "userdebug",
|
||||
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "io.island.IslandBeta",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "35:31:83:1A:9E:2B:21:1D:E6:AA:C3:69:4B:45:83:6E:56:09:B9:D7:D0:04:C3:1B:21:87:40:FB:77:17:38:D1"
|
||||
},
|
||||
{
|
||||
"build": "userdebug",
|
||||
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "io.island.IslandDev",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "userdebug",
|
||||
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "io.island.island.intune",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "C2:38:24:15:41:20:A0:8F:C3:95:42:AC:D8:2A:E9:24:94:78:80:1E:47:FD:6C:66:2B:18:1C:28:CA:7E:59:4E"
|
||||
},
|
||||
{
|
||||
"build": "userdebug",
|
||||
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "io.island.island.canary.intune",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "1E:16:74:BB:79:EA:09:FB:37:CF:9F:1B:07:1B:1D:51:8D:46:03:0E:D3:EE:F2:C1:4E:AD:93:9E:C6:EE:3A:4C"
|
||||
},
|
||||
{
|
||||
"build": "userdebug",
|
||||
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "io.island.island.beta.intune",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "D2:5E:AD:F6:1C:E6:36:6C:A4:23:A4:7F:C4:DB:9B:8C:9C:8A:35:B4:B0:19:E8:D9:82:FB:D0:8A:D9:DB:49:5A"
|
||||
},
|
||||
{
|
||||
"build": "userdebug",
|
||||
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "io.island.island.dev.intune",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "userdebug",
|
||||
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "net.quetta.browser",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "BE:FE:E7:31:12:6A:A5:6E:7E:FD:AE:AF:5E:F3:FA:EA:44:1C:19:CC:E0:CA:EC:42:6B:65:BB:F8:2C:59:46:80"
|
||||
},
|
||||
{
|
||||
"build": "userdebug",
|
||||
"cert_fingerprint_sha256": "F1:38:00:4F:38:04:51:D4:8A:05:2B:B3:A3:EF:17:24:23:D4:B0:D0:C8:A3:AA:DD:FB:DB:66:30:31:48:EC:A4"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "cz.seznam.sbrowser",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "DB:95:40:66:10:78:83:6E:4E:B1:66:F6:9E:F4:07:30:9E:8D:AE:33:34:68:5E:C8:F6:FA:2F:13:81:B9:AC:F6"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.opera.mini.native",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "57:AC:BC:52:5F:1B:2E:BD:19:19:6C:D6:F0:14:39:7C:C9:10:FD:18:84:1E:0A:E8:50:FE:BC:3E:1E:59:3F:F2"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"info": {
|
||||
"package_name": "com.opera.mini.native.beta",
|
||||
"signatures": [
|
||||
{
|
||||
"build": "release",
|
||||
"cert_fingerprint_sha256": "57:AC:BC:52:5F:1B:2E:BD:19:19:6C:D6:F0:14:39:7C:C9:10:FD:18:84:1E:0A:E8:50:FE:BC:3E:1E:59:3F:F2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,61 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108"
|
||||
android:width="108dp"
|
||||
android:height="108dp">
|
||||
android:height="108dp"
|
||||
android:viewportWidth="120"
|
||||
android:viewportHeight="120">
|
||||
<group
|
||||
android:translateY="-332">
|
||||
<group
|
||||
android:translateY="332">
|
||||
<path
|
||||
android:pathData="M65.728516 32.791016L58.052734 35.904297 56.173828 48.380859 35.306641 69.267578 35.238281 73.759766 69.478516 108 108 108 108 70.810547 73.09375 35.904297 65.728516 32.791016Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeMiterLimit="4" >
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endColor="#0000"
|
||||
android:endX="80"
|
||||
android:endY="80"
|
||||
android:startColor="#4e000000"
|
||||
android:startX="0"
|
||||
android:startY="0"
|
||||
android:type="linear"/>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</group>
|
||||
<group
|
||||
android:scaleX="0.3939503"
|
||||
android:scaleY="0.3939503"
|
||||
android:translateX="33.66343"
|
||||
android:translateY="233.998">
|
||||
<path
|
||||
android:pathData="M88.76953 339.91602L4.1718754 424.59766 4.0000004 436 15.400391 435.82813 27.240234 424 40 424l0 -12 12 0 0 -12.73438 34.01172 -33.97656A8 8 0 0 1 84 360a8 8 0 0 1 8 -8 8 8 0 0 1 5.296882 2.01367l2.787098 -2.7832 -11.31445 -11.31445z"
|
||||
android:fillColor="#eaeaea"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#58000000" />
|
||||
</group>
|
||||
<group
|
||||
android:scaleX="0.3939503"
|
||||
android:scaleY="0.3939503"
|
||||
android:translateX="33.66343"
|
||||
android:translateY="233.998">
|
||||
<path
|
||||
android:pathData="M4.0000004 340L4.1718754 351.40137 88.59863 435.82812 100 436 99.828122 424.59863 15.401367 340.17188Z"
|
||||
android:fillColor="#81c784" />
|
||||
</group>
|
||||
<group
|
||||
android:scaleX="0.3939503"
|
||||
android:scaleY="0.3939503"
|
||||
android:translateX="33.66343"
|
||||
android:translateY="233.998">
|
||||
<path
|
||||
android:pathData="M81.39454 332.00195a27 27 0 0 0 -19.48634 7.90625 27 27 0 0 0 0 38.1836 27 27 0 0 0 38.1836 0 27 27 0 0 0 0 -38.1836 27 27 0 0 0 -18.69726 -7.90625zM92 352a8 8 0 0 1 8 8 8 8 0 0 1 -8 8 8 8 0 0 1 -8 -8 8 8 0 0 1 8 -8z"
|
||||
android:fillColor="#eaeaea"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#58000000" />
|
||||
</group>
|
||||
android:translateX="6"
|
||||
android:translateY="8">
|
||||
<path
|
||||
android:fillColor="#24000000"
|
||||
android:strokeWidth="1.99999297"
|
||||
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||
<path
|
||||
android:fillColor="#24000000"
|
||||
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>
|
||||
<group
|
||||
android:translateX="6"
|
||||
android:translateY="6">
|
||||
<path
|
||||
android:fillColor="#ffa726"
|
||||
android:strokeWidth="1.99999297"
|
||||
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||
<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-v24/ic_launcher_monochrome.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="120"
|
||||
android:viewportHeight="120">
|
||||
<group
|
||||
android:translateX="6"
|
||||
android:translateY="6">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeWidth="1.99999297"
|
||||
android:pathData="M63.9961,34.0059 C61.5643,34.096,59.2564,35.102,57.5352,36.8223 C53.7682,40.589,53.7682,46.6982,57.5352,50.4649 C61.3017,54.232,67.4073,54.232,71.1739,50.4649 C74.9409,46.6982,74.9409,40.589,71.1739,36.8223 C69.2766,34.9258,66.6768,33.9054,63.9962,34.0059 Z M68.1992,40.6954 C69.8278,40.6958,71.148,42.016,71.1484,43.6446 C71.148,45.2732,69.8278,46.5934,68.1992,46.5938 C66.5706,46.5934,65.2504,45.2732,65.25,43.6446 C65.2504,42.016,66.5706,40.6958,68.1992,40.6954 Z M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
15
app/src/free/res/drawable/ic_app_key_white_24dp.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="84"
|
||||
android:viewportHeight="84">
|
||||
<group
|
||||
android:translateX="-12"
|
||||
android:translateY="-12">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeWidth="1.99999297"
|
||||
android:pathData="M63.9961,34.0059 C61.5643,34.096,59.2564,35.102,57.5352,36.8223 C53.7682,40.589,53.7682,46.6982,57.5352,50.4649 C61.3017,54.232,67.4073,54.232,71.1739,50.4649 C74.9409,46.6982,74.9409,40.589,71.1739,36.8223 C69.2766,34.9258,66.6768,33.9054,63.9962,34.0059 Z M68.1992,40.6954 C69.8278,40.6958,71.148,42.016,71.1484,43.6446 C71.148,45.2732,69.8278,46.5934,68.1992,46.5938 C66.5706,46.5934,65.2504,45.2732,65.25,43.6446 C65.2504,42.016,66.5706,40.6958,68.1992,40.6954 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
15
app/src/free/res/drawable/ic_app_lock_white_24dp.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="84"
|
||||
android:viewportHeight="84">
|
||||
<group
|
||||
android:translateX="-12"
|
||||
android:translateY="-12">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeWidth="1.99999297"
|
||||
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.8 KiB |
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/green" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 13 KiB |
@@ -1,61 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108"
|
||||
android:width="108dp"
|
||||
android:height="108dp">
|
||||
android:height="108dp"
|
||||
android:viewportWidth="120"
|
||||
android:viewportHeight="120">
|
||||
<group
|
||||
android:translateY="-332">
|
||||
<group
|
||||
android:translateY="332">
|
||||
<path
|
||||
android:pathData="M65.728516 32.791016L58.052734 35.904297 56.173828 48.380859 35.306641 69.267578 35.238281 73.759766 69.478516 108 108 108 108 70.810547 73.09375 35.904297 65.728516 32.791016Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeMiterLimit="4" >
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endColor="#0000"
|
||||
android:endX="80"
|
||||
android:endY="80"
|
||||
android:startColor="#4e000000"
|
||||
android:startX="0"
|
||||
android:startY="0"
|
||||
android:type="linear"/>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</group>
|
||||
<group
|
||||
android:scaleX="0.3939503"
|
||||
android:scaleY="0.3939503"
|
||||
android:translateX="33.66343"
|
||||
android:translateY="233.998">
|
||||
<path
|
||||
android:pathData="M88.76953 339.91602L4.1718754 424.59766 4.0000004 436 15.400391 435.82813 27.240234 424 40 424l0 -12 12 0 0 -12.73438 34.01172 -33.97656A8 8 0 0 1 84 360a8 8 0 0 1 8 -8 8 8 0 0 1 5.296882 2.01367l2.787098 -2.7832 -11.31445 -11.31445z"
|
||||
android:fillColor="#eaeaea"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#58000000"/>
|
||||
</group>
|
||||
<group
|
||||
android:scaleX="0.3939503"
|
||||
android:scaleY="0.3939503"
|
||||
android:translateX="33.66343"
|
||||
android:translateY="233.998">
|
||||
<path
|
||||
android:pathData="M4.0000004 340L4.1718754 351.40137 88.59863 435.82812 100 436 99.828122 424.59863 15.401367 340.17188Z"
|
||||
android:fillColor="#64b5f6" />
|
||||
</group>
|
||||
<group
|
||||
android:scaleX="0.3939503"
|
||||
android:scaleY="0.3939503"
|
||||
android:translateX="33.66343"
|
||||
android:translateY="233.998">
|
||||
<path
|
||||
android:pathData="M81.39454 332.00195a27 27 0 0 0 -19.48634 7.90625 27 27 0 0 0 0 38.1836 27 27 0 0 0 38.1836 0 27 27 0 0 0 0 -38.1836 27 27 0 0 0 -18.69726 -7.90625zM92 352a8 8 0 0 1 8 8 8 8 0 0 1 -8 8 8 8 0 0 1 -8 -8 8 8 0 0 1 8 -8z"
|
||||
android:fillColor="#eaeaea"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#58000000" />
|
||||
</group>
|
||||
android:translateX="6"
|
||||
android:translateY="8">
|
||||
<path
|
||||
android:fillColor="#24000000"
|
||||
android:strokeWidth="1.99999297"
|
||||
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||
<path
|
||||
android:fillColor="#24000000"
|
||||
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>
|
||||
<group
|
||||
android:translateX="6"
|
||||
android:translateY="6">
|
||||
<path
|
||||
android:fillColor="#ffa726"
|
||||
android:strokeWidth="1.99999297"
|
||||
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||
<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-v24/ic_launcher_monochrome.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="120"
|
||||
android:viewportHeight="120">
|
||||
<group
|
||||
android:translateX="6"
|
||||
android:translateY="6">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeWidth="1.99999297"
|
||||
android:pathData="M64.501,35.0576 C63.7095,35.0576,62.918,35.3613,62.3115,35.9678 L55.0127,43.2666 C53.7998,44.4795,53.7998,46.4306,55.0127,47.6436 L62.3115,54.9424 C63.5244,56.1553,65.4775,56.1553,66.6904,54.9424 L73.9873,47.6436 C75.2002,46.4307,75.2002,44.4796,73.9873,43.2666 L66.6904,35.9678 C66.0839,35.3613,65.2924,35.0576,64.5009,35.0576 Z M67.6729,42.6006 C69.3298,42.6006,70.6729,43.9437,70.6729,45.6006 C70.6729,47.2575,69.3298,48.6006,67.6729,48.6006 C66.016,48.6006,64.6729,47.2575,64.6729,45.6006 C64.6729,43.9437,66.016,42.6006,67.6729,42.6006 Z M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
15
app/src/libre/res/drawable/ic_app_key_white_24dp.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="84"
|
||||
android:viewportHeight="84">
|
||||
<group
|
||||
android:translateX="-12"
|
||||
android:translateY="-12">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeWidth="1.99999297"
|
||||
android:pathData="M64.501,35.0576 C63.7095,35.0576,62.918,35.3613,62.3115,35.9678 L55.0127,43.2666 C53.7998,44.4795,53.7998,46.4306,55.0127,47.6436 L62.3115,54.9424 C63.5244,56.1553,65.4775,56.1553,66.6904,54.9424 L73.9873,47.6436 C75.2002,46.4307,75.2002,44.4796,73.9873,43.2666 L66.6904,35.9678 C66.0839,35.3613,65.2924,35.0576,64.5009,35.0576 Z M67.6729,42.6006 C69.3298,42.6006,70.6729,43.9437,70.6729,45.6006 C70.6729,47.2575,69.3298,48.6006,67.6729,48.6006 C66.016,48.6006,64.6729,47.2575,64.6729,45.6006 C64.6729,43.9437,66.016,42.6006,67.6729,42.6006 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
15
app/src/libre/res/drawable/ic_app_lock_white_24dp.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="84"
|
||||
android:viewportHeight="84">
|
||||
<group
|
||||
android:translateX="-12"
|
||||
android:translateY="-12">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeWidth="1.99999297"
|
||||
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/green" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 12 KiB |
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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,19 +9,28 @@
|
||||
android:anyDensity="true" />
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
||||
<uses-permission
|
||||
android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission
|
||||
android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission
|
||||
android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission
|
||||
android:name="android.permission.VIBRATE"/>
|
||||
<!-- Write permission until Android 10 -->
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<!-- Open apps from links -->
|
||||
<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"
|
||||
@@ -30,29 +38,32 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:name="com.kunzisoft.keepass.app.App"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:fullBackupContent="@xml/old_backup_rules"
|
||||
android:dataExtractionRules="@xml/backup_rules"
|
||||
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
|
||||
android:largeHeap="true"
|
||||
android:resizeableActivity="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/KeepassDXStyle.Night"
|
||||
tools:targetApi="n">
|
||||
tools:targetApi="s"
|
||||
tools:ignore="CredentialDependency">
|
||||
<meta-data
|
||||
android:name="com.google.android.backup.api_key"
|
||||
android:value="${googleAndroidBackupAPIKey}" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.FileDatabaseSelectActivity"
|
||||
android:theme="@style/KeepassDXStyle.SplashScreen"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTop"
|
||||
android:exported="true"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:windowSoftInputMode="stateHidden" >
|
||||
android:windowSoftInputMode="stateHidden|stateAlwaysHidden" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.PasswordActivity"
|
||||
android:name="com.kunzisoft.keepass.activities.MainCredentialActivity"
|
||||
android:exported="true"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:windowSoftInputMode="adjustResize|stateUnchanged">
|
||||
<intent-filter>
|
||||
@@ -111,9 +122,9 @@
|
||||
<!-- Main Activity -->
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.GroupActivity"
|
||||
android:exported="false"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:windowSoftInputMode="adjustPan"
|
||||
android:launchMode="singleTask">
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<meta-data
|
||||
android:name="android.app.default_searchable"
|
||||
android:value="com.kunzisoft.keepass.search.SearchResults"
|
||||
@@ -132,6 +143,9 @@
|
||||
<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" />
|
||||
@@ -146,16 +160,40 @@
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
|
||||
android:name="com.kunzisoft.keepass.settings.DeviceUnlockSettingsActivity" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
||||
android:label="@string/keyboard_setting_label"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity"
|
||||
tools:targetApi="26" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.settings.PasskeysSettingsActivity"
|
||||
tools:targetApi="34" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity"
|
||||
android:theme="@style/Theme.Transparent"
|
||||
android:configChanges="keyboardHidden" />
|
||||
android:exported="false"
|
||||
android:excludeFromRecents="true" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
|
||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity"
|
||||
android:theme="@style/Theme.Transparent"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:exported="false"
|
||||
android:excludeFromRecents="true" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
|
||||
android:theme="@style/Theme.Transparent">
|
||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity"
|
||||
android:theme="@style/Theme.Transparent"
|
||||
android:launchMode="singleInstance"
|
||||
android:exported="true"
|
||||
android:excludeFromRecents="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -165,41 +203,48 @@
|
||||
<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">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity"
|
||||
android:theme="@style/Theme.Transparent"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:exported="false"
|
||||
android:excludeFromRecents="true"
|
||||
tools:targetApi="upside_down_cake" />
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.services.AttachmentFileNotificationService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.services.ClipboardEntryNotificationService"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.services.AdvancedUnlockNotificationService"
|
||||
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.services.DeviceUnlockNotificationService"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<!-- Receiver for Autofill -->
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
|
||||
android:label="@string/autofill_service_name"
|
||||
android:name="com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService"
|
||||
android:label="@string/app_name"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
||||
<meta-data
|
||||
android:name="android.autofill"
|
||||
@@ -209,8 +254,9 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.magikeyboard.MagikIME"
|
||||
android:name="com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService"
|
||||
android:label="@string/keyboard_label"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_INPUT_METHOD" >
|
||||
<meta-data android:name="android.view.im"
|
||||
android:resource="@xml/keyboard_method"/>
|
||||
@@ -219,9 +265,29 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
|
||||
android:name="com.kunzisoft.keepass.credentialprovider.passkey.PasskeyProviderService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
android:exported="true"
|
||||
android:label="@string/passkey_service_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
|
||||
tools:targetApi="upside_down_cake">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.credentials.CredentialProviderService" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.credentials.provider"
|
||||
android:resource="@xml/provider" />
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.app.action.ENTER_KNOX_DESKTOP_MODE" />
|
||||
<action android:name="android.app.action.EXIT_KNOX_DESKTOP_MODE" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
||||
</application>
|
||||
|
||||
@@ -30,6 +30,7 @@ package com.igreenwood.loupe
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.ValueAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.PointF
|
||||
import android.graphics.Rect
|
||||
@@ -37,7 +38,11 @@ import android.graphics.RectF
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.os.Build
|
||||
import android.util.TypedValue
|
||||
import android.view.*
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.ScaleGestureDetector
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.view.animation.Interpolator
|
||||
@@ -108,6 +113,8 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
var viewDragFriction = DEFAULT_VIEW_DRAG_FRICTION
|
||||
// drag distance threshold in dp for swipe to dismiss
|
||||
var dragDismissDistanceInDp = DEFAULT_DRAG_DISMISS_DISTANCE_IN_DP
|
||||
// on view touched
|
||||
var onViewTouchedListener: View.OnTouchListener? = null
|
||||
// on view translate listener
|
||||
var onViewTranslateListener: OnViewTranslateListener? = null
|
||||
// on scale changed
|
||||
@@ -169,16 +176,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
|
||||
}
|
||||
@@ -188,22 +195,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
|
||||
}
|
||||
|
||||
@@ -216,13 +224,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 {
|
||||
@@ -231,9 +237,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
|
||||
}
|
||||
@@ -272,7 +276,10 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
private var imageViewRef: WeakReference<ImageView> = WeakReference(imageView)
|
||||
private var containerRef: WeakReference<ViewGroup> = WeakReference(container)
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouch(view: View?, event: MotionEvent?): Boolean {
|
||||
onViewTouchedListener?.onTouch(view, event)
|
||||
|
||||
event ?: return false
|
||||
val imageView = imageViewRef.get() ?: return false
|
||||
val container = containerRef.get() ?: return false
|
||||
@@ -352,77 +359,40 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
|
||||
isViewTranslateAnimationRunning = true
|
||||
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
imageView.run {
|
||||
val translationY = if (velY > 0) {
|
||||
originalViewBounds.top + height - top
|
||||
} else {
|
||||
originalViewBounds.top - height - top
|
||||
}
|
||||
animate()
|
||||
.setDuration(dismissAnimationDuration)
|
||||
.setInterpolator(dismissAnimationInterpolator)
|
||||
.translationY(translationY.toFloat())
|
||||
.setUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||
}
|
||||
.setListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator?) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
onViewTranslateListener?.onDismiss(imageView)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator?) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator?) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, if (velY > 0) {
|
||||
originalViewBounds.top + imageView.height - imageView.top
|
||||
imageView.run {
|
||||
val translationY = if (velY > 0) {
|
||||
originalViewBounds.top + height - top
|
||||
} else {
|
||||
originalViewBounds.top - imageView.height - imageView.top
|
||||
}.toFloat()).apply {
|
||||
duration = dismissAnimationDuration
|
||||
interpolator = dismissAnimationInterpolator
|
||||
addUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||
}
|
||||
addListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator?) {
|
||||
// no op
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator?) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
onViewTranslateListener?.onDismiss(imageView)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator?) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator?) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
start()
|
||||
originalViewBounds.top - height - top
|
||||
}
|
||||
animate()
|
||||
.setDuration(dismissAnimationDuration)
|
||||
.setInterpolator(dismissAnimationInterpolator)
|
||||
.translationY(translationY.toFloat())
|
||||
.setUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||
}
|
||||
.setListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
onViewTranslateListener?.onDismiss(imageView)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,20 +444,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
|
||||
}
|
||||
})
|
||||
@@ -525,11 +495,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)
|
||||
@@ -537,11 +507,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
|
||||
}
|
||||
})
|
||||
@@ -579,11 +549,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
|
||||
@@ -593,11 +563,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
|
||||
}
|
||||
})
|
||||
@@ -650,137 +620,76 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
||||
|
||||
private fun restoreViewTransform() {
|
||||
val imageView = imageViewRef.get() ?: return
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
imageView.run {
|
||||
animate()
|
||||
.setDuration(restoreAnimationDuration)
|
||||
.setInterpolator(restoreAnimationInterpolator)
|
||||
.translationY((originalViewBounds.top - top).toFloat())
|
||||
.setUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(this, amount)
|
||||
imageView.run {
|
||||
animate()
|
||||
.setDuration(restoreAnimationDuration)
|
||||
.setInterpolator(restoreAnimationInterpolator)
|
||||
.translationY((originalViewBounds.top - top).toFloat())
|
||||
.setUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(this, amount)
|
||||
}
|
||||
.setListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
.setListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator?) {
|
||||
// no op
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator?) {
|
||||
onViewTranslateListener?.onRestore(imageView)
|
||||
}
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
onViewTranslateListener?.onRestore(imageView)
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator?) {
|
||||
// no op
|
||||
}
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator?) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, (originalViewBounds.top - imageView.top).toFloat()).apply {
|
||||
duration = restoreAnimationDuration
|
||||
interpolator = restoreAnimationInterpolator
|
||||
addUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||
}
|
||||
addListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator?) {
|
||||
// no op
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator?) {
|
||||
onViewTranslateListener?.onRestore(imageView)
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator?) {
|
||||
// no op
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator?) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
start()
|
||||
}
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun startDragToDismissAnimation() {
|
||||
val imageView = imageViewRef.get() ?: return
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
imageView.run {
|
||||
val translationY = if (y - initialY > 0) {
|
||||
originalViewBounds.top + height - top
|
||||
} else {
|
||||
originalViewBounds.top - height - top
|
||||
}
|
||||
animate()
|
||||
.setDuration(dismissAnimationDuration)
|
||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||
.translationY(translationY.toFloat())
|
||||
.setUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(this, amount)
|
||||
imageView.run {
|
||||
val translationY = if (y - initialY > 0) {
|
||||
originalViewBounds.top + height - top
|
||||
} else {
|
||||
originalViewBounds.top - height - top
|
||||
}
|
||||
animate()
|
||||
.setDuration(dismissAnimationDuration)
|
||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||
.translationY(translationY.toFloat())
|
||||
.setUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(this, amount)
|
||||
}
|
||||
.setListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = true
|
||||
}
|
||||
.setListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator?) {
|
||||
isViewTranslateAnimationRunning = true
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator?) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
onViewTranslateListener?.onDismiss(imageView)
|
||||
cleanup()
|
||||
}
|
||||
override fun onAnimationEnd(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
onViewTranslateListener?.onDismiss(imageView)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator?) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
}
|
||||
override fun onAnimationCancel(p0: Animator) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator?) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, imageView.translationY.toFloat()).apply {
|
||||
duration = dismissAnimationDuration
|
||||
interpolator = AccelerateDecelerateInterpolator()
|
||||
addUpdateListener {
|
||||
val amount = calcTranslationAmount()
|
||||
changeBackgroundAlpha(amount)
|
||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||
}
|
||||
addListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(p0: Animator?) {
|
||||
isViewTranslateAnimationRunning = true
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(p0: Animator?) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
onViewTranslateListener?.onDismiss(imageView)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(p0: Animator?) {
|
||||
isViewTranslateAnimationRunning = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(p0: Animator?) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
start()
|
||||
}
|
||||
override fun onAnimationRepeat(p0: Animator) {
|
||||
// no op
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun processFlingToDismiss(velocityY: Float) {
|
||||
|
||||
@@ -24,12 +24,15 @@ import android.os.Bundle
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
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.AppUtil.isContributingUser
|
||||
import com.kunzisoft.keepass.utils.getPackageInfoCompat
|
||||
import org.joda.time.DateTime
|
||||
|
||||
class AboutActivity : StylishActivity() {
|
||||
@@ -45,10 +48,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)
|
||||
@@ -68,6 +77,14 @@ class AboutActivity : StylishActivity() {
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
text = HtmlCompat.fromHtml(getString(R.string.html_about_licence, DateTime().year),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
textDirection = View.TEXT_DIRECTION_ANY_RTL
|
||||
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentSender
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.inputmethod.InlineSuggestionsRequest
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper.EXTRA_INLINE_SUGGESTIONS_REQUEST
|
||||
import com.kunzisoft.keepass.autofill.KeeAutofillService
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
class AutofillLauncherActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
// Retrieve selection mode
|
||||
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
|
||||
when (specialMode) {
|
||||
SpecialMode.SELECTION -> {
|
||||
// Build search param
|
||||
val searchInfo = SearchInfo().apply {
|
||||
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
|
||||
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
|
||||
webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME)
|
||||
}
|
||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
launchSelection(searchInfo)
|
||||
}
|
||||
}
|
||||
SpecialMode.REGISTRATION -> {
|
||||
// To register info
|
||||
val registerInfo = intent.getParcelableExtra<RegisterInfo>(KEY_REGISTER_INFO)
|
||||
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
launchRegistration(searchInfo, registerInfo)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Not an autofill call
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun launchSelection(searchInfo: SearchInfo) {
|
||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
||||
val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent)
|
||||
|
||||
if (autofillComponent == null) {
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
} else if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
|
||||
PreferencesUtil.applicationIdBlocklist(this))
|
||||
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
|
||||
PreferencesUtil.webDomainBlocklist(this))) {
|
||||
showBlockRestartMessage()
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
} else {
|
||||
val database = Database.getInstance()
|
||||
val readOnly = database.isReadOnly
|
||||
// If database is open
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
Database.getInstance(),
|
||||
searchInfo,
|
||||
{ items ->
|
||||
// Items found
|
||||
AutofillHelper.buildResponseAndSetResult(this, items)
|
||||
finish()
|
||||
},
|
||||
{
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForAutofillResult(this,
|
||||
readOnly,
|
||||
autofillComponent,
|
||||
searchInfo,
|
||||
false)
|
||||
},
|
||||
{
|
||||
// If database not open
|
||||
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
||||
autofillComponent,
|
||||
searchInfo)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchRegistration(searchInfo: SearchInfo, registerInfo: RegisterInfo?) {
|
||||
if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
|
||||
PreferencesUtil.applicationIdBlocklist(this))
|
||||
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
|
||||
PreferencesUtil.webDomainBlocklist(this))) {
|
||||
showBlockRestartMessage()
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
} else {
|
||||
val database = Database.getInstance()
|
||||
val readOnly = database.isReadOnly
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
database,
|
||||
searchInfo,
|
||||
{ _ ->
|
||||
if (!readOnly) {
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForRegistration(this,
|
||||
registerInfo)
|
||||
} else {
|
||||
showReadOnlySaveMessage()
|
||||
}
|
||||
},
|
||||
{
|
||||
if (!readOnly) {
|
||||
// Show the database UI to select the entry
|
||||
GroupActivity.launchForRegistration(this,
|
||||
registerInfo)
|
||||
} else {
|
||||
showReadOnlySaveMessage()
|
||||
}
|
||||
},
|
||||
{
|
||||
// If database not open
|
||||
FileDatabaseSelectActivity.launchForRegistration(this,
|
||||
registerInfo)
|
||||
}
|
||||
)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun showBlockRestartMessage() {
|
||||
// If item not allowed, show a toast
|
||||
Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
private fun showReadOnlySaveMessage() {
|
||||
Toast.makeText(this.applicationContext, R.string.autofill_read_only_save, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||
|
||||
if (PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
|
||||
// Close the database
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
|
||||
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
|
||||
private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME"
|
||||
|
||||
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
|
||||
|
||||
fun getAuthIntentSenderForSelection(context: Context,
|
||||
searchInfo: SearchInfo? = null,
|
||||
inlineSuggestionsRequest: InlineSuggestionsRequest? = null): IntentSender {
|
||||
return PendingIntent.getActivity(context, 0,
|
||||
// Doesn't work with Parcelable (don't know why?)
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
searchInfo?.let {
|
||||
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
|
||||
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
|
||||
putExtra(KEY_SEARCH_SCHEME, it.webScheme)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
inlineSuggestionsRequest?.let {
|
||||
putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||
}
|
||||
}
|
||||
},
|
||||
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
||||
}
|
||||
|
||||
fun getAuthIntentSenderForRegistration(context: Context,
|
||||
registerInfo: RegisterInfo): IntentSender {
|
||||
return PendingIntent.getActivity(context, 0,
|
||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
||||
putExtra(KEY_REGISTER_INFO, registerInfo)
|
||||
},
|
||||
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
||||
}
|
||||
|
||||
fun launchForRegistration(context: Context,
|
||||
registerInfo: RegisterInfo) {
|
||||
val intent = Intent(context, AutofillLauncherActivity::class.java)
|
||||
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
|
||||
intent.putExtra(KEY_REGISTER_INFO, registerInfo)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,70 +30,102 @@ 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 android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||
import androidx.core.graphics.BlendModeCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.view.ViewCompat
|
||||
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.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
||||
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.adapters.TagsAdapter
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||
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.Entry
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.otp.OtpType
|
||||
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
||||
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
import com.kunzisoft.keepass.view.EntryContentsView
|
||||
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||
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 java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||
import java.util.EnumSet
|
||||
import java.util.UUID
|
||||
|
||||
class EntryActivity : LockingActivity() {
|
||||
class EntryActivity : DatabaseLockActivity() {
|
||||
|
||||
private var footer: ViewGroup? = null
|
||||
private var container: View? = 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 entryContentsView: EntryContentsView? = null
|
||||
private var entryProgress: ProgressBar? = 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
|
||||
private var toolbar: Toolbar? = null
|
||||
private var loadingView: ProgressBar? = null
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
private val mEntryViewModel: EntryViewModel by viewModels()
|
||||
|
||||
private var mEntry: Entry? = null
|
||||
private val mEntryActivityEducation = EntryActivityEducation(this)
|
||||
|
||||
private var mIsHistory: Boolean = false
|
||||
private var mEntryLastVersion: Entry? = null
|
||||
private var mEntryHistoryPosition: Int = -1
|
||||
|
||||
private var mShowPassword: Boolean = false
|
||||
private var mMainEntryId: NodeId<UUID>? = null
|
||||
private var mHistoryPosition: Int = -1
|
||||
private var mEntryIsHistory: Boolean = false
|
||||
private var mEntryLoaded = false
|
||||
|
||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
private var mAttachmentSelected: Attachment? = null
|
||||
|
||||
private var clipboardHelper: ClipboardHelper? = null
|
||||
private var mFirstLaunchOfActivity: Boolean = false
|
||||
private var mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) {
|
||||
// Reload the current id from database
|
||||
mEntryViewModel.loadDatabase(mDatabase)
|
||||
}
|
||||
|
||||
private var iconColor: Int = 0
|
||||
private var mIcon: IconImage? = null
|
||||
private var mColorSecondary: Int = 0
|
||||
private var mColorSurface: Int = 0
|
||||
private var mColorOnSurface: Int = 0
|
||||
private var mColorBackground: Int = 0
|
||||
private var mBackgroundColor: Int? = null
|
||||
private var mForegroundColor: Int? = null
|
||||
|
||||
override fun manageDatabaseInfo(): Boolean = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -105,60 +137,221 @@ class EntryActivity : LockingActivity() {
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
mDatabase = Database.getInstance()
|
||||
mReadOnly = mDatabase!!.isReadOnly || mReadOnly
|
||||
|
||||
mShowPassword = !PreferencesUtil.isPasswordMask(this)
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
iconColor = taIconColor.getColor(0, Color.BLACK)
|
||||
taIconColor.recycle()
|
||||
|
||||
// Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set
|
||||
invalidateOptionsMenu()
|
||||
|
||||
// Get views
|
||||
footer = findViewById(R.id.activity_entry_footer)
|
||||
container = findViewById(R.id.activity_entry_container)
|
||||
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)
|
||||
entryContentsView = findViewById(R.id.entry_contents)
|
||||
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
||||
entryContentsView?.setAttachmentCipherKey(mDatabase)
|
||||
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)
|
||||
container?.applyWindowInsets(EnumSet.of(
|
||||
WindowInsetPosition.TOP_MARGINS,
|
||||
WindowInsetPosition.BOTTOM_MARGINS,
|
||||
WindowInsetPosition.START_MARGINS,
|
||||
WindowInsetPosition.END_MARGINS,
|
||||
))
|
||||
}
|
||||
|
||||
// Empty title
|
||||
collapsingToolbarLayout?.title = " "
|
||||
toolbar?.title = " "
|
||||
|
||||
// Retrieve the textColor to tint the toolbar
|
||||
val taColorSecondary = theme.obtainStyledAttributes(intArrayOf(R.attr.colorSecondary))
|
||||
val taColorSurface = theme.obtainStyledAttributes(intArrayOf(R.attr.colorSurface))
|
||||
val taColorOnSurface = theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnSurface))
|
||||
val taColorBackground = theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
|
||||
mColorSecondary = taColorSecondary.getColor(0, Color.BLACK)
|
||||
mColorSurface = taColorSurface.getColor(0, Color.BLACK)
|
||||
mColorOnSurface = taColorOnSurface.getColor(0, Color.BLACK)
|
||||
mColorBackground = taColorBackground.getColor(0, Color.BLACK)
|
||||
taColorSecondary.recycle()
|
||||
taColorSurface.recycle()
|
||||
taColorOnSurface.recycle()
|
||||
taColorBackground.recycle()
|
||||
|
||||
// Init Tags adapter
|
||||
tagsAdapter = TagsAdapter(this)
|
||||
tagsListView?.apply {
|
||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||
adapter = tagsAdapter
|
||||
}
|
||||
|
||||
// Init content tab
|
||||
entryContentTab?.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
mEntryViewModel.selectSection(EntryViewModel.EntrySection.
|
||||
getEntrySectionByPosition(tab?.position ?: 0)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
||||
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) {}
|
||||
})
|
||||
|
||||
// Get Entry from UUID
|
||||
try {
|
||||
intent.getParcelableExtraCompat<NodeId<UUID>>(KEY_ENTRY)?.let { mainEntryId ->
|
||||
intent.removeExtra(KEY_ENTRY)
|
||||
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
|
||||
intent.removeExtra(KEY_ENTRY_HISTORY_POSITION)
|
||||
|
||||
mEntryViewModel.loadEntry(mDatabase, mainEntryId, historyPosition)
|
||||
}
|
||||
} catch (e: ClassCastException) {
|
||||
Log.e(TAG, "Unable to retrieve the entry key")
|
||||
}
|
||||
|
||||
// Init SAF manager
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildCreateDocument { createdFileUri ->
|
||||
mAttachmentSelected?.let { attachment ->
|
||||
if (createdFileUri != null) {
|
||||
mAttachmentFileBinderManager
|
||||
?.startDownloadAttachment(createdFileUri, attachment)
|
||||
}
|
||||
mAttachmentSelected = null
|
||||
}
|
||||
}
|
||||
// Init attachment service binder manager
|
||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||
|
||||
lockView?.setOnClickListener {
|
||||
lockAndExit()
|
||||
}
|
||||
|
||||
// Focus view to reinitialize timeout
|
||||
coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
|
||||
mEntryViewModel.sectionSelected.observe(this) { entrySection ->
|
||||
entryContentTab?.getTabAt(entrySection.position)?.select()
|
||||
}
|
||||
|
||||
// Init the clipboard helper
|
||||
clipboardHelper = ClipboardHelper(this)
|
||||
mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true
|
||||
mEntryViewModel.entryInfoHistory.observe(this) { entryInfoHistory ->
|
||||
if (entryInfoHistory != null) {
|
||||
this.mMainEntryId = entryInfoHistory.mainEntryId
|
||||
|
||||
// Init attachment service binder manager
|
||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||
// Manage history position
|
||||
val historyPosition = entryInfoHistory.historyPosition
|
||||
this.mHistoryPosition = historyPosition
|
||||
val entryIsHistory = historyPosition > -1
|
||||
this.mEntryIsHistory = entryIsHistory
|
||||
// Assign history dedicated view
|
||||
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
|
||||
// TODO History badge
|
||||
/*
|
||||
if (entryIsHistory) {
|
||||
}*/
|
||||
|
||||
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
|
||||
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
|
||||
// Close the current activity after an history action
|
||||
if (result.isSuccess)
|
||||
finish()
|
||||
val entryInfo = entryInfoHistory.entryInfo
|
||||
// Manage entry copy to start notification if allowed (at the first start)
|
||||
if (savedInstanceState == null) {
|
||||
// Manage entry to launch copying notification if allowed
|
||||
ClipboardEntryNotificationService.checkAndLaunchNotification(this, entryInfo)
|
||||
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
||||
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
|
||||
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
|
||||
}
|
||||
}
|
||||
ACTION_DATABASE_RELOAD_TASK -> {
|
||||
// Close the current activity
|
||||
this.showActionErrorIfNeeded(result)
|
||||
finish()
|
||||
// Assign title icon
|
||||
mIcon = entryInfo.icon
|
||||
// Assign title text
|
||||
val entryTitle =
|
||||
entryInfo.title.ifEmpty { entryInfo.id.asHexString() }
|
||||
collapsingToolbarLayout?.title = entryTitle
|
||||
toolbar?.title = entryTitle
|
||||
// Assign tags
|
||||
val tags = entryInfo.tags
|
||||
tagsListView?.visibility = if (tags.isEmpty()) View.GONE else View.VISIBLE
|
||||
tagsAdapter?.setTags(tags)
|
||||
// Assign colors
|
||||
val showEntryColors = PreferencesUtil.showEntryColors(this)
|
||||
mBackgroundColor = if (showEntryColors) entryInfo.backgroundColor else null
|
||||
mForegroundColor = if (showEntryColors) entryInfo.foregroundColor else null
|
||||
|
||||
loadingView?.hideByFading()
|
||||
mEntryLoaded = true
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
// Refresh Menu
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement ->
|
||||
if (otpElement == null) {
|
||||
entryProgress?.visibility = View.GONE
|
||||
} else when (otpElement.type) {
|
||||
// Only add token if HOTP
|
||||
OtpType.HOTP -> {
|
||||
entryProgress?.visibility = View.GONE
|
||||
}
|
||||
// Refresh view if TOTP
|
||||
OtpType.TOTP -> {
|
||||
entryProgress?.apply {
|
||||
max = otpElement.period
|
||||
setProgressCompat(otpElement.secondsRemaining, true)
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
coordinatorLayout?.showActionErrorIfNeeded(result)
|
||||
}
|
||||
|
||||
mEntryViewModel.attachmentSelected.observe(this) { attachmentSelected ->
|
||||
mAttachmentSelected = attachmentSelected
|
||||
mExternalFileHelper?.createDocument(attachmentSelected.name)
|
||||
}
|
||||
|
||||
mEntryViewModel.historySelected.observe(this) { historySelected ->
|
||||
mDatabase?.let { database ->
|
||||
launch(
|
||||
activity = this,
|
||||
database = database,
|
||||
entryId = historySelected.nodeId,
|
||||
historyPosition = historySelected.historyPosition,
|
||||
activityResultLauncher = mEntryActivityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun finishActivityIfReloadRequested(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun viewToInvalidateTimeout(): View? {
|
||||
return coordinatorLayout
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
mEntryViewModel.loadDatabase(database)
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
super.onDatabaseActionFinished(database, actionTask, result)
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
|
||||
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
|
||||
// Close the current activity after an history action
|
||||
if (result.isSuccess)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
coordinatorLayout?.showActionErrorIfNeeded(result)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -171,63 +364,19 @@ class EntryActivity : LockingActivity() {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
// Get Entry from UUID
|
||||
try {
|
||||
val keyEntry: NodeId<UUID>? = intent.getParcelableExtra(KEY_ENTRY)
|
||||
if (keyEntry != null) {
|
||||
mEntry = mDatabase?.getEntryById(keyEntry)
|
||||
mEntryLastVersion = mEntry
|
||||
}
|
||||
} catch (e: ClassCastException) {
|
||||
Log.e(TAG, "Unable to retrieve the entry key")
|
||||
}
|
||||
|
||||
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, mEntryHistoryPosition)
|
||||
mEntryHistoryPosition = historyPosition
|
||||
if (historyPosition >= 0) {
|
||||
mIsHistory = true
|
||||
mEntry = mEntry?.getHistory()?.get(historyPosition)
|
||||
}
|
||||
|
||||
if (mEntry == null) {
|
||||
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
// Update last access time.
|
||||
mEntry?.touch(modified = false, touchParents = false)
|
||||
|
||||
mEntry?.let { entry ->
|
||||
// Fill data in resume to update from EntryEditActivity
|
||||
fillEntryDataInContentsView(entry)
|
||||
// Refresh Menu
|
||||
invalidateOptionsMenu()
|
||||
|
||||
val entryInfo = entry.getEntryInfo(mDatabase)
|
||||
// Manage entry copy to start notification if allowed
|
||||
if (mFirstLaunchOfActivity) {
|
||||
// Manage entry to launch copying notification if allowed
|
||||
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
||||
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
||||
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
|
||||
MagikIME.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mAttachmentFileBinderManager?.apply {
|
||||
registerProgressTask()
|
||||
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
||||
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
||||
if (entryAttachmentState.streamDirection != StreamDirection.UPLOAD) {
|
||||
entryContentsView?.putAttachment(entryAttachmentState)
|
||||
}
|
||||
mEntryViewModel.onAttachmentAction(entryAttachmentState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mFirstLaunchOfActivity = false
|
||||
// Keep the screen on
|
||||
if (PreferencesUtil.isKeepScreenOnEnabled(this)) {
|
||||
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -236,223 +385,90 @@ class EntryActivity : LockingActivity() {
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun fillEntryDataInContentsView(entry: Entry) {
|
||||
|
||||
val entryInfo = entry.getEntryInfo(mDatabase)
|
||||
|
||||
// Assign title icon
|
||||
titleIconView?.let { iconView ->
|
||||
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, iconColor)
|
||||
}
|
||||
|
||||
// Assign title text
|
||||
val entryTitle = entryInfo.title
|
||||
collapsingToolbarLayout?.title = entryTitle
|
||||
toolbar?.title = entryTitle
|
||||
|
||||
// Assign basic fields
|
||||
entryContentsView?.assignUserName(entryInfo.username) {
|
||||
clipboardHelper?.timeoutCopyToClipboard(entryInfo.username,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_user_name)))
|
||||
}
|
||||
|
||||
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
||||
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
|
||||
val allowCopyPasswordAndProtectedFields =
|
||||
PreferencesUtil.allowCopyPasswordAndProtectedFields(this)
|
||||
|
||||
val showWarningClipboardDialogOnClickListener = View.OnClickListener {
|
||||
AlertDialog.Builder(this@EntryActivity)
|
||||
.setMessage(getString(R.string.allow_copy_password_warning) +
|
||||
"\n\n" +
|
||||
getString(R.string.clipboard_warning))
|
||||
.create().apply {
|
||||
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) { dialog, _ ->
|
||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true)
|
||||
dialog.dismiss()
|
||||
fillEntryDataInContentsView(entry)
|
||||
}
|
||||
setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ ->
|
||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, false)
|
||||
dialog.dismiss()
|
||||
fillEntryDataInContentsView(entry)
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) {
|
||||
View.OnClickListener {
|
||||
clipboardHelper?.timeoutCopyToClipboard(entryInfo.password,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_password)))
|
||||
}
|
||||
private fun applyToolbarColors() {
|
||||
collapsingToolbarLayout?.setBackgroundColor(mBackgroundColor ?: mColorSurface)
|
||||
collapsingToolbarLayout?.contentScrim = ColorDrawable(mBackgroundColor ?: mColorSurface)
|
||||
val backgroundDarker = if (mBackgroundColor != null) {
|
||||
ColorUtils.blendARGB(mBackgroundColor!!, Color.WHITE, 0.1f)
|
||||
} else {
|
||||
// If dialog not already shown
|
||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||
showWarningClipboardDialogOnClickListener
|
||||
} else {
|
||||
null
|
||||
}
|
||||
mColorBackground
|
||||
}
|
||||
entryContentsView?.assignPassword(entryInfo.password,
|
||||
allowCopyPasswordAndProtectedFields,
|
||||
onPasswordCopyClickListener)
|
||||
|
||||
//Assign OTP field
|
||||
entry.getOtpElement()?.let { otpElement ->
|
||||
entryContentsView?.assignOtp(otpElement, entryProgress) {
|
||||
clipboardHelper?.timeoutCopyToClipboard(
|
||||
otpElement.token,
|
||||
getString(R.string.copy_field, getString(R.string.entry_otp))
|
||||
titleIconView?.background?.colorFilter = BlendModeColorFilterCompat
|
||||
.createBlendModeColorFilterCompat(backgroundDarker, BlendModeCompat.SRC_IN)
|
||||
mIcon?.let { icon ->
|
||||
titleIconView?.let { iconView ->
|
||||
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(
|
||||
iconView,
|
||||
icon,
|
||||
mForegroundColor ?: mColorSecondary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
entryContentsView?.assignURL(entryInfo.url)
|
||||
entryContentsView?.assignNotes(entryInfo.notes)
|
||||
|
||||
// Assign custom fields
|
||||
if (mDatabase?.allowEntryCustomFields() == true) {
|
||||
entryContentsView?.clearExtraFields()
|
||||
entryInfo.customFields.forEach { field ->
|
||||
val label = field.name
|
||||
// OTP field is already managed in dedicated view
|
||||
if (label != OtpEntryFields.OTP_TOKEN_FIELD) {
|
||||
val value = field.protectedValue
|
||||
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
||||
if (allowCopyProtectedField) {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
|
||||
clipboardHelper?.timeoutCopyToClipboard(
|
||||
value.toString(),
|
||||
getString(R.string.copy_field, label)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// If dialog not already shown
|
||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
|
||||
} else {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
|
||||
|
||||
// Manage attachments
|
||||
entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
|
||||
createDocument(this, attachmentItem.name)?.let { requestCode ->
|
||||
mAttachmentsToDownload[requestCode] = attachmentItem
|
||||
}
|
||||
}
|
||||
|
||||
// Assign dates
|
||||
entryContentsView?.assignCreationDate(entryInfo.creationTime)
|
||||
entryContentsView?.assignModificationDate(entryInfo.lastModificationTime)
|
||||
entryContentsView?.setExpires(entryInfo.expires, entryInfo.expiryTime)
|
||||
|
||||
// Manage history
|
||||
historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE
|
||||
if (mIsHistory) {
|
||||
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
||||
taColorAccent.recycle()
|
||||
}
|
||||
entryContentsView?.assignHistory(entry.getHistory()) { historyItem, position ->
|
||||
launch(this, historyItem, mReadOnly, position)
|
||||
}
|
||||
|
||||
// Assign special data
|
||||
entryContentsView?.assignUUID(entry.nodeId.id)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
when (requestCode) {
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE ->
|
||||
// Not directly get the entry from intent data but from database
|
||||
mEntry?.let {
|
||||
fillEntryDataInContentsView(it)
|
||||
}
|
||||
}
|
||||
|
||||
onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
||||
if (createdFileUri != null) {
|
||||
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
|
||||
mAttachmentFileBinderManager
|
||||
?.startDownloadAttachment(createdFileUri, attachmentToDownload)
|
||||
}
|
||||
}
|
||||
}
|
||||
toolbar?.changeControlColor(mForegroundColor ?: mColorOnSurface)
|
||||
collapsingToolbarLayout?.changeTitleColor(mForegroundColor ?: mColorOnSurface)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
if (mEntryLoaded) {
|
||||
val inflater = menuInflater
|
||||
|
||||
val inflater = menuInflater
|
||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||
inflater.inflate(R.menu.entry, menu)
|
||||
inflater.inflate(R.menu.database, menu)
|
||||
if (mIsHistory && !mReadOnly) {
|
||||
inflater.inflate(R.menu.entry_history, menu)
|
||||
}
|
||||
if (mIsHistory || mReadOnly) {
|
||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||
menu.findItem(R.id.menu_edit)?.isVisible = false
|
||||
}
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
menu.findItem(R.id.menu_reload_database)?.isVisible = false
|
||||
}
|
||||
inflater.inflate(R.menu.entry, menu)
|
||||
inflater.inflate(R.menu.database, menu)
|
||||
|
||||
val gotoUrl = menu.findItem(R.id.menu_goto_url)
|
||||
gotoUrl?.apply {
|
||||
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
|
||||
// so mEntry may not be set
|
||||
if (mEntry == null) {
|
||||
isVisible = false
|
||||
} else {
|
||||
if (mEntry?.url?.isEmpty() != false) {
|
||||
// disable button if url is not available
|
||||
isVisible = false
|
||||
}
|
||||
if (mEntryIsHistory && !mDatabaseReadOnly) {
|
||||
inflater.inflate(R.menu.entry_history, menu)
|
||||
}
|
||||
|
||||
// Show education views
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
performedNextEducation(menu)
|
||||
}
|
||||
}
|
||||
|
||||
// Show education views
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(EntryActivityEducation(this), menu) }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
||||
menu: Menu) {
|
||||
val entryFieldCopyView = entryContentsView?.firstEntryFieldCopyView()
|
||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||
if (mEntryIsHistory || mDatabaseReadOnly) {
|
||||
menu?.findItem(R.id.menu_save_database)?.isVisible = false
|
||||
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||
menu?.findItem(R.id.menu_edit)?.isVisible = false
|
||||
}
|
||||
if (!mMergeDataAllowed) {
|
||||
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||
}
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||
menu?.findItem(R.id.menu_reload_database)?.isVisible = false
|
||||
}
|
||||
applyToolbarColors()
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
private fun performedNextEducation(menu: Menu) {
|
||||
val entryFragment = supportFragmentManager.findFragmentByTag(ENTRY_FRAGMENT_TAG)
|
||||
as? EntryFragment?
|
||||
val entryFieldCopyView: View? = entryFragment?.firstEntryFieldCopyView()
|
||||
val entryCopyEducationPerformed = entryFieldCopyView != null
|
||||
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
||||
entryFieldCopyView,
|
||||
{
|
||||
val appNameString = getString(R.string.app_name)
|
||||
clipboardHelper?.timeoutCopyToClipboard(appNameString,
|
||||
getString(R.string.copy_field, appNameString))
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryActivityEducation, menu)
|
||||
})
|
||||
&& mEntryActivityEducation.checkAndPerformedEntryCopyEducation(
|
||||
entryFieldCopyView,
|
||||
{
|
||||
entryFragment.launchEntryCopyEducationAction()
|
||||
},
|
||||
{
|
||||
performedNextEducation(menu)
|
||||
})
|
||||
|
||||
if (!entryCopyEducationPerformed) {
|
||||
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
|
||||
// entryEditEducationPerformed
|
||||
menuEditView != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
|
||||
menuEditView != null && mEntryActivityEducation.checkAndPerformedEntryEditEducation(
|
||||
menuEditView,
|
||||
{
|
||||
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryActivityEducation, menu)
|
||||
performedNextEducation(menu)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -460,65 +476,53 @@ class EntryActivity : LockingActivity() {
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_contribute -> {
|
||||
MenuUtil.onContributionItemSelected(this)
|
||||
return true
|
||||
}
|
||||
R.id.menu_edit -> {
|
||||
mEntry?.let {
|
||||
EntryEditActivity.launch(this@EntryActivity, it)
|
||||
mDatabase?.let { database ->
|
||||
mMainEntryId?.let { entryId ->
|
||||
EntryEditActivity.launch(
|
||||
activity = this,
|
||||
database = database,
|
||||
registrationType = EntryEditActivity.RegistrationType.UPDATE,
|
||||
nodeId = entryId,
|
||||
activityResultLauncher = mEntryActivityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.menu_goto_url -> {
|
||||
var url: String = mEntry?.url ?: ""
|
||||
|
||||
// Default http:// if no protocol specified
|
||||
if (!url.contains("://")) {
|
||||
url = "http://$url"
|
||||
}
|
||||
|
||||
UriUtil.gotoUrl(this, url)
|
||||
return true
|
||||
}
|
||||
R.id.menu_restore_entry_history -> {
|
||||
mEntryLastVersion?.let { mainEntry ->
|
||||
mProgressDatabaseTaskProvider?.startDatabaseRestoreEntryHistory(
|
||||
mainEntry,
|
||||
mEntryHistoryPosition,
|
||||
!mReadOnly && mAutoSaveEnable)
|
||||
mMainEntryId?.let { mainEntryId ->
|
||||
restoreEntryHistory(
|
||||
mainEntryId,
|
||||
mHistoryPosition)
|
||||
}
|
||||
}
|
||||
R.id.menu_delete_entry_history -> {
|
||||
mEntryLastVersion?.let { mainEntry ->
|
||||
mProgressDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(
|
||||
mainEntry,
|
||||
mEntryHistoryPosition,
|
||||
!mReadOnly && mAutoSaveEnable)
|
||||
mMainEntryId?.let { mainEntryId ->
|
||||
deleteEntryHistory(
|
||||
mainEntryId,
|
||||
mHistoryPosition)
|
||||
}
|
||||
}
|
||||
R.id.menu_save_database -> {
|
||||
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
|
||||
saveDatabase()
|
||||
}
|
||||
R.id.menu_merge_database -> {
|
||||
mergeDatabase()
|
||||
}
|
||||
R.id.menu_reload_database -> {
|
||||
mProgressDatabaseTaskProvider?.startDatabaseReload(false)
|
||||
reloadDatabase()
|
||||
}
|
||||
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
outState.putBoolean(KEY_FIRST_LAUNCH_ACTIVITY, mFirstLaunchOfActivity)
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
// Transit data in previous Activity after an update
|
||||
Intent().apply {
|
||||
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry)
|
||||
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, this)
|
||||
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
|
||||
setResult(RESULT_OK, this)
|
||||
}
|
||||
super.finish()
|
||||
}
|
||||
@@ -526,19 +530,30 @@ class EntryActivity : LockingActivity() {
|
||||
companion object {
|
||||
private val TAG = EntryActivity::class.java.name
|
||||
|
||||
private const val KEY_FIRST_LAUNCH_ACTIVITY = "KEY_FIRST_LAUNCH_ACTIVITY"
|
||||
|
||||
const val KEY_ENTRY = "KEY_ENTRY"
|
||||
const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION"
|
||||
|
||||
fun launch(activity: Activity, entry: Entry, readOnly: Boolean, historyPosition: Int? = null) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, entry.nodeId)
|
||||
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
||||
if (historyPosition != null)
|
||||
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
||||
activity.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
||||
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
|
||||
|
||||
/**
|
||||
* Open standard or history Entry activity
|
||||
*/
|
||||
fun launch(
|
||||
activity: Activity,
|
||||
database: ContextualDatabase,
|
||||
entryId: NodeId<UUID>,
|
||||
historyPosition: Int? = null,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>
|
||||
) {
|
||||
if (database.loaded) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, entryId)
|
||||
historyPosition?.let {
|
||||
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
||||
}
|
||||
activityResultLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
|
||||
/**
|
||||
* Activity to search or select entry in database,
|
||||
* Commonly used with Magikeyboard
|
||||
*/
|
||||
class EntrySelectionLauncherActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
var sharedWebDomain: String? = null
|
||||
var otpString: String? = null
|
||||
|
||||
when (intent?.action) {
|
||||
Intent.ACTION_SEND -> {
|
||||
if ("text/plain" == intent.type) {
|
||||
// Retrieve web domain or OTP
|
||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
|
||||
if (OtpEntryFields.isOTPUri(extra))
|
||||
otpString = extra
|
||||
else
|
||||
sharedWebDomain = Uri.parse(extra).host
|
||||
}
|
||||
}
|
||||
}
|
||||
Intent.ACTION_VIEW -> {
|
||||
// Retrieve OTP
|
||||
intent.dataString?.let { extra ->
|
||||
if (OtpEntryFields.isOTPUri(extra))
|
||||
otpString = extra
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
|
||||
// Build domain search param
|
||||
val searchInfo = SearchInfo().apply {
|
||||
this.webDomain = sharedWebDomain
|
||||
this.otpString = otpString
|
||||
}
|
||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||
searchInfo.webDomain = concreteWebDomain
|
||||
launch(searchInfo)
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun launch(searchInfo: SearchInfo) {
|
||||
|
||||
if (!searchInfo.containsOnlyNullValues()) {
|
||||
// Setting to integrate Magikeyboard
|
||||
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
|
||||
|
||||
// If database is open
|
||||
val database = Database.getInstance()
|
||||
val readOnly = database.isReadOnly
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
database,
|
||||
searchInfo,
|
||||
{ items ->
|
||||
// Items found
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
GroupActivity.launchForSaveResult(this,
|
||||
searchInfo,
|
||||
false)
|
||||
} else {
|
||||
Toast.makeText(applicationContext,
|
||||
R.string.autofill_read_only_save,
|
||||
Toast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
} else if (searchShareForMagikeyboard) {
|
||||
if (items.size == 1) {
|
||||
// Automatically populate keyboard
|
||||
val entryPopulate = items[0]
|
||||
populateKeyboardAndMoveAppToBackground(this,
|
||||
entryPopulate,
|
||||
intent)
|
||||
} else {
|
||||
// Select the one we want
|
||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||
readOnly,
|
||||
searchInfo,
|
||||
true)
|
||||
}
|
||||
} else {
|
||||
GroupActivity.launchForSearchResult(this,
|
||||
readOnly,
|
||||
searchInfo,
|
||||
true)
|
||||
}
|
||||
},
|
||||
{
|
||||
// Show the database UI to select the entry
|
||||
if (searchInfo.otpString != null) {
|
||||
if (!readOnly) {
|
||||
GroupActivity.launchForSaveResult(this,
|
||||
searchInfo,
|
||||
false)
|
||||
} else {
|
||||
Toast.makeText(applicationContext,
|
||||
R.string.autofill_read_only_save,
|
||||
Toast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
} else if (readOnly || searchShareForMagikeyboard) {
|
||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||
readOnly,
|
||||
searchInfo,
|
||||
false)
|
||||
} else {
|
||||
GroupActivity.launchForSaveResult(this,
|
||||
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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
||||
entry: EntryInfo,
|
||||
intent: Intent,
|
||||
toast: Boolean = true) {
|
||||
// Populate Magikeyboard with entry
|
||||
MagikIME.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
||||
// Consume the selection mode
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
activity.moveTaskToBack(true)
|
||||
}
|
||||
@@ -19,7 +19,6 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
@@ -31,50 +30,59 @@ import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||
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.action.ProgressDatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||
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
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
|
||||
import com.kunzisoft.keepass.utils.DexUtil
|
||||
import com.kunzisoft.keepass.utils.MagikeyboardUtil
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
||||
class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
SetMainCredentialDialogFragment.AssignMainCredentialDialogListener {
|
||||
|
||||
// Views
|
||||
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||
private var specialTitle: View? = null
|
||||
private var createDatabaseButtonView: View? = null
|
||||
private var openDatabaseButtonView: View? = null
|
||||
|
||||
private val databaseFilesViewModel: DatabaseFilesViewModel by viewModels()
|
||||
|
||||
private val mFileDatabaseSelectActivityEducation = FileDatabaseSelectActivityEducation(this)
|
||||
|
||||
// Adapter to manage database history list
|
||||
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
|
||||
|
||||
@@ -82,13 +90,19 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
|
||||
private var mDatabaseFileUri: Uri? = null
|
||||
|
||||
private var mSelectFileHelper: SelectFileHelper? = null
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||
override fun manageDatabaseInfo(): Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Enabling/disabling MagikeyboardService is normally done by DexModeReceiver, but this
|
||||
// additional check will allow the keyboard to be reenabled more easily if the app crashes
|
||||
// or is force quit within DeX mode and then the user leaves DeX mode. Without this, the
|
||||
// user would need to enter and exit DeX mode once to reenable the service.
|
||||
MagikeyboardUtil.setEnabled(this, !DexUtil.isDexMode(resources.configuration))
|
||||
|
||||
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
|
||||
|
||||
setContentView(R.layout.activity_file_selection)
|
||||
@@ -98,19 +112,33 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
toolbar.title = ""
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
// Special title
|
||||
specialTitle = findViewById(R.id.file_selection_title_part_3)
|
||||
|
||||
// Create database button
|
||||
createDatabaseButtonView = findViewById(R.id.create_database_button)
|
||||
createDatabaseButtonView?.setOnClickListener { createNewFile() }
|
||||
|
||||
// Open database button
|
||||
mSelectFileHelper = SelectFileHelper(this)
|
||||
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
||||
openDatabaseButtonView?.apply {
|
||||
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
||||
setOnClickListener(it)
|
||||
setOnLongClickListener(it)
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
uri?.let {
|
||||
launchMainCredentialActivityWithPath(uri)
|
||||
}
|
||||
}
|
||||
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
|
||||
mDatabaseFileUri = databaseFileCreatedUri
|
||||
if (mDatabaseFileUri != null) {
|
||||
SetMainCredentialDialogFragment.getInstance(true)
|
||||
.show(supportFragmentManager, "passwordDialog")
|
||||
} else {
|
||||
val error = getString(R.string.error_create_database)
|
||||
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||
Log.e(TAG, error)
|
||||
}
|
||||
}
|
||||
openDatabaseButtonView = findViewById(R.id.open_database_button)
|
||||
openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||
|
||||
// History list
|
||||
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
||||
@@ -124,14 +152,14 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
}
|
||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
||||
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
|
||||
launchPasswordActivity(
|
||||
databaseFileUri,
|
||||
fileDatabaseHistoryEntityToOpen.keyFileUri
|
||||
launchMainCredentialActivity(
|
||||
databaseFileUri,
|
||||
fileDatabaseHistoryEntityToOpen.keyFileUri,
|
||||
fileDatabaseHistoryEntityToOpen.hardwareKey
|
||||
)
|
||||
}
|
||||
}
|
||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
||||
// Remove from app database
|
||||
databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete)
|
||||
true
|
||||
}
|
||||
@@ -141,23 +169,15 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
}
|
||||
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
|
||||
|
||||
// Load default database if not an orientation change
|
||||
if (!(savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(EXTRA_STAY)
|
||||
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
|
||||
val databasePath = PreferencesUtil.getDefaultDatabasePath(this)
|
||||
|
||||
UriUtil.parse(databasePath)?.let { databaseFileUri ->
|
||||
launchPasswordActivityWithPath(databaseFileUri)
|
||||
} ?: run {
|
||||
Log.i(TAG, "No default database to prepare")
|
||||
}
|
||||
// Load default database the first time
|
||||
databaseFilesViewModel.doForDefaultDatabase { databaseFileUri ->
|
||||
launchMainCredentialActivityWithPath(databaseFileUri)
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -171,8 +191,6 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
|
||||
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
|
||||
}
|
||||
GroupActivity.launch(this@FileDatabaseSelectActivity,
|
||||
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity))
|
||||
}
|
||||
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
|
||||
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
|
||||
@@ -185,10 +203,10 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
}
|
||||
}
|
||||
}
|
||||
databaseFilesViewModel.consumeAction()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to observe database action", e)
|
||||
}
|
||||
databaseFilesViewModel.consumeAction()
|
||||
}
|
||||
|
||||
// Observe default database
|
||||
@@ -197,34 +215,35 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
mAdapterDatabaseHistory?.setDefaultDatabase(it)
|
||||
}
|
||||
|
||||
// Attach the dialog thread to this activity
|
||||
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
|
||||
onActionFinish = { actionTask, result ->
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_CREATE_TASK -> {
|
||||
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
|
||||
val mainCredential = result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY) ?: MainCredential()
|
||||
databaseFilesViewModel.addDatabaseFile(databaseUri, mainCredential.keyFileUri)
|
||||
}
|
||||
}
|
||||
ACTION_DATABASE_LOAD_TASK -> {
|
||||
val database = Database.getInstance()
|
||||
if (result.isSuccess
|
||||
&& database.loaded) {
|
||||
launchGroupActivity(database)
|
||||
} else {
|
||||
var resultError = ""
|
||||
val resultMessage = result.message
|
||||
// Show error message
|
||||
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
||||
resultError = "$resultError $resultMessage"
|
||||
}
|
||||
Log.e(TAG, resultError)
|
||||
Snackbar.make(coordinatorLayout,
|
||||
resultError,
|
||||
Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
}
|
||||
// Remove all the remember locations if needed
|
||||
if (PreferencesUtil.rememberDatabaseLocations(applicationContext).not()) {
|
||||
FileDatabaseHistoryAction.getInstance(applicationContext)
|
||||
.deleteAll()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
if (result.isSuccess) {
|
||||
// Launch activity
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_CREATE_TASK -> {
|
||||
GroupActivity.launch(
|
||||
this@FileDatabaseSelectActivity,
|
||||
database,
|
||||
false
|
||||
)
|
||||
coordinatorLayout.showActionErrorIfNeeded(result)
|
||||
}
|
||||
ACTION_DATABASE_LOAD_TASK -> {
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,8 +253,9 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
* Create a new file by calling the content provider
|
||||
*/
|
||||
private fun createNewFile() {
|
||||
createDocument(this, getString(R.string.database_file_name_default) +
|
||||
getString(R.string.database_file_extension_default), "application/x-keepass")
|
||||
mExternalFileHelper?.createDocument(
|
||||
getString(R.string.database_file_name_default) +
|
||||
getString(R.string.database_file_extension_default))
|
||||
}
|
||||
|
||||
private fun fileNoFoundAction(e: FileNotFoundException) {
|
||||
@@ -244,38 +264,76 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
|
||||
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
||||
PasswordActivity.launch(this,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
{ exception ->
|
||||
fileNoFoundAction(exception)
|
||||
private fun launchMainCredentialActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
|
||||
try {
|
||||
EntrySelectionHelper.doSpecialAction(
|
||||
intent = this.intent,
|
||||
defaultAction = {
|
||||
MainCredentialActivity.launch(
|
||||
activity = this,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey
|
||||
)
|
||||
},
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() })
|
||||
searchAction = { searchInfo ->
|
||||
MainCredentialActivity.launchForSearchResult(
|
||||
activity = this,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
selectionAction = { intentSenderMode, typeMode, searchInfo ->
|
||||
MainCredentialActivity.launchForSelection(
|
||||
activity = this,
|
||||
activityResultLauncher = if (intentSenderMode)
|
||||
mCredentialActivityResultLauncher else null,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey,
|
||||
typeMode = typeMode,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
registrationAction = { intentSenderMode, typeMode, registerInfo ->
|
||||
MainCredentialActivity.launchForRegistration(
|
||||
activity = this,
|
||||
activityResultLauncher = if (intentSenderMode)
|
||||
mCredentialActivityResultLauncher else null,
|
||||
databaseFile = databaseUri,
|
||||
keyFile = keyFile,
|
||||
hardwareKey = hardwareKey,
|
||||
typeMode = typeMode,
|
||||
registerInfo = registerInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
}
|
||||
)
|
||||
} catch (e: FileNotFoundException) {
|
||||
fileNoFoundAction(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchGroupActivity(database: Database) {
|
||||
GroupActivity.launch(this,
|
||||
database.isReadOnly,
|
||||
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
||||
if (database.loaded) {
|
||||
GroupActivity.launch(this,
|
||||
database,
|
||||
{ onValidateSpecialMode() },
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() })
|
||||
{ onLaunchActivitySpecialMode() },
|
||||
mCredentialActivityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onValidateSpecialMode() {
|
||||
super.onValidateSpecialMode()
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onCancelSpecialMode() {
|
||||
super.onCancelSpecialMode()
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
||||
launchPasswordActivity(databaseUri, null)
|
||||
private fun launchMainCredentialActivityWithPath(databaseUri: Uri) {
|
||||
launchMainCredentialActivity(databaseUri, null, null)
|
||||
// Delete flickering for kitkat <=
|
||||
@Suppress("DEPRECATION")
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
||||
overridePendingTransition(0, 0)
|
||||
}
|
||||
@@ -283,16 +341,13 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// Define special title
|
||||
specialTitle?.isVisible = this.isContributingUser()
|
||||
|
||||
// Show open and create button or special mode
|
||||
when (mSpecialMode) {
|
||||
SpecialMode.DEFAULT -> {
|
||||
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
||||
// There is an activity which can handle this intent.
|
||||
createDatabaseButtonView?.visibility = View.VISIBLE
|
||||
} else{
|
||||
// No Activity found that can handle this intent.
|
||||
createDatabaseButtonView?.visibility = View.GONE
|
||||
}
|
||||
createDatabaseButtonView?.visibility = View.VISIBLE
|
||||
}
|
||||
else -> {
|
||||
// Disable create button if in selection mode or request for autofill
|
||||
@@ -300,48 +355,25 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
val database = Database.getInstance()
|
||||
if (database.loaded) {
|
||||
launchGroupActivity(database)
|
||||
// Show recent files if allowed
|
||||
if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
|
||||
databaseFilesViewModel.loadListOfDatabases()
|
||||
} else {
|
||||
// Construct adapter with listeners
|
||||
if (PreferencesUtil.showRecentFiles(this)) {
|
||||
databaseFilesViewModel.loadListOfDatabases()
|
||||
} else {
|
||||
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
// Register progress task
|
||||
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
// Unregister progress task
|
||||
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
// only to keep the current activity
|
||||
outState.putBoolean(EXTRA_STAY, true)
|
||||
// to retrieve the URI of a created database after an orientation change
|
||||
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
||||
}
|
||||
|
||||
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
|
||||
|
||||
try {
|
||||
mDatabaseFileUri?.let { databaseUri ->
|
||||
|
||||
// Create the new database
|
||||
mProgressDatabaseTaskProvider?.startDatabaseCreate(
|
||||
databaseUri,
|
||||
mainCredential
|
||||
)
|
||||
mDatabaseViewModel.createDatabase(databaseUri, mainCredential)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
val error = getString(R.string.error_create_database_file)
|
||||
@@ -352,79 +384,53 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
|
||||
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||
if (uri != null) {
|
||||
launchPasswordActivityWithPath(uri)
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve the created URI from the file manager
|
||||
onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
|
||||
mDatabaseFileUri = databaseFileCreatedUri
|
||||
if (mDatabaseFileUri != null) {
|
||||
AssignMasterKeyDialogFragment.getInstance(true)
|
||||
.show(supportFragmentManager, "passwordDialog")
|
||||
} else {
|
||||
val error = getString(R.string.error_create_database)
|
||||
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||
Log.e(TAG, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||
MenuUtil.defaultMenuInflater(menuInflater, menu)
|
||||
MenuUtil.defaultMenuInflater(this, menuInflater, menu)
|
||||
}
|
||||
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
performedNextEducation()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
|
||||
private fun performedNextEducation() {
|
||||
// If no recent files
|
||||
val createDatabaseEducationPerformed =
|
||||
createDatabaseButtonView != null
|
||||
&& createDatabaseButtonView!!.visibility == View.VISIBLE
|
||||
&& mAdapterDatabaseHistory != null
|
||||
&& mAdapterDatabaseHistory!!.itemCount == 0
|
||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
||||
&& mFileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
||||
createDatabaseButtonView!!,
|
||||
{
|
||||
createNewFile()
|
||||
},
|
||||
{
|
||||
// But if the user cancel, it can also select a database
|
||||
performedNextEducation(fileDatabaseSelectActivityEducation)
|
||||
performedNextEducation()
|
||||
})
|
||||
if (!createDatabaseEducationPerformed) {
|
||||
// selectDatabaseEducationPerformed
|
||||
openDatabaseButtonView != null
|
||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
||||
openDatabaseButtonView!!,
|
||||
{tapTargetView ->
|
||||
tapTargetView?.let {
|
||||
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
|
||||
}
|
||||
},
|
||||
{}
|
||||
)
|
||||
&& mFileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
||||
openDatabaseButtonView!!,
|
||||
{ tapTargetView ->
|
||||
tapTargetView?.let {
|
||||
mExternalFileHelper?.openDocument()
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -433,7 +439,6 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
companion object {
|
||||
|
||||
private const val TAG = "FileDbSelectActivity"
|
||||
private const val EXTRA_STAY = "EXTRA_STAY"
|
||||
private const val EXTRA_DATABASE_URI = "EXTRA_DATABASE_URI"
|
||||
|
||||
/*
|
||||
@@ -452,53 +457,36 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
fun launchForSearchResult(context: Context,
|
||||
searchInfo: SearchInfo) {
|
||||
EntrySelectionHelper.startActivityForSearchModeResult(context,
|
||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
searchInfo)
|
||||
fun launchForSearch(
|
||||
context: Context,
|
||||
searchInfo: SearchInfo
|
||||
) {
|
||||
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||
context = context,
|
||||
intent = Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Save Launch
|
||||
* Selection Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
fun launchForSaveResult(context: Context,
|
||||
searchInfo: SearchInfo) {
|
||||
EntrySelectionHelper.startActivityForSaveModeResult(context,
|
||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
searchInfo)
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Keyboard Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
fun launchForKeyboardSelectionResult(activity: Activity,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(activity,
|
||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||
searchInfo)
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Autofill Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
fun launchForAutofillResult(activity: Activity,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
AutofillHelper.startActivityForAutofillResult(activity,
|
||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||
autofillComponent,
|
||||
searchInfo)
|
||||
fun launchForSelection(
|
||||
context: Context,
|
||||
typeMode: TypeMode,
|
||||
searchInfo: SearchInfo? = null,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
) {
|
||||
EntrySelectionHelper.startActivityForSelectionModeResult(
|
||||
context = context,
|
||||
intent = Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
searchInfo = searchInfo,
|
||||
typeMode = typeMode,
|
||||
activityResultLauncher = activityResultLauncher
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -506,11 +494,19 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
* Registration Launch
|
||||
* -------------------------
|
||||
*/
|
||||
fun launchForRegistration(context: Context,
|
||||
registerInfo: RegisterInfo? = null) {
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
|
||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
registerInfo)
|
||||
fun launchForRegistration(
|
||||
context: Context,
|
||||
typeMode: TypeMode,
|
||||
registerInfo: RegisterInfo? = null,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
) {
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||
context = context,
|
||||
intent = Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
registerInfo = registerInfo,
|
||||
typeMode = typeMode,
|
||||
activityResultLauncher = activityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,29 +27,41 @@ import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.commit
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.IconEditDialogFragment
|
||||
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||
import com.kunzisoft.keepass.utils.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.view.updateLockPaddingStart
|
||||
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 : LockingActivity() {
|
||||
class IconPickerActivity : DatabaseLockActivity() {
|
||||
|
||||
private lateinit var toolbar: Toolbar
|
||||
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||
@@ -64,17 +76,15 @@ class IconPickerActivity : LockingActivity() {
|
||||
private var mCustomIconsSelectionMode = false
|
||||
private var mIconsSelected: List<IconImageCustom> = ArrayList()
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
private var mSelectFileHelper: SelectFileHelper? = null
|
||||
override fun manageDatabaseInfo(): Boolean = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_icon_picker)
|
||||
|
||||
mDatabase = Database.getInstance()
|
||||
|
||||
toolbar = findViewById(R.id.toolbar)
|
||||
toolbar.title = " "
|
||||
setSupportActionBar(toolbar)
|
||||
@@ -84,25 +94,19 @@ class IconPickerActivity : LockingActivity() {
|
||||
|
||||
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
|
||||
|
||||
uploadButton = findViewById(R.id.icon_picker_upload)
|
||||
if (mDatabase?.allowCustomIcons == true) {
|
||||
uploadButton.setOnClickListener {
|
||||
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
|
||||
}
|
||||
uploadButton.setOnLongClickListener {
|
||||
mSelectFileHelper?.selectFileOnClickViewListener?.onLongClick(it)
|
||||
true
|
||||
}
|
||||
} else {
|
||||
uploadButton.visibility = View.GONE
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
addCustomIcon(uri)
|
||||
}
|
||||
|
||||
uploadButton = findViewById(R.id.icon_picker_upload)
|
||||
|
||||
lockView = findViewById(R.id.lock_button)
|
||||
lockView?.setOnClickListener {
|
||||
lockAndExit()
|
||||
}
|
||||
|
||||
intent?.getParcelableExtra<IconImage>(EXTRA_ICON)?.let {
|
||||
intent?.getParcelableExtraCompat<IconImage>(EXTRA_ICON)?.let {
|
||||
mIconImage = it
|
||||
}
|
||||
|
||||
@@ -118,14 +122,9 @@ class IconPickerActivity : LockingActivity() {
|
||||
), ICON_PICKER_FRAGMENT_TAG)
|
||||
}
|
||||
} else {
|
||||
mIconImage = savedInstanceState.getParcelable(EXTRA_ICON) ?: mIconImage
|
||||
mIconImage = savedInstanceState.getParcelableCompat(EXTRA_ICON) ?: mIconImage
|
||||
}
|
||||
|
||||
// Focus view to reinitialize timeout
|
||||
findViewById<ViewGroup>(R.id.icon_picker_container)?.resetAppTimeoutWhenViewFocusedOrChanged(this)
|
||||
|
||||
mSelectFileHelper = SelectFileHelper(this)
|
||||
|
||||
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
|
||||
mIconImage.standard = iconStandard
|
||||
// Remove the custom icon if a standard one is selected
|
||||
@@ -157,6 +156,34 @@ class IconPickerActivity : LockingActivity() {
|
||||
}
|
||||
uploadButton.isEnabled = true
|
||||
}
|
||||
iconPickerViewModel.customIconUpdated.observe(this) { iconCustomUpdated ->
|
||||
if (iconCustomUpdated.error && !iconCustomUpdated.errorConsumed) {
|
||||
Snackbar.make(coordinatorLayout, iconCustomUpdated.errorStringId, Snackbar.LENGTH_LONG).asError().show()
|
||||
iconCustomUpdated.errorConsumed = true
|
||||
}
|
||||
iconCustomUpdated.iconCustom?.let {
|
||||
mDatabase?.updateCustomIcon(it)
|
||||
}
|
||||
iconPickerViewModel.deselectAllCustomIcons()
|
||||
}
|
||||
}
|
||||
|
||||
override fun viewToInvalidateTimeout(): View? {
|
||||
return findViewById<ViewGroup>(R.id.icon_picker_container)
|
||||
}
|
||||
|
||||
override fun finishActivityIfReloadRequested(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
if (database.allowCustomIcons) {
|
||||
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
|
||||
} else {
|
||||
uploadButton.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateIconsSelectedViews() {
|
||||
@@ -187,32 +214,47 @@ class IconPickerActivity : LockingActivity() {
|
||||
}
|
||||
|
||||
// Padding if lock button visible
|
||||
toolbar.updateLockPaddingLeft()
|
||||
toolbar.updateLockPaddingStart()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
if (mCustomIconsSelectionMode) {
|
||||
menuInflater.inflate(R.menu.icon, menu)
|
||||
}
|
||||
menuInflater.inflate(R.menu.icon, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||
menu?.findItem(R.id.menu_edit)?.apply {
|
||||
isEnabled = mIconsSelected.size == 1
|
||||
isVisible = isEnabled
|
||||
}
|
||||
menu?.findItem(R.id.menu_delete)?.apply {
|
||||
isEnabled = mCustomIconsSelectionMode
|
||||
isVisible = isEnabled
|
||||
}
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
if (mCustomIconsSelectionMode) {
|
||||
iconPickerViewModel.deselectAllCustomIcons()
|
||||
} else {
|
||||
onBackPressed()
|
||||
onDatabaseBackPressed()
|
||||
}
|
||||
}
|
||||
R.id.menu_edit -> {
|
||||
updateCustomIcon(mIconsSelected[0])
|
||||
}
|
||||
R.id.menu_delete -> {
|
||||
mIconsSelected.forEach { iconToRemove ->
|
||||
removeCustomIcon(iconToRemove)
|
||||
}
|
||||
}
|
||||
R.id.menu_external_icon -> {
|
||||
this.openUrl(R.string.external_icon_url)
|
||||
}
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
@@ -225,7 +267,7 @@ class IconPickerActivity : LockingActivity() {
|
||||
// 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 {
|
||||
@@ -269,6 +311,11 @@ class IconPickerActivity : LockingActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCustomIcon(iconImageCustom: IconImageCustom) {
|
||||
IconEditDialogFragment.update(iconImageCustom)
|
||||
.show(supportFragmentManager, IconEditDialogFragment.TAG_UPDATE_ICON)
|
||||
}
|
||||
|
||||
private fun removeCustomIcon(iconImageCustom: IconImageCustom) {
|
||||
uploadButton.isEnabled = false
|
||||
iconPickerViewModel.deselectAllCustomIcons()
|
||||
@@ -278,52 +325,42 @@ class IconPickerActivity : LockingActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||
addCustomIcon(uri)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setResult() {
|
||||
setResult(Activity.RESULT_OK, Intent().apply {
|
||||
putExtra(EXTRA_ICON, mIconImage)
|
||||
})
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
override fun onDatabaseBackPressed() {
|
||||
setResult()
|
||||
super.onBackPressed()
|
||||
super.onDatabaseBackPressed()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ICON_PICKER_FRAGMENT_TAG = "ICON_PICKER_FRAGMENT_TAG"
|
||||
|
||||
private const val ICON_SELECTED_REQUEST = 15861
|
||||
private const val EXTRA_ICON = "EXTRA_ICON"
|
||||
|
||||
private const val MAX_ICON_SIZE = 5242880
|
||||
|
||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?, listener: (icon: IconImage) -> Unit) {
|
||||
if (requestCode == ICON_SELECTED_REQUEST) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
listener.invoke(data?.getParcelableExtra(EXTRA_ICON) ?: IconImage())
|
||||
fun registerIconSelectionForResult(context: FragmentActivity,
|
||||
listener: (icon: IconImage) -> Unit): ActivityResultLauncher<Intent> {
|
||||
return context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
listener.invoke(result.data?.getParcelableExtraCompat(EXTRA_ICON) ?: IconImage())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun launch(context: Activity,
|
||||
previousIcon: IconImage?) {
|
||||
fun launch(context: FragmentActivity,
|
||||
previousIcon: IconImage?,
|
||||
resultLauncher: ActivityResultLauncher<Intent>) {
|
||||
// Create an instance to return the picker icon
|
||||
context.startActivityForResult(
|
||||
Intent(context,
|
||||
IconPickerActivity::class.java).apply {
|
||||
resultLauncher.launch(
|
||||
Intent(context, IconPickerActivity::class.java).apply {
|
||||
if (previousIcon != null)
|
||||
putExtra(EXTRA_ICON, previousIcon)
|
||||
},
|
||||
ICON_SELECTED_REQUEST)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
@@ -31,16 +32,22 @@ import android.widget.ImageView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import com.igreenwood.loupe.Loupe
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
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 : LockingActivity() {
|
||||
class ImageViewerActivity : DatabaseLockActivity() {
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
private var imageContainerView: ViewGroup? = null
|
||||
private lateinit var imageView: ImageView
|
||||
private lateinit var progressView: View
|
||||
|
||||
override fun manageDatabaseInfo(): Boolean = false
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -50,49 +57,21 @@ class ImageViewerActivity : LockingActivity() {
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
val imageContainerView: ViewGroup = findViewById(R.id.image_viewer_container)
|
||||
val imageView: ImageView = findViewById(R.id.image_viewer_image)
|
||||
val progressView: View = findViewById(R.id.image_viewer_progress)
|
||||
|
||||
// Approximately, to not OOM and allow a zoom
|
||||
val mImagePreviewMaxWidth = max(
|
||||
resources.displayMetrics.widthPixels * 2,
|
||||
resources.displayMetrics.heightPixels * 2
|
||||
)
|
||||
|
||||
mDatabase = Database.getInstance()
|
||||
|
||||
try {
|
||||
progressView.visibility = View.VISIBLE
|
||||
intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
|
||||
|
||||
supportActionBar?.title = attachment.name
|
||||
|
||||
val size = attachment.binaryData.getSize()
|
||||
supportActionBar?.subtitle = Formatter.formatFileSize(this, size)
|
||||
|
||||
mDatabase?.let { database ->
|
||||
BinaryDatabaseManager.loadBitmap(
|
||||
database,
|
||||
attachment.binaryData,
|
||||
mImagePreviewMaxWidth
|
||||
) { bitmapLoaded ->
|
||||
if (bitmapLoaded == null) {
|
||||
finish()
|
||||
} else {
|
||||
progressView.visibility = View.GONE
|
||||
imageView.setImageBitmap(bitmapLoaded)
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?: finish()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to view the binary", e)
|
||||
finish()
|
||||
toolbar.setOnTouchListener { _, _ ->
|
||||
resetAppTimeout()
|
||||
false
|
||||
}
|
||||
|
||||
Loupe.create(imageView, imageContainerView) {
|
||||
imageContainerView = findViewById(R.id.image_viewer_container)
|
||||
imageView = findViewById(R.id.image_viewer_image)
|
||||
progressView = findViewById(R.id.image_viewer_progress)
|
||||
|
||||
Loupe.create(imageView, imageContainerView!!) {
|
||||
onViewTouchedListener = View.OnTouchListener { _, _ ->
|
||||
// to reset timeout when Loupe image view touched
|
||||
resetAppTimeout()
|
||||
false
|
||||
}
|
||||
onViewTranslateListener = object : Loupe.OnViewTranslateListener {
|
||||
|
||||
override fun onStart(view: ImageView) {
|
||||
@@ -115,6 +94,52 @@ class ImageViewerActivity : LockingActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun viewToInvalidateTimeout(): View? {
|
||||
// Null to manually manage events
|
||||
return null
|
||||
}
|
||||
|
||||
override fun finishActivityIfReloadRequested(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
try {
|
||||
progressView.visibility = View.VISIBLE
|
||||
intent.getParcelableExtraCompat<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
|
||||
|
||||
supportActionBar?.title = attachment.name
|
||||
|
||||
val size = attachment.binaryData.getSize()
|
||||
supportActionBar?.subtitle = Formatter.formatFileSize(this, size)
|
||||
|
||||
// Approximately, to not OOM and allow a zoom
|
||||
val mImagePreviewMaxWidth = max(
|
||||
resources.displayMetrics.widthPixels * 2,
|
||||
resources.displayMetrics.heightPixels * 2
|
||||
)
|
||||
|
||||
BinaryDatabaseManager.loadBitmap(
|
||||
database,
|
||||
attachment.binaryData,
|
||||
mImagePreviewMaxWidth
|
||||
) { bitmapLoaded ->
|
||||
if (bitmapLoaded == null) {
|
||||
finish()
|
||||
} else {
|
||||
progressView.visibility = View.GONE
|
||||
imageView.setImageBitmap(bitmapLoaded)
|
||||
}
|
||||
}
|
||||
} ?: finish()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to view the binary", e)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> finish()
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
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.updateLockPaddingStart
|
||||
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
|
||||
|
||||
override fun manageDatabaseInfo(): Boolean = true
|
||||
|
||||
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.updateLockPaddingStart()
|
||||
}
|
||||
|
||||
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,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
|
||||
/**
|
||||
* Activity to select entry in database and populate it in Magikeyboard
|
||||
*/
|
||||
class MagikeyboardLauncherActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val database = Database.getInstance()
|
||||
val readOnly = database.isReadOnly
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
database,
|
||||
null,
|
||||
{
|
||||
// Not called
|
||||
// if items found directly returns before calling this activity
|
||||
},
|
||||
{
|
||||
// Select if not found
|
||||
GroupActivity.launchForKeyboardSelectionResult(this, readOnly)
|
||||
},
|
||||
{
|
||||
// Pass extra to get entry
|
||||
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this)
|
||||
}
|
||||
)
|
||||
finish()
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,853 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.biometric.DeviceUnlockFragment
|
||||
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
||||
import com.kunzisoft.keepass.biometric.deviceUnlockError
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||
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.CipherDecryptDatabase
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.CredentialStorage
|
||||
import com.kunzisoft.keepass.model.DatabaseFile
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
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.AppearanceSettingsActivity
|
||||
import com.kunzisoft.keepass.settings.DeviceUnlockSettingsActivity
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil.getUri
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||
import com.kunzisoft.keepass.view.MainCredentialView
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||
import com.kunzisoft.keepass.viewmodels.DeviceUnlockViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
|
||||
class MainCredentialActivity : DatabaseModeActivity() {
|
||||
|
||||
// Views
|
||||
private var toolbar: Toolbar? = null
|
||||
private var filenameView: TextView? = null
|
||||
private var logotypeButton: View? = null
|
||||
private var deviceUnlockButton: View? = null
|
||||
private var mainCredentialView: MainCredentialView? = null
|
||||
private var confirmButtonView: Button? = null
|
||||
private var infoContainerView: ViewGroup? = null
|
||||
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||
private var deviceUnlockFragment: DeviceUnlockFragment? = null
|
||||
|
||||
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||
private val mDeviceUnlockViewModel: DeviceUnlockViewModel? by lazy {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
ViewModelProvider(this)[DeviceUnlockViewModel::class.java]
|
||||
} else null
|
||||
}
|
||||
|
||||
private val mPasswordActivityEducation = PasswordActivityEducation(this)
|
||||
|
||||
private var mDefaultDatabase: Boolean = false
|
||||
private var mDatabaseFileUri: Uri? = null
|
||||
|
||||
private var mRememberKeyFile: Boolean = false
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
private var mRememberHardwareKey: Boolean = false
|
||||
|
||||
private var mReadOnly: Boolean = false
|
||||
private var mForceReadOnly: Boolean = false
|
||||
|
||||
override fun manageDatabaseInfo(): Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_main_credential)
|
||||
|
||||
toolbar = findViewById(R.id.toolbar)
|
||||
toolbar?.title = getString(R.string.app_name)
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
filenameView = findViewById(R.id.filename)
|
||||
logotypeButton = findViewById(R.id.activity_password_logotype)
|
||||
deviceUnlockButton = findViewById(R.id.fragment_device_unlock_container_view)
|
||||
mainCredentialView = findViewById(R.id.activity_password_credentials)
|
||||
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
||||
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
|
||||
|
||||
mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) {
|
||||
savedInstanceState.getBoolean(KEY_READ_ONLY)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this)
|
||||
|
||||
// Build elements to manage keyfile selection
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
if (uri != null) {
|
||||
mainCredentialView?.populateKeyFileView(uri)
|
||||
}
|
||||
}
|
||||
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
|
||||
mainCredentialView?.onValidateListener = {
|
||||
loadDatabase()
|
||||
}
|
||||
|
||||
// If is a view intent
|
||||
getUriFromIntent(intent)
|
||||
|
||||
// Show appearance
|
||||
logotypeButton?.setOnClickListener {
|
||||
startActivity(Intent(this, AppearanceSettingsActivity::class.java))
|
||||
}
|
||||
|
||||
// Listen password checkbox to init advanced unlock and confirmation button
|
||||
mainCredentialView?.onConditionToStoreCredentialChanged = { _, verified ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mDeviceUnlockViewModel?.checkConditionToStoreCredential(
|
||||
condition = verified
|
||||
)
|
||||
}
|
||||
// TODO Async by ViewModel
|
||||
enableConfirmationButton()
|
||||
}
|
||||
|
||||
// Observe if default database
|
||||
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
||||
mDefaultDatabase = isDefaultDatabase
|
||||
}
|
||||
|
||||
// Observe database file change
|
||||
mDatabaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
|
||||
|
||||
// Force read only if the file does not exists
|
||||
val databaseFileNotExists = databaseFile?.let {
|
||||
!it.databaseFileExists
|
||||
} ?: true
|
||||
infoContainerView?.visibility = if (databaseFileNotExists) {
|
||||
mReadOnly = true
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
mForceReadOnly = databaseFileNotExists
|
||||
|
||||
// Restore read-only state from database file if not forced
|
||||
if (!mForceReadOnly) {
|
||||
databaseFile?.readOnly?.let { savedReadOnlyState ->
|
||||
mReadOnly = savedReadOnlyState
|
||||
}
|
||||
}
|
||||
|
||||
invalidateOptionsMenu()
|
||||
|
||||
// Post init uri with KeyFile only if needed
|
||||
val databaseKeyFileUri = mainCredentialView?.getMainCredential()?.keyFileUri
|
||||
val keyFileUri =
|
||||
if (mRememberKeyFile
|
||||
&& (databaseKeyFileUri == null || databaseKeyFileUri.toString().isEmpty())) {
|
||||
databaseFile?.keyFileUri
|
||||
} else {
|
||||
databaseKeyFileUri
|
||||
}
|
||||
|
||||
val databaseHardwareKey = mainCredentialView?.getMainCredential()?.hardwareKey
|
||||
val hardwareKey =
|
||||
if (mRememberHardwareKey
|
||||
&& databaseHardwareKey == null) {
|
||||
databaseFile?.hardwareKey
|
||||
} else {
|
||||
databaseHardwareKey
|
||||
}
|
||||
|
||||
// Define title
|
||||
filenameView?.text = databaseFile?.databaseAlias ?: ""
|
||||
|
||||
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri, hardwareKey)
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mDeviceUnlockViewModel?.let { deviceUnlockViewModel ->
|
||||
deviceUnlockViewModel.uiState.collect { uiState ->
|
||||
// New value received
|
||||
uiState.credentialRequiredCipher?.let { cipher ->
|
||||
deviceUnlockViewModel.encryptCredential(
|
||||
credential = getCredentialForEncryption(),
|
||||
cipher = cipher
|
||||
)
|
||||
}
|
||||
uiState.cipherEncryptDatabase?.let { cipherEncryptDatabase ->
|
||||
onCredentialEncrypted(cipherEncryptDatabase)
|
||||
deviceUnlockViewModel.consumeCredentialEncrypted()
|
||||
}
|
||||
uiState.cipherDecryptDatabase?.let { cipherDecryptDatabase ->
|
||||
onCredentialDecrypted(cipherDecryptDatabase)
|
||||
deviceUnlockViewModel.consumeCredentialDecrypted()
|
||||
}
|
||||
uiState.exception?.let { error ->
|
||||
Snackbar.make(
|
||||
coordinatorLayout,
|
||||
deviceUnlockError(error, this@MainCredentialActivity),
|
||||
Snackbar.LENGTH_LONG
|
||||
).asError().show()
|
||||
deviceUnlockViewModel.exceptionShown()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// Init Biometric elements only if allowed
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& PreferencesUtil.isDeviceUnlockEnable(this)) {
|
||||
deviceUnlockFragment = supportFragmentManager
|
||||
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? DeviceUnlockFragment?
|
||||
if (deviceUnlockFragment == null) {
|
||||
deviceUnlockFragment = DeviceUnlockFragment().also {
|
||||
supportFragmentManager.commit {
|
||||
replace(
|
||||
R.id.fragment_device_unlock_container_view,
|
||||
it,
|
||||
UNLOCK_FRAGMENT_TAG
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity)
|
||||
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this@MainCredentialActivity)
|
||||
|
||||
// Back to previous keyboard is setting activated
|
||||
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@MainCredentialActivity)) {
|
||||
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
||||
}
|
||||
|
||||
mDatabaseFileUri?.let { databaseFileUri ->
|
||||
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
// Trying to load another database
|
||||
if (mDatabaseFileUri != null
|
||||
&& database.fileUri != null
|
||||
&& mDatabaseFileUri != database.fileUri) {
|
||||
Toast.makeText(this,
|
||||
R.string.warning_database_already_opened,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
launchGroupActivityIfLoaded(database)
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
super.onDatabaseActionFinished(database, actionTask, result)
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_LOAD_TASK -> {
|
||||
if (result.isSuccess) {
|
||||
launchGroupActivityIfLoaded(database)
|
||||
} else {
|
||||
mainCredentialView?.requestPasswordFocus()
|
||||
// Manage special exceptions
|
||||
when (result.exception) {
|
||||
is DuplicateUuidDatabaseException -> {
|
||||
// Relaunch loading if we need to fix UUID
|
||||
showLoadDatabaseDuplicateUuidMessage {
|
||||
|
||||
var databaseUri: Uri? = null
|
||||
var mainCredential = MainCredential()
|
||||
var readOnly = true
|
||||
var cipherEncryptDatabase: CipherEncryptDatabase? = null
|
||||
|
||||
result.data?.let { resultData ->
|
||||
databaseUri = resultData.getParcelableCompat(DATABASE_URI_KEY)
|
||||
mainCredential =
|
||||
resultData.getParcelableCompat(MAIN_CREDENTIAL_KEY)
|
||||
?: mainCredential
|
||||
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
||||
cipherEncryptDatabase =
|
||||
resultData.getParcelableCompat(CIPHER_DATABASE_KEY)
|
||||
}
|
||||
|
||||
databaseUri?.let { databaseFileUri ->
|
||||
showProgressDialogAndLoadDatabase(
|
||||
databaseFileUri,
|
||||
mainCredential,
|
||||
readOnly,
|
||||
cipherEncryptDatabase,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is FileNotFoundDatabaseException -> {
|
||||
// Remove this default database inaccessible
|
||||
if (mDefaultDatabase) {
|
||||
mDatabaseFileViewModel.removeDefaultDatabase()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
coordinatorLayout.showActionErrorIfNeeded(result)
|
||||
}
|
||||
|
||||
private fun getUriFromIntent(intent: Intent?) {
|
||||
// If is a view intent
|
||||
val action = intent?.action
|
||||
if (action == VIEW_INTENT) {
|
||||
fillCredentials(
|
||||
intent.data,
|
||||
intent.getUri(KEY_KEYFILE),
|
||||
HardwareKey.getHardwareKeyFromString(intent.getStringExtra(KEY_HARDWARE_KEY))
|
||||
)
|
||||
} else {
|
||||
fillCredentials(
|
||||
intent?.getParcelableExtraCompat(KEY_FILENAME),
|
||||
intent?.getParcelableExtraCompat(KEY_KEYFILE),
|
||||
HardwareKey.getHardwareKeyFromString(intent?.getStringExtra(KEY_HARDWARE_KEY))
|
||||
)
|
||||
}
|
||||
try {
|
||||
intent?.removeExtra(KEY_KEYFILE)
|
||||
intent?.removeExtra(KEY_HARDWARE_KEY)
|
||||
} catch (_: Exception) {}
|
||||
mDatabaseFileUri?.let {
|
||||
mDatabaseFileViewModel.checkIfIsDefaultDatabase(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fillCredentials(databaseUri: Uri?,
|
||||
keyFileUri: Uri?,
|
||||
hardwareKey: HardwareKey?) {
|
||||
mDatabaseFileUri = databaseUri
|
||||
mainCredentialView?.populateKeyFileView(keyFileUri)
|
||||
mainCredentialView?.populateHardwareKeyView(hardwareKey)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
getUriFromIntent(intent)
|
||||
}
|
||||
|
||||
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
||||
// Check if database really loaded
|
||||
if (database.loaded) {
|
||||
clearCredentialsViews(clearKeyFile = true, clearHardwareKey = true)
|
||||
GroupActivity.launch(this,
|
||||
database,
|
||||
{ onValidateSpecialMode() },
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() },
|
||||
mCredentialActivityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val credentialStorageListener = object: MainCredentialView.CredentialStorageListener {
|
||||
override fun passwordToStore(password: String?): ByteArray? {
|
||||
return password?.toByteArray()
|
||||
}
|
||||
|
||||
override fun keyfileToStore(keyfile: Uri?): ByteArray? {
|
||||
// TODO create byte array to store keyfile
|
||||
return null
|
||||
}
|
||||
|
||||
override fun hardwareKeyToStore(): ByteArray? {
|
||||
// TODO create byte array to store hardware key
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCredentialForEncryption(): ByteArray {
|
||||
return mainCredentialView?.retrieveCredentialForStorage(credentialStorageListener)
|
||||
?: byteArrayOf()
|
||||
}
|
||||
|
||||
private fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) {
|
||||
// Load the database if password is registered with biometric
|
||||
loadDatabase(mDatabaseFileUri,
|
||||
mainCredentialView?.getMainCredential(),
|
||||
cipherEncryptDatabase
|
||||
)
|
||||
}
|
||||
|
||||
private fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
|
||||
// Load the database if password is retrieve from biometric
|
||||
// Retrieve from biometric
|
||||
val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
|
||||
when (cipherDecryptDatabase.credentialStorage) {
|
||||
CredentialStorage.PASSWORD -> {
|
||||
mainCredential.password = String(cipherDecryptDatabase.decryptedValue)
|
||||
}
|
||||
CredentialStorage.KEY_FILE -> {
|
||||
// TODO advanced unlock key file
|
||||
}
|
||||
CredentialStorage.HARDWARE_KEY -> {
|
||||
// TODO advanced unlock hardware key
|
||||
}
|
||||
}
|
||||
loadDatabase(mDatabaseFileUri,
|
||||
mainCredential,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
private fun onDatabaseFileLoaded(databaseFileUri: Uri?,
|
||||
keyFileUri: Uri?,
|
||||
hardwareKey: HardwareKey?) {
|
||||
// Define Key File text
|
||||
if (mRememberKeyFile) {
|
||||
mainCredentialView?.populateKeyFileView(keyFileUri)
|
||||
}
|
||||
|
||||
// Define hardware key
|
||||
if (mRememberHardwareKey) {
|
||||
mainCredentialView?.populateHardwareKeyView(hardwareKey)
|
||||
}
|
||||
|
||||
// Define listener for validate button
|
||||
confirmButtonView?.setOnClickListener {
|
||||
mainCredentialView?.validateCredential()
|
||||
}
|
||||
|
||||
// If Activity is launch with a password and want to open directly
|
||||
val intent = intent
|
||||
val password = intent.getStringExtra(KEY_PASSWORD)
|
||||
// Consume the intent extra password
|
||||
intent.removeExtra(KEY_PASSWORD)
|
||||
if (password != null) {
|
||||
mainCredentialView?.populatePasswordTextView(password)
|
||||
}
|
||||
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
|
||||
intent.removeExtra(KEY_LAUNCH_IMMEDIATELY)
|
||||
if (launchImmediately) {
|
||||
loadDatabase()
|
||||
} else {
|
||||
// Init Biometric elements
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mDeviceUnlockViewModel?.connect(databaseFileUri)
|
||||
}
|
||||
}
|
||||
|
||||
enableConfirmationButton()
|
||||
|
||||
mainCredentialView?.focusPasswordFieldAndOpenKeyboard()
|
||||
}
|
||||
|
||||
private fun enableConfirmationButton() {
|
||||
// Enable or not the open button if setting is checked
|
||||
if (!PreferencesUtil.emptyPasswordAllowed(this@MainCredentialActivity)) {
|
||||
confirmButtonView?.isEnabled = mainCredentialView?.isFill() ?: false
|
||||
} else {
|
||||
confirmButtonView?.isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile,
|
||||
clearHardwareKey: Boolean = !mRememberHardwareKey) {
|
||||
mainCredentialView?.populatePasswordTextView(null)
|
||||
if (clearKeyFile) {
|
||||
mainCredentialView?.populateKeyFileView(null)
|
||||
}
|
||||
if (clearHardwareKey) {
|
||||
mainCredentialView?.populateHardwareKeyView(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean(KEY_READ_ONLY, mReadOnly)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
private fun loadDatabase() {
|
||||
loadDatabase(mDatabaseFileUri,
|
||||
mainCredentialView?.getMainCredential(),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadDatabase(databaseFileUri: Uri?,
|
||||
mainCredential: MainCredential?,
|
||||
cipherEncryptDatabase: CipherEncryptDatabase?) {
|
||||
|
||||
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
|
||||
clearCredentialsViews()
|
||||
}
|
||||
|
||||
if (mReadOnly && mSpecialMode == SpecialMode.REGISTRATION) {
|
||||
Log.e(TAG, getString(R.string.error_save_read_only))
|
||||
Snackbar.make(coordinatorLayout,
|
||||
R.string.error_save_read_only,
|
||||
Snackbar.LENGTH_LONG).asError().show()
|
||||
} else {
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
// Show the progress dialog and load the database
|
||||
showProgressDialogAndLoadDatabase(
|
||||
databaseUri,
|
||||
mainCredential ?: MainCredential(),
|
||||
mReadOnly,
|
||||
cipherEncryptDatabase,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
|
||||
mainCredential: MainCredential,
|
||||
readOnly: Boolean,
|
||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||
fixDuplicateUUID: Boolean) {
|
||||
mDatabaseViewModel.loadDatabase(
|
||||
databaseUri,
|
||||
mainCredential,
|
||||
readOnly,
|
||||
cipherEncryptDatabase,
|
||||
fixDuplicateUUID
|
||||
)
|
||||
}
|
||||
|
||||
private fun showLoadDatabaseDuplicateUuidMessage(loadDatabaseWithFix: (() -> Unit)? = null) {
|
||||
DuplicateUuidDialog().apply {
|
||||
positiveAction = loadDatabaseWithFix
|
||||
}.show(supportFragmentManager, "duplicateUUIDDialog")
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
val inflater = menuInflater
|
||||
// Read menu
|
||||
inflater.inflate(R.menu.open_file, menu)
|
||||
if (mForceReadOnly) {
|
||||
menu.removeItem(R.id.menu_open_file_read_mode_key)
|
||||
} else {
|
||||
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
|
||||
}
|
||||
|
||||
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||
MenuUtil.defaultMenuInflater(this, inflater, menu)
|
||||
}
|
||||
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
launchEducation(menu)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// To fix multiple view education
|
||||
private var performedEductionInProgress = false
|
||||
private fun launchEducation(menu: Menu) {
|
||||
if (!performedEductionInProgress) {
|
||||
performedEductionInProgress = true
|
||||
// Show education views
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
performedNextEducation(menu)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun performedNextEducation(menu: Menu) {
|
||||
val educationToolbar = toolbar
|
||||
val unlockEducationPerformed = educationToolbar != null
|
||||
&& mPasswordActivityEducation.checkAndPerformedUnlockEducation(
|
||||
educationToolbar,
|
||||
{
|
||||
performedNextEducation(menu)
|
||||
},
|
||||
{
|
||||
performedNextEducation(menu)
|
||||
})
|
||||
if (!unlockEducationPerformed) {
|
||||
val readOnlyEducationPerformed =
|
||||
educationToolbar?.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
|
||||
&& mPasswordActivityEducation.checkAndPerformedReadOnlyEducation(
|
||||
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
|
||||
{
|
||||
try {
|
||||
menu.findItem(R.id.menu_open_file_read_mode_key)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to find read mode menu", e)
|
||||
}
|
||||
performedNextEducation(menu)
|
||||
},
|
||||
{
|
||||
performedNextEducation(menu)
|
||||
})
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& !readOnlyEducationPerformed) {
|
||||
val biometricCanAuthenticate = DeviceUnlockManager.canAuthenticate(this)
|
||||
if ((biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
||||
&& deviceUnlockButton != null) {
|
||||
mPasswordActivityEducation.checkAndPerformedBiometricEducation(
|
||||
deviceUnlockButton!!,
|
||||
{
|
||||
startActivity(
|
||||
Intent(
|
||||
this,
|
||||
DeviceUnlockSettingsActivity::class.java
|
||||
)
|
||||
)
|
||||
},
|
||||
{
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeOpenFileReadIcon(togglePassword: MenuItem) {
|
||||
if (mReadOnly) {
|
||||
togglePassword.setTitle(R.string.menu_file_selection_read_only)
|
||||
togglePassword.setIcon(R.drawable.ic_read_only_white_24dp)
|
||||
} else {
|
||||
togglePassword.setTitle(R.string.menu_open_file_read_and_write)
|
||||
togglePassword.setIcon(R.drawable.ic_read_write_white_24dp)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> finish()
|
||||
R.id.menu_open_file_read_mode_key -> {
|
||||
mReadOnly = !mReadOnly
|
||||
changeOpenFileReadIcon(item)
|
||||
// Save the read-only state to database
|
||||
mDatabaseFileUri?.let { databaseUri ->
|
||||
FileDatabaseHistoryAction.getInstance(applicationContext).addOrUpdateDatabaseFile(
|
||||
DatabaseFile(databaseUri = databaseUri, readOnly = mReadOnly)
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
mDeviceUnlockViewModel?.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = MainCredentialActivity::class.java.name
|
||||
|
||||
private const val UNLOCK_FRAGMENT_TAG = "UNLOCK_FRAGMENT_TAG"
|
||||
|
||||
private const val KEY_FILENAME = "fileName"
|
||||
private const val KEY_KEYFILE = "keyFile"
|
||||
private const val KEY_HARDWARE_KEY = "hardwareKey"
|
||||
private const val VIEW_INTENT = "android.intent.action.VIEW"
|
||||
|
||||
private const val KEY_READ_ONLY = "KEY_READ_ONLY"
|
||||
private const val KEY_PASSWORD = "password"
|
||||
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||
|
||||
private fun buildAndLaunchIntent(
|
||||
activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
intentBuildLauncher: (Intent) -> Unit
|
||||
) {
|
||||
val intent = Intent(activity, MainCredentialActivity::class.java)
|
||||
intent.putExtra(KEY_FILENAME, databaseFile)
|
||||
if (keyFile != null)
|
||||
intent.putExtra(KEY_KEYFILE, keyFile)
|
||||
if (hardwareKey != null)
|
||||
intent.putExtra(KEY_HARDWARE_KEY, hardwareKey.toString())
|
||||
intentBuildLauncher.invoke(intent)
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Standard Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launch(
|
||||
activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?
|
||||
) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Share Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForSearchResult(
|
||||
activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
searchInfo: SearchInfo
|
||||
) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||
context = activity,
|
||||
intent = intent,
|
||||
searchInfo = searchInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Selection Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForSelection(
|
||||
activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
typeMode: TypeMode,
|
||||
searchInfo: SearchInfo?,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?
|
||||
) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForSelectionModeResult(
|
||||
context = activity,
|
||||
intent = intent,
|
||||
typeMode = typeMode,
|
||||
searchInfo = searchInfo,
|
||||
activityResultLauncher = activityResultLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Registration Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForRegistration(
|
||||
activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
typeMode: TypeMode,
|
||||
registerInfo: RegisterInfo?,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?
|
||||
) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||
context = activity,
|
||||
intent = intent,
|
||||
typeMode = typeMode,
|
||||
registerInfo = registerInfo,
|
||||
activityResultLauncher = activityResultLauncher,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,924 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
||||
import android.widget.*
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.fragment.app.commit
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
|
||||
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_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.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
||||
|
||||
// Views
|
||||
private var toolbar: Toolbar? = null
|
||||
private var filenameView: TextView? = null
|
||||
private var passwordView: EditText? = null
|
||||
private var keyFileSelectionView: KeyFileSelectionView? = null
|
||||
private var confirmButtonView: Button? = null
|
||||
private var checkboxPasswordView: CompoundButton? = null
|
||||
private var checkboxKeyFileView: CompoundButton? = null
|
||||
private var infoContainerView: ViewGroup? = null
|
||||
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
|
||||
|
||||
private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||
|
||||
private var mDefaultDatabase: Boolean = false
|
||||
private var mDatabaseFileUri: Uri? = null
|
||||
private var mDatabaseKeyFileUri: Uri? = null
|
||||
|
||||
private var mRememberKeyFile: Boolean = false
|
||||
private var mSelectFileHelper: SelectFileHelper? = null
|
||||
|
||||
private var mPermissionAsked = false
|
||||
private var readOnly: Boolean = false
|
||||
private var mForceReadOnly: Boolean = false
|
||||
set(value) {
|
||||
infoContainerView?.visibility = if (value) {
|
||||
readOnly = true
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||
|
||||
private var mAllowAutoOpenBiometricPrompt: Boolean = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_password)
|
||||
|
||||
toolbar = findViewById(R.id.toolbar)
|
||||
toolbar?.title = getString(R.string.app_name)
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
||||
filenameView = findViewById(R.id.filename)
|
||||
passwordView = findViewById(R.id.password)
|
||||
keyFileSelectionView = findViewById(R.id.keyfile_selection)
|
||||
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
||||
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
||||
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
|
||||
|
||||
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||
|
||||
mSelectFileHelper = SelectFileHelper(this@PasswordActivity)
|
||||
keyFileSelectionView?.apply {
|
||||
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
||||
setOnClickListener(it)
|
||||
setOnLongClickListener(it)
|
||||
}
|
||||
}
|
||||
|
||||
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
||||
passwordView?.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun afterTextChanged(editable: Editable) {
|
||||
if (editable.toString().isNotEmpty() && checkboxPasswordView?.isChecked != true)
|
||||
checkboxPasswordView?.isChecked = true
|
||||
}
|
||||
})
|
||||
|
||||
// If is a view intent
|
||||
getUriFromIntent(intent)
|
||||
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
|
||||
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
|
||||
}
|
||||
if (savedInstanceState?.containsKey(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT) == true) {
|
||||
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
|
||||
}
|
||||
|
||||
// Init Biometric elements
|
||||
advancedUnlockFragment = supportFragmentManager
|
||||
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
|
||||
if (advancedUnlockFragment == null) {
|
||||
advancedUnlockFragment = AdvancedUnlockFragment()
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.fragment_advanced_unlock_container_view,
|
||||
advancedUnlockFragment!!,
|
||||
UNLOCK_FRAGMENT_TAG)
|
||||
}
|
||||
}
|
||||
|
||||
// Listen password checkbox to init advanced unlock and confirmation button
|
||||
checkboxPasswordView?.setOnCheckedChangeListener { _, _ ->
|
||||
advancedUnlockFragment?.checkUnlockAvailability()
|
||||
enableOrNotTheConfirmationButton()
|
||||
}
|
||||
|
||||
// Observe if default database
|
||||
databaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
||||
mDefaultDatabase = isDefaultDatabase
|
||||
}
|
||||
|
||||
// Observe database file change
|
||||
databaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
|
||||
// Force read only if the file does not exists
|
||||
mForceReadOnly = databaseFile?.let {
|
||||
!it.databaseFileExists
|
||||
} ?: true
|
||||
invalidateOptionsMenu()
|
||||
|
||||
// Post init uri with KeyFile only if needed
|
||||
val keyFileUri =
|
||||
if (mRememberKeyFile
|
||||
&& (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) {
|
||||
databaseFile?.keyFileUri
|
||||
} else {
|
||||
mDatabaseKeyFileUri
|
||||
}
|
||||
|
||||
// Define title
|
||||
filenameView?.text = databaseFile?.databaseAlias ?: ""
|
||||
|
||||
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
|
||||
}
|
||||
|
||||
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
|
||||
onActionFinish = { actionTask, result ->
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_LOAD_TASK -> {
|
||||
// Recheck advanced unlock if error
|
||||
advancedUnlockFragment?.initAdvancedUnlockMode()
|
||||
|
||||
if (result.isSuccess) {
|
||||
mDatabaseKeyFileUri = null
|
||||
clearCredentialsViews(true)
|
||||
launchGroupActivity()
|
||||
} else {
|
||||
var resultError = ""
|
||||
val resultException = result.exception
|
||||
val resultMessage = result.message
|
||||
|
||||
if (resultException != null) {
|
||||
resultError = resultException.getLocalizedMessage(resources)
|
||||
|
||||
when (resultException) {
|
||||
is DuplicateUuidDatabaseException -> {
|
||||
// Relaunch loading if we need to fix UUID
|
||||
showLoadDatabaseDuplicateUuidMessage {
|
||||
|
||||
var databaseUri: Uri? = null
|
||||
var mainCredential: MainCredential = MainCredential()
|
||||
var readOnly = true
|
||||
var cipherEntity: CipherDatabaseEntity? = null
|
||||
|
||||
result.data?.let { resultData ->
|
||||
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
||||
mainCredential = resultData.getParcelable(MAIN_CREDENTIAL_KEY) ?: mainCredential
|
||||
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
||||
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
|
||||
}
|
||||
|
||||
databaseUri?.let { databaseFileUri ->
|
||||
showProgressDialogAndLoadDatabase(
|
||||
databaseFileUri,
|
||||
mainCredential,
|
||||
readOnly,
|
||||
cipherEntity,
|
||||
true)
|
||||
}
|
||||
}
|
||||
}
|
||||
is FileNotFoundDatabaseException -> {
|
||||
// Remove this default database inaccessible
|
||||
if (mDefaultDatabase) {
|
||||
databaseFileViewModel.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUriFromIntent(intent: Intent?) {
|
||||
// If is a view intent
|
||||
val action = intent?.action
|
||||
if (action != null
|
||||
&& action == VIEW_INTENT) {
|
||||
mDatabaseFileUri = intent.data
|
||||
mDatabaseKeyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
|
||||
} else {
|
||||
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
|
||||
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
|
||||
}
|
||||
mDatabaseFileUri?.let {
|
||||
databaseFileViewModel.checkIfIsDefaultDatabase(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
getUriFromIntent(intent)
|
||||
}
|
||||
|
||||
private fun launchGroupActivity() {
|
||||
GroupActivity.launch(this,
|
||||
readOnly,
|
||||
{ onValidateSpecialMode() },
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() }
|
||||
)
|
||||
}
|
||||
|
||||
override fun onValidateSpecialMode() {
|
||||
super.onValidateSpecialMode()
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onCancelSpecialMode() {
|
||||
super.onCancelSpecialMode()
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun retrieveCredentialForEncryption(): String {
|
||||
return passwordView?.text?.toString() ?: ""
|
||||
}
|
||||
|
||||
override fun conditionToStoreCredential(): Boolean {
|
||||
return checkboxPasswordView?.isChecked == true
|
||||
}
|
||||
|
||||
override fun onCredentialEncrypted(databaseUri: Uri,
|
||||
encryptedCredential: String,
|
||||
ivSpec: String) {
|
||||
// Load the database if password is registered with biometric
|
||||
verifyCheckboxesAndLoadDatabase(
|
||||
CipherDatabaseEntity(
|
||||
databaseUri.toString(),
|
||||
encryptedCredential,
|
||||
ivSpec)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCredentialDecrypted(databaseUri: Uri,
|
||||
decryptedCredential: String) {
|
||||
// Load the database if password is retrieve from biometric
|
||||
// Retrieve from biometric
|
||||
verifyKeyFileCheckboxAndLoadDatabase(decryptedCredential)
|
||||
}
|
||||
|
||||
private val onEditorActionListener = object : TextView.OnEditorActionListener {
|
||||
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
|
||||
if (actionId == IME_ACTION_DONE) {
|
||||
verifyCheckboxesAndLoadDatabase()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (Database.getInstance().loaded) {
|
||||
launchGroupActivity()
|
||||
} else {
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||
|
||||
// If the database isn't accessible make sure to clear the password field, if it
|
||||
// was saved in the instance state
|
||||
if (Database.getInstance().loaded) {
|
||||
clearCredentialsViews()
|
||||
}
|
||||
|
||||
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||
|
||||
// Back to previous keyboard is setting activated
|
||||
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this)) {
|
||||
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
||||
}
|
||||
|
||||
// Don't allow auto open prompt if lock become when UI visible
|
||||
mAllowAutoOpenBiometricPrompt = if (LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true)
|
||||
false
|
||||
else
|
||||
mAllowAutoOpenBiometricPrompt
|
||||
mDatabaseFileUri?.let { databaseFileUri ->
|
||||
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||
}
|
||||
|
||||
checkPermission()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
||||
// Define Key File text
|
||||
if (mRememberKeyFile) {
|
||||
populateKeyFileTextView(keyFileUri)
|
||||
}
|
||||
|
||||
// Define listener for validate button
|
||||
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
|
||||
|
||||
// If Activity is launch with a password and want to open directly
|
||||
val intent = intent
|
||||
val password = intent.getStringExtra(KEY_PASSWORD)
|
||||
// Consume the intent extra password
|
||||
intent.removeExtra(KEY_PASSWORD)
|
||||
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
|
||||
if (password != null) {
|
||||
populatePasswordTextView(password)
|
||||
}
|
||||
if (launchImmediately) {
|
||||
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
|
||||
} else {
|
||||
// Init Biometric elements
|
||||
advancedUnlockFragment?.loadDatabase(databaseFileUri,
|
||||
mAllowAutoOpenBiometricPrompt
|
||||
&& mProgressDatabaseTaskProvider?.isBinded() != true)
|
||||
}
|
||||
|
||||
enableOrNotTheConfirmationButton()
|
||||
}
|
||||
|
||||
private fun enableOrNotTheConfirmationButton() {
|
||||
// Enable or not the open button if setting is checked
|
||||
if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) {
|
||||
checkboxPasswordView?.let {
|
||||
confirmButtonView?.isEnabled = (checkboxPasswordView?.isChecked == true
|
||||
|| checkboxKeyFileView?.isChecked == true)
|
||||
}
|
||||
} else {
|
||||
confirmButtonView?.isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
|
||||
populatePasswordTextView(null)
|
||||
if (clearKeyFile) {
|
||||
populateKeyFileTextView(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun populatePasswordTextView(text: String?) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
passwordView?.setText("")
|
||||
if (checkboxPasswordView?.isChecked == true)
|
||||
checkboxPasswordView?.isChecked = false
|
||||
} else {
|
||||
passwordView?.setText(text)
|
||||
if (checkboxPasswordView?.isChecked != true)
|
||||
checkboxPasswordView?.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateKeyFileTextView(uri: Uri?) {
|
||||
if (uri == null || uri.toString().isEmpty()) {
|
||||
keyFileSelectionView?.uri = null
|
||||
if (checkboxKeyFileView?.isChecked == true)
|
||||
checkboxKeyFileView?.isChecked = false
|
||||
} else {
|
||||
keyFileSelectionView?.uri = uri
|
||||
if (checkboxKeyFileView?.isChecked != true)
|
||||
checkboxKeyFileView?.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||
|
||||
// Reinit locking activity UI variable
|
||||
LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
||||
mAllowAutoOpenBiometricPrompt = true
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
|
||||
mDatabaseKeyFileUri?.let {
|
||||
outState.putString(KEY_KEYFILE, it.toString())
|
||||
}
|
||||
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
||||
outState.putBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT, false)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||
val password: String? = passwordView?.text?.toString()
|
||||
val keyFile: Uri? = keyFileSelectionView?.uri
|
||||
verifyCheckboxesAndLoadDatabase(password, keyFile, cipherDatabaseEntity)
|
||||
}
|
||||
|
||||
private fun verifyCheckboxesAndLoadDatabase(password: String?,
|
||||
keyFile: Uri?,
|
||||
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||
val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password
|
||||
verifyKeyFileCheckbox(keyFile)
|
||||
loadDatabase(mDatabaseFileUri, keyPassword, mDatabaseKeyFileUri, cipherDatabaseEntity)
|
||||
}
|
||||
|
||||
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) {
|
||||
val keyFile: Uri? = keyFileSelectionView?.uri
|
||||
verifyKeyFileCheckbox(keyFile)
|
||||
loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri)
|
||||
}
|
||||
|
||||
private fun verifyKeyFileCheckbox(keyFile: Uri?) {
|
||||
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
|
||||
}
|
||||
|
||||
private fun loadDatabase(databaseFileUri: Uri?,
|
||||
password: String?,
|
||||
keyFileUri: Uri?,
|
||||
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||
|
||||
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
|
||||
clearCredentialsViews()
|
||||
}
|
||||
|
||||
if (readOnly && (
|
||||
mSpecialMode == SpecialMode.SAVE
|
||||
|| mSpecialMode == SpecialMode.REGISTRATION)
|
||||
) {
|
||||
Log.e(TAG, getString(R.string.autofill_read_only_save))
|
||||
Snackbar.make(coordinatorLayout,
|
||||
R.string.autofill_read_only_save,
|
||||
Snackbar.LENGTH_LONG).asError().show()
|
||||
} else {
|
||||
databaseFileUri?.let { databaseUri ->
|
||||
// Show the progress dialog and load the database
|
||||
showProgressDialogAndLoadDatabase(
|
||||
databaseUri,
|
||||
MainCredential(password, keyFileUri),
|
||||
readOnly,
|
||||
cipherDatabaseEntity,
|
||||
false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
|
||||
mainCredential: MainCredential,
|
||||
readOnly: Boolean,
|
||||
cipherDatabaseEntity: CipherDatabaseEntity?,
|
||||
fixDuplicateUUID: Boolean) {
|
||||
mProgressDatabaseTaskProvider?.startDatabaseLoad(
|
||||
databaseUri,
|
||||
mainCredential,
|
||||
readOnly,
|
||||
cipherDatabaseEntity,
|
||||
fixDuplicateUUID
|
||||
)
|
||||
}
|
||||
|
||||
private fun showLoadDatabaseDuplicateUuidMessage(loadDatabaseWithFix: (() -> Unit)? = null) {
|
||||
DuplicateUuidDialog().apply {
|
||||
positiveAction = loadDatabaseWithFix
|
||||
}.show(supportFragmentManager, "duplicateUUIDDialog")
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
val inflater = menuInflater
|
||||
// Read menu
|
||||
inflater.inflate(R.menu.open_file, menu)
|
||||
if (mForceReadOnly) {
|
||||
menu.removeItem(R.id.menu_open_file_read_mode_key)
|
||||
} else {
|
||||
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
|
||||
}
|
||||
|
||||
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||
MenuUtil.defaultMenuInflater(inflater, menu)
|
||||
}
|
||||
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
launchEducation(menu)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Check permission
|
||||
private fun checkPermission() {
|
||||
if (Build.VERSION.SDK_INT in 23..28
|
||||
&& !readOnly
|
||||
&& !mPermissionAsked) {
|
||||
mPermissionAsked = true
|
||||
// Check self permission to show or not the dialog
|
||||
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
val permissions = arrayOf(writePermission)
|
||||
if (toolbar != null
|
||||
&& ActivityCompat.checkSelfPermission(this, writePermission) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this, permissions, WRITE_EXTERNAL_STORAGE_REQUEST)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
when (requestCode) {
|
||||
WRITE_EXTERNAL_STORAGE_REQUEST -> {
|
||||
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
||||
if (ActivityCompat.shouldShowRequestPermissionRationale(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE))
|
||||
Toast.makeText(this, R.string.read_only_warning, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// To fix multiple view education
|
||||
private var performedEductionInProgress = false
|
||||
private fun launchEducation(menu: Menu) {
|
||||
if (!performedEductionInProgress) {
|
||||
performedEductionInProgress = true
|
||||
// Show education views
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(PasswordActivityEducation(this), menu) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
|
||||
menu: Menu) {
|
||||
val educationToolbar = toolbar
|
||||
val unlockEducationPerformed = educationToolbar != null
|
||||
&& passwordActivityEducation.checkAndPerformedUnlockEducation(
|
||||
educationToolbar,
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
},
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
})
|
||||
if (!unlockEducationPerformed) {
|
||||
val readOnlyEducationPerformed =
|
||||
educationToolbar?.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
|
||||
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
|
||||
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
|
||||
{
|
||||
try {
|
||||
menu.findItem(R.id.menu_open_file_read_mode_key)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to find read mode menu")
|
||||
}
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
},
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
})
|
||||
|
||||
advancedUnlockFragment?.performEducation(passwordActivityEducation,
|
||||
readOnlyEducationPerformed,
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
},
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeOpenFileReadIcon(togglePassword: MenuItem) {
|
||||
if (readOnly) {
|
||||
togglePassword.setTitle(R.string.menu_file_selection_read_only)
|
||||
togglePassword.setIcon(R.drawable.ic_read_only_white_24dp)
|
||||
} else {
|
||||
togglePassword.setTitle(R.string.menu_open_file_read_and_write)
|
||||
togglePassword.setIcon(R.drawable.ic_read_write_white_24dp)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> finish()
|
||||
R.id.menu_open_file_read_mode_key -> {
|
||||
readOnly = !readOnly
|
||||
changeOpenFileReadIcon(item)
|
||||
}
|
||||
else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
mAllowAutoOpenBiometricPrompt = false
|
||||
|
||||
// To get device credential unlock result
|
||||
advancedUnlockFragment?.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
// To get entry in result
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
var keyFileResult = false
|
||||
mSelectFileHelper?.let {
|
||||
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
mDatabaseKeyFileUri = uri
|
||||
populateKeyFileTextView(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!keyFileResult) {
|
||||
// this block if not a key file response
|
||||
when (resultCode) {
|
||||
LockingActivity.RESULT_EXIT_LOCK -> {
|
||||
clearCredentialsViews()
|
||||
Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this))
|
||||
}
|
||||
Activity.RESULT_CANCELED -> {
|
||||
clearCredentialsViews()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = PasswordActivity::class.java.name
|
||||
|
||||
private const val UNLOCK_FRAGMENT_TAG = "UNLOCK_FRAGMENT_TAG"
|
||||
|
||||
private const val KEY_FILENAME = "fileName"
|
||||
private const val KEY_KEYFILE = "keyFile"
|
||||
private const val VIEW_INTENT = "android.intent.action.VIEW"
|
||||
|
||||
private const val KEY_PASSWORD = "password"
|
||||
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"
|
||||
private const val WRITE_EXTERNAL_STORAGE_REQUEST = 647
|
||||
|
||||
private const val ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT = "ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT"
|
||||
|
||||
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
|
||||
intentBuildLauncher: (Intent) -> Unit) {
|
||||
val intent = Intent(activity, PasswordActivity::class.java)
|
||||
intent.putExtra(KEY_FILENAME, databaseFile)
|
||||
if (keyFile != null)
|
||||
intent.putExtra(KEY_KEYFILE, keyFile)
|
||||
intentBuildLauncher.invoke(intent)
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Standard Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launch(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Share Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForSearchResult(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
searchInfo: SearchInfo) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||
activity,
|
||||
intent,
|
||||
searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Save Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForSaveResult(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
searchInfo: SearchInfo) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
EntrySelectionHelper.startActivityForSaveModeResult(
|
||||
activity,
|
||||
intent,
|
||||
searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Keyboard Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForKeyboardResult(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
searchInfo: SearchInfo?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
|
||||
activity,
|
||||
intent,
|
||||
searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Autofill Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForAutofillResult(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
AutofillHelper.startActivityForAutofillResult(
|
||||
activity,
|
||||
intent,
|
||||
autofillComponent,
|
||||
searchInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Registration Launch
|
||||
* -------------------------
|
||||
*/
|
||||
fun launchForRegistration(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
registerInfo: RegisterInfo?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||
activity,
|
||||
intent,
|
||||
registerInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Global Launch
|
||||
* -------------------------
|
||||
*/
|
||||
fun launch(activity: Activity,
|
||||
databaseUri: Uri,
|
||||
keyFile: Uri?,
|
||||
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
||||
onCancelSpecialMode: () -> Unit,
|
||||
onLaunchActivitySpecialMode: () -> Unit) {
|
||||
|
||||
try {
|
||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||
{
|
||||
PasswordActivity.launch(activity,
|
||||
databaseUri, keyFile)
|
||||
},
|
||||
{ searchInfo -> // Search Action
|
||||
PasswordActivity.launchForSearchResult(activity,
|
||||
databaseUri, keyFile,
|
||||
searchInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo -> // Save Action
|
||||
PasswordActivity.launchForSaveResult(activity,
|
||||
databaseUri, keyFile,
|
||||
searchInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo -> // Keyboard Selection Action
|
||||
PasswordActivity.launchForKeyboardResult(activity,
|
||||
databaseUri, keyFile,
|
||||
searchInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo, autofillComponent -> // Autofill Selection Action
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
PasswordActivity.launchForAutofillResult(activity,
|
||||
databaseUri, keyFile,
|
||||
autofillComponent,
|
||||
searchInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
} else {
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
},
|
||||
{ registerInfo -> // Registration Action
|
||||
PasswordActivity.launchForRegistration(activity,
|
||||
databaseUri, keyFile,
|
||||
registerInfo)
|
||||
onLaunchActivitySpecialMode()
|
||||
}
|
||||
)
|
||||
} catch (e: FileNotFoundException) {
|
||||
fileNoFoundAction(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,318 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||
|
||||
class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
|
||||
private var mMasterPassword: String? = null
|
||||
private var mKeyFile: Uri? = null
|
||||
|
||||
private var rootView: View? = null
|
||||
|
||||
private var passwordCheckBox: CompoundButton? = null
|
||||
|
||||
private var passwordTextInputLayout: TextInputLayout? = null
|
||||
private var passwordView: TextView? = null
|
||||
private var passwordRepeatTextInputLayout: TextInputLayout? = null
|
||||
private var passwordRepeatView: TextView? = null
|
||||
|
||||
private var keyFileCheckBox: CompoundButton? = null
|
||||
private var keyFileSelectionView: KeyFileSelectionView? = null
|
||||
|
||||
private var mListener: AssignPasswordDialogListener? = null
|
||||
|
||||
private var mSelectFileHelper: SelectFileHelper? = null
|
||||
|
||||
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
|
||||
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
||||
private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null
|
||||
|
||||
private val passwordTextWatcher = object : TextWatcher {
|
||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun afterTextChanged(editable: Editable) {
|
||||
passwordCheckBox?.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
interface AssignPasswordDialogListener {
|
||||
fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential)
|
||||
fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential)
|
||||
}
|
||||
|
||||
override fun onAttach(activity: Context) {
|
||||
super.onAttach(activity)
|
||||
try {
|
||||
mListener = activity as AssignPasswordDialogListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(activity.toString()
|
||||
+ " must implement " + AssignPasswordDialogListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
mEmptyPasswordConfirmationDialog?.dismiss()
|
||||
mEmptyPasswordConfirmationDialog = null
|
||||
mNoKeyConfirmationDialog?.dismiss()
|
||||
mNoKeyConfirmationDialog = null
|
||||
mEmptyKeyFileConfirmationDialog?.dismiss()
|
||||
mEmptyKeyFileConfirmationDialog = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
|
||||
var allowNoMasterKey = false
|
||||
arguments?.apply {
|
||||
if (containsKey(ALLOW_NO_MASTER_KEY_ARG))
|
||||
allowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false)
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
val inflater = activity.layoutInflater
|
||||
|
||||
rootView = inflater.inflate(R.layout.fragment_set_password, null)
|
||||
builder.setView(rootView)
|
||||
// Add action buttons
|
||||
.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)
|
||||
}
|
||||
|
||||
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
||||
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
|
||||
passwordView = rootView?.findViewById(R.id.pass_password)
|
||||
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
|
||||
passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password)
|
||||
|
||||
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
||||
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
||||
|
||||
mSelectFileHelper = SelectFileHelper(this)
|
||||
keyFileSelectionView?.apply {
|
||||
setOnClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
||||
setOnLongClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
||||
}
|
||||
|
||||
val dialog = builder.create()
|
||||
|
||||
if (passwordCheckBox != null && keyFileCheckBox!= null) {
|
||||
dialog.setOnShowListener { dialog1 ->
|
||||
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
positiveButton.setOnClickListener {
|
||||
|
||||
mMasterPassword = ""
|
||||
mKeyFile = null
|
||||
|
||||
var error = verifyPassword() || 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun retrieveMainCredential(): MainCredential {
|
||||
val masterPassword = if (passwordCheckBox!!.isChecked) mMasterPassword else null
|
||||
val keyFile = if (keyFileCheckBox!!.isChecked) mKeyFile else null
|
||||
return MainCredential(masterPassword, keyFile)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// To check checkboxes if a text is present
|
||||
passwordView?.addTextChangedListener(passwordTextWatcher)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
passwordView?.removeTextChangedListener(passwordTextWatcher)
|
||||
}
|
||||
|
||||
private fun verifyPassword(): Boolean {
|
||||
var error = false
|
||||
if (passwordCheckBox != null
|
||||
&& passwordCheckBox!!.isChecked
|
||||
&& passwordView != null
|
||||
&& passwordRepeatView != null) {
|
||||
mMasterPassword = passwordView!!.text.toString()
|
||||
val confPassword = passwordRepeatView!!.text.toString()
|
||||
|
||||
// Verify that passwords match
|
||||
if (mMasterPassword != confPassword) {
|
||||
error = true
|
||||
// Passwords do not match
|
||||
passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match)
|
||||
}
|
||||
|
||||
if (mMasterPassword == null || mMasterPassword!!.isEmpty()) {
|
||||
error = true
|
||||
showEmptyPasswordConfirmationDialog()
|
||||
}
|
||||
}
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
private fun 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
|
||||
}
|
||||
|
||||
private fun showEmptyPasswordConfirmationDialog() {
|
||||
activity?.let {
|
||||
val builder = AlertDialog.Builder(it)
|
||||
builder.setMessage(R.string.warning_empty_password)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
if (!verifyKeyFile()) {
|
||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||
this@AssignMasterKeyDialogFragment.dismiss()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
mEmptyPasswordConfirmationDialog = builder.create()
|
||||
mEmptyPasswordConfirmationDialog?.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showNoKeyConfirmationDialog() {
|
||||
activity?.let {
|
||||
val builder = AlertDialog.Builder(it)
|
||||
builder.setMessage(R.string.warning_no_encryption_key)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||
this@AssignMasterKeyDialogFragment.dismiss()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
mNoKeyConfirmationDialog = builder.create()
|
||||
mNoKeyConfirmationDialog?.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showEmptyKeyFileConfirmationDialog() {
|
||||
activity?.let {
|
||||
val builder = AlertDialog.Builder(it)
|
||||
builder.setMessage(SpannableStringBuilder().apply {
|
||||
append(getString(R.string.warning_empty_keyfile))
|
||||
append("\n\n")
|
||||
append(getString(R.string.warning_empty_keyfile_explanation))
|
||||
append("\n\n")
|
||||
append(getString(R.string.warning_sure_add_file))
|
||||
})
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
keyFileCheckBox?.isChecked = false
|
||||
keyFileSelectionView?.uri = null
|
||||
}
|
||||
mEmptyKeyFileConfirmationDialog = builder.create()
|
||||
mEmptyKeyFileConfirmationDialog?.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||
uri?.let { pathUri ->
|
||||
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
||||
keyFileSelectionView?.error = null
|
||||
keyFileCheckBox?.isChecked = true
|
||||
keyFileSelectionView?.uri = pathUri
|
||||
if (lengthFile <= 0L) {
|
||||
showEmptyKeyFileConfirmationDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
|
||||
|
||||
fun getInstance(allowNoMasterKey: Boolean): AssignMasterKeyDialogFragment {
|
||||
val fragment = AssignMasterKeyDialogFragment()
|
||||
val args = Bundle()
|
||||
args.putBoolean(ALLOW_NO_MASTER_KEY_ARG, allowNoMasterKey)
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.widget.CompoundButton
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.kunzisoft.androidclearchroma.view.ChromaColorView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
|
||||
|
||||
class ColorPickerDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private val mColorPickerViewModel: ColorPickerViewModel by activityViewModels()
|
||||
|
||||
private lateinit var enableSwitchView: CompoundButton
|
||||
private lateinit var chromaColorView: ChromaColorView
|
||||
|
||||
private var mDefaultColor = Color.WHITE
|
||||
private var mActivated = false
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
||||
activity?.let { activity ->
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_color_picker, null)
|
||||
enableSwitchView = root.findViewById(R.id.switch_element)
|
||||
chromaColorView = root.findViewById(R.id.chroma_color_view)
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
if (savedInstanceState.containsKey(ARG_INITIAL_COLOR)) {
|
||||
mDefaultColor = savedInstanceState.getInt(ARG_INITIAL_COLOR)
|
||||
}
|
||||
if (savedInstanceState.containsKey(ARG_ACTIVATED)) {
|
||||
mActivated = savedInstanceState.getBoolean(ARG_ACTIVATED)
|
||||
}
|
||||
} else {
|
||||
arguments?.apply {
|
||||
if (containsKey(ARG_INITIAL_COLOR)) {
|
||||
mDefaultColor = getInt(ARG_INITIAL_COLOR)
|
||||
}
|
||||
if (containsKey(ARG_ACTIVATED)) {
|
||||
mActivated = getBoolean(ARG_ACTIVATED)
|
||||
}
|
||||
}
|
||||
}
|
||||
enableSwitchView.isChecked = mActivated
|
||||
chromaColorView.currentColor = mDefaultColor
|
||||
|
||||
chromaColorView.setOnColorChangedListener {
|
||||
if (!enableSwitchView.isChecked)
|
||||
enableSwitchView.isChecked = true
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setView(root)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val color: Int? = if (enableSwitchView.isChecked)
|
||||
chromaColorView.currentColor
|
||||
else
|
||||
null
|
||||
mColorPickerViewModel.pickColor(color)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putInt(ARG_INITIAL_COLOR, chromaColorView.currentColor)
|
||||
outState.putBoolean(ARG_ACTIVATED, mActivated)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_INITIAL_COLOR = "ARG_INITIAL_COLOR"
|
||||
private const val ARG_ACTIVATED = "ARG_ACTIVATED"
|
||||
|
||||
fun newInstance(
|
||||
@ColorInt initialColor: Int?,
|
||||
): ColorPickerDialogFragment {
|
||||
return ColorPickerDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putInt(ARG_INITIAL_COLOR, initialColor ?: Color.WHITE)
|
||||
putBoolean(ARG_ACTIVATED, initialColor != null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,12 +23,12 @@ import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
|
||||
|
||||
class DatabaseChangedDialogFragment : DialogFragment() {
|
||||
class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
var actionDatabaseListener: ActionDatabaseChangedListener? = null
|
||||
|
||||
@@ -41,8 +41,9 @@ class DatabaseChangedDialogFragment : DialogFragment() {
|
||||
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)
|
||||
val readOnlyDatabase: Boolean = arguments?.getBoolean(READ_ONLY_DATABASE) ?: true
|
||||
|
||||
if (oldSnapFileDatabaseInfo != null && newSnapFileDatabaseInfo != null) {
|
||||
// Use the Builder class for convenient dialog construction
|
||||
@@ -54,13 +55,19 @@ class DatabaseChangedDialogFragment : DialogFragment() {
|
||||
stringBuilder.append("\n\n" +oldSnapFileDatabaseInfo.toString(activity)
|
||||
+ "\n→\n" +
|
||||
newSnapFileDatabaseInfo.toString(activity) + "\n\n")
|
||||
stringBuilder.append(getString(R.string.warning_database_info_changed_options))
|
||||
stringBuilder.append(getString(
|
||||
if (readOnlyDatabase) {
|
||||
R.string.warning_database_info_changed_options_read_only
|
||||
} else {
|
||||
R.string.warning_database_info_changed_options
|
||||
}
|
||||
))
|
||||
} else {
|
||||
stringBuilder.append(getString(R.string.warning_database_revoked))
|
||||
}
|
||||
builder.setMessage(stringBuilder)
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
actionDatabaseListener?.validateDatabaseChanged()
|
||||
actionDatabaseListener?.onDatabaseChangeValidated()
|
||||
}
|
||||
return builder.create()
|
||||
}
|
||||
@@ -69,7 +76,7 @@ class DatabaseChangedDialogFragment : DialogFragment() {
|
||||
}
|
||||
|
||||
interface ActionDatabaseChangedListener {
|
||||
fun validateDatabaseChanged()
|
||||
fun onDatabaseChangeValidated()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -77,14 +84,19 @@ class DatabaseChangedDialogFragment : DialogFragment() {
|
||||
const val DATABASE_CHANGED_DIALOG_TAG = "databaseChangedDialogFragment"
|
||||
private const val OLD_FILE_DATABASE_INFO = "OLD_FILE_DATABASE_INFO"
|
||||
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
|
||||
private const val READ_ONLY_DATABASE = "READ_ONLY_DATABASE"
|
||||
|
||||
fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||
newSnapFileDatabaseInfo: SnapFileDatabaseInfo)
|
||||
fun getInstance(
|
||||
oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||
newSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||
readOnly: Boolean
|
||||
)
|
||||
: DatabaseChangedDialogFragment {
|
||||
val fragment = DatabaseChangedDialogFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
putParcelable(OLD_FILE_DATABASE_INFO, oldSnapFileDatabaseInfo)
|
||||
putParcelable(NEW_FILE_DATABASE_INFO, newSnapFileDatabaseInfo)
|
||||
putBoolean(READ_ONLY_DATABASE, readOnly)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.WindowManager.LayoutParams.FLAG_SECURE
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
||||
|
||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||
private val mDatabase: ContextualDatabase?
|
||||
get() = mDatabaseViewModel.database
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
mDatabaseViewModel.actionState.collect { uiState ->
|
||||
when (uiState) {
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||
onDatabaseActionFinished(
|
||||
uiState.database,
|
||||
uiState.actionTask,
|
||||
uiState.result
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
mDatabaseViewModel.databaseState.collect { database ->
|
||||
database?.let {
|
||||
onDatabaseRetrieved(database)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
// Screenshot mode or hide views
|
||||
context?.let {
|
||||
if (PreferencesUtil.isScreenshotModeEnabled(it)) {
|
||||
dialog?.window?.clearFlags(FLAG_SECURE)
|
||||
} else {
|
||||
dialog?.window?.setFlags(FLAG_SECURE, FLAG_SECURE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated(message = "")
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
resetAppTimeoutOnTouchOrFocus()
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
// Can be overridden by a subclass
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
// Can be overridden by a subclass
|
||||
}
|
||||
|
||||
fun resetAppTimeout() {
|
||||
context?.let {
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(it,
|
||||
mDatabase?.loaded ?: false)
|
||||
}
|
||||
}
|
||||
|
||||
open fun overrideTimeoutTouchAndFocusEvents(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
private fun resetAppTimeoutOnTouchOrFocus() {
|
||||
if (!overrideTimeoutTouchAndFocusEvents()) {
|
||||
context?.let {
|
||||
dialog?.window?.decorView?.resetAppTimeoutWhenViewTouchedOrFocused(
|
||||
it,
|
||||
mDatabase?.loaded
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.DatePickerDialog
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
|
||||
class DatePickerFragment : DialogFragment() {
|
||||
|
||||
private var mDefaultYear: Int = 2000
|
||||
private var mDefaultMonth: Int = 1
|
||||
private var mDefaultDay: Int = 1
|
||||
|
||||
private var mListener: DatePickerDialog.OnDateSetListener? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
try {
|
||||
mListener = context as DatePickerDialog.OnDateSetListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,61 +20,38 @@
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
||||
import com.kunzisoft.keepass.viewmodels.NodesViewModel
|
||||
|
||||
open class DeleteNodesDialogFragment : DialogFragment() {
|
||||
class DeleteNodesDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mNodesToDelete: List<Node> = ArrayList()
|
||||
private var mListener: DeleteNodeListener? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
try {
|
||||
mListener = context as DeleteNodeListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + DeleteNodeListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
protected open fun retrieveMessage(): String {
|
||||
return getString(R.string.warning_permanently_delete_nodes)
|
||||
}
|
||||
private var mNodesToDelete: List<Node> = listOf()
|
||||
private val mNodesViewModel: NodesViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
||||
mNodesViewModel.nodesToDelete.observe(this) { nodes ->
|
||||
this.mNodesToDelete = nodes
|
||||
}
|
||||
var recycleBin = false
|
||||
arguments?.apply {
|
||||
if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY)
|
||||
&& containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) {
|
||||
mNodesToDelete = getListNodesFromBundle(Database.getInstance(), this)
|
||||
}
|
||||
} ?: savedInstanceState?.apply {
|
||||
if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY)
|
||||
&& containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) {
|
||||
mNodesToDelete = getListNodesFromBundle(Database.getInstance(), savedInstanceState)
|
||||
if (containsKey(RECYCLE_BIN_TAG)) {
|
||||
recycleBin = this.getBoolean(RECYCLE_BIN_TAG)
|
||||
}
|
||||
}
|
||||
activity?.let { activity ->
|
||||
// Use the Builder class for convenient dialog construction
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
builder.setMessage(retrieveMessage())
|
||||
builder.setMessage(if (recycleBin)
|
||||
getString(R.string.warning_empty_recycle_bin)
|
||||
else
|
||||
getString(R.string.warning_permanently_delete_nodes))
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mListener?.permanentlyDeleteNodes(mNodesToDelete)
|
||||
mNodesViewModel.permanentlyDeleteNodes(mNodesToDelete)
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||
// Create the AlertDialog object and return it
|
||||
@@ -83,19 +60,14 @@ open class DeleteNodesDialogFragment : DialogFragment() {
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putAll(getBundleFromListNodes(mNodesToDelete))
|
||||
}
|
||||
|
||||
interface DeleteNodeListener {
|
||||
fun permanentlyDeleteNodes(nodes: List<Node>)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getInstance(nodesToDelete: List<Node>): DeleteNodesDialogFragment {
|
||||
private const val RECYCLE_BIN_TAG = "RECYCLE_BIN_TAG"
|
||||
|
||||
fun getInstance(recycleBin: Boolean): DeleteNodesDialogFragment {
|
||||
return DeleteNodesDialogFragment().apply {
|
||||
arguments = getBundleFromListNodes(nodesToDelete)
|
||||
arguments = Bundle().apply {
|
||||
putBoolean(RECYCLE_BIN_TAG, recycleBin)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,14 +31,14 @@ import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Field
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.model.Field
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
|
||||
|
||||
class EntryCustomFieldDialogFragment: DialogFragment() {
|
||||
class EntryCustomFieldDialogFragment: DatabaseDialogFragment() {
|
||||
|
||||
private var oldField: Field? = null
|
||||
|
||||
@@ -73,7 +73,7 @@ class EntryCustomFieldDialogFragment: DialogFragment() {
|
||||
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,210 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.password.PasswordGenerator
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||
|
||||
class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
|
||||
private var mListener: GeneratePasswordListener? = null
|
||||
|
||||
private var root: View? = null
|
||||
private var lengthTextView: EditText? = null
|
||||
private var passwordInputLayoutView: TextInputLayout? = null
|
||||
private var passwordView: EditText? = null
|
||||
|
||||
private var uppercaseBox: CompoundButton? = null
|
||||
private var lowercaseBox: CompoundButton? = null
|
||||
private var digitsBox: CompoundButton? = null
|
||||
private var minusBox: CompoundButton? = null
|
||||
private var underlineBox: CompoundButton? = null
|
||||
private var spaceBox: CompoundButton? = null
|
||||
private var specialsBox: CompoundButton? = null
|
||||
private var bracketsBox: CompoundButton? = null
|
||||
private var extendedBox: CompoundButton? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
try {
|
||||
mListener = context as GeneratePasswordListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + GeneratePasswordListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun 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.allowCopyPasswordAndProtectedFields(activity))
|
||||
View.VISIBLE else View.GONE
|
||||
val clipboardHelper = ClipboardHelper(activity)
|
||||
passwordCopyView?.setOnClickListener {
|
||||
clipboardHelper.timeoutCopyToClipboard(passwordView!!.text.toString(),
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_password)))
|
||||
}
|
||||
|
||||
lengthTextView = root?.findViewById(R.id.length)
|
||||
|
||||
uppercaseBox = root?.findViewById(R.id.cb_uppercase)
|
||||
lowercaseBox = root?.findViewById(R.id.cb_lowercase)
|
||||
digitsBox = root?.findViewById(R.id.cb_digits)
|
||||
minusBox = root?.findViewById(R.id.cb_minus)
|
||||
underlineBox = root?.findViewById(R.id.cb_underline)
|
||||
spaceBox = root?.findViewById(R.id.cb_space)
|
||||
specialsBox = root?.findViewById(R.id.cb_specials)
|
||||
bracketsBox = root?.findViewById(R.id.cb_brackets)
|
||||
extendedBox = root?.findViewById(R.id.cb_extended)
|
||||
|
||||
assignDefaultCharacters()
|
||||
|
||||
val seekBar = root?.findViewById<SeekBar>(R.id.seekbar_length)
|
||||
seekBar?.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||
lengthTextView?.setText(progress.toString())
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||
})
|
||||
|
||||
context?.let { context ->
|
||||
seekBar?.progress = PreferencesUtil.getDefaultPasswordLength(context)
|
||||
}
|
||||
|
||||
root?.findViewById<Button>(R.id.generate_password_button)
|
||||
?.setOnClickListener { fillPassword() }
|
||||
|
||||
builder.setView(root)
|
||||
.setPositiveButton(R.string.accept) { _, _ ->
|
||||
val bundle = Bundle()
|
||||
bundle.putString(KEY_PASSWORD_ID, passwordView!!.text.toString())
|
||||
mListener?.acceptPassword(bundle)
|
||||
|
||||
dismiss()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
val bundle = Bundle()
|
||||
mListener?.cancelPassword(bundle)
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
// Pre-populate a password to possibly save the user a few clicks
|
||||
fillPassword()
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun assignDefaultCharacters() {
|
||||
uppercaseBox?.isChecked = false
|
||||
lowercaseBox?.isChecked = false
|
||||
digitsBox?.isChecked = false
|
||||
minusBox?.isChecked = false
|
||||
underlineBox?.isChecked = false
|
||||
spaceBox?.isChecked = false
|
||||
specialsBox?.isChecked = false
|
||||
bracketsBox?.isChecked = false
|
||||
extendedBox?.isChecked = false
|
||||
|
||||
context?.let { context ->
|
||||
PreferencesUtil.getDefaultPasswordCharacters(context)?.let { charSet ->
|
||||
for (passwordChar in charSet) {
|
||||
when (passwordChar) {
|
||||
getString(R.string.value_password_uppercase) -> uppercaseBox?.isChecked = true
|
||||
getString(R.string.value_password_lowercase) -> lowercaseBox?.isChecked = true
|
||||
getString(R.string.value_password_digits) -> digitsBox?.isChecked = true
|
||||
getString(R.string.value_password_minus) -> minusBox?.isChecked = true
|
||||
getString(R.string.value_password_underline) -> underlineBox?.isChecked = true
|
||||
getString(R.string.value_password_space) -> spaceBox?.isChecked = true
|
||||
getString(R.string.value_password_special) -> specialsBox?.isChecked = true
|
||||
getString(R.string.value_password_brackets) -> bracketsBox?.isChecked = true
|
||||
getString(R.string.value_password_extended) -> extendedBox?.isChecked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fillPassword() {
|
||||
root?.findViewById<EditText>(R.id.password)?.setText(generatePassword())
|
||||
}
|
||||
|
||||
fun generatePassword(): String {
|
||||
var password = ""
|
||||
try {
|
||||
val length = Integer.valueOf(root?.findViewById<EditText>(R.id.length)?.text.toString())
|
||||
password = PasswordGenerator(resources).generatePassword(length,
|
||||
uppercaseBox?.isChecked == true,
|
||||
lowercaseBox?.isChecked == true,
|
||||
digitsBox?.isChecked == true,
|
||||
minusBox?.isChecked == true,
|
||||
underlineBox?.isChecked == true,
|
||||
spaceBox?.isChecked == true,
|
||||
specialsBox?.isChecked == true,
|
||||
bracketsBox?.isChecked == true,
|
||||
extendedBox?.isChecked == true)
|
||||
passwordInputLayoutView?.error = null
|
||||
} catch (e: NumberFormatException) {
|
||||
passwordInputLayoutView?.error = getString(R.string.error_wrong_length)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
passwordInputLayoutView?.error = e.message
|
||||
}
|
||||
|
||||
return password
|
||||
}
|
||||
|
||||
interface GeneratePasswordListener {
|
||||
fun acceptPassword(bundle: Bundle)
|
||||
fun cancelPassword(bundle: Bundle)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY_PASSWORD_ID = "KEY_PASSWORD_ID"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.adapters.TagsAdapter
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.model.GroupInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
||||
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import com.kunzisoft.keepass.view.DateTimeFieldView
|
||||
|
||||
class GroupDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
|
||||
private var mGroupInfo = GroupInfo()
|
||||
|
||||
private lateinit var iconView: ImageView
|
||||
private var mIconColor: Int = 0
|
||||
private lateinit var nameTextView: TextView
|
||||
private lateinit var tagsListView: RecyclerView
|
||||
private var tagsAdapter: TagsAdapter? = null
|
||||
private lateinit var notesTextLabelView: TextView
|
||||
private lateinit var notesTextView: TextView
|
||||
private lateinit var expirationView: DateTimeFieldView
|
||||
private lateinit var creationView: TextView
|
||||
private lateinit var modificationView: TextView
|
||||
private lateinit var searchableLabelView: TextView
|
||||
private lateinit var searchableView: TextView
|
||||
private lateinit var autoTypeLabelView: TextView
|
||||
private lateinit var autoTypeView: TextView
|
||||
private lateinit var uuidContainerView: ViewGroup
|
||||
private lateinit var uuidReferenceView: TextView
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
mPopulateIconMethod = { imageView, icon ->
|
||||
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||
}
|
||||
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
|
||||
|
||||
if (database.allowCustomSearchableGroup()) {
|
||||
searchableLabelView.visibility = View.VISIBLE
|
||||
searchableView.visibility = View.VISIBLE
|
||||
} else {
|
||||
searchableLabelView.visibility = View.GONE
|
||||
searchableView.visibility = View.GONE
|
||||
}
|
||||
|
||||
// TODO Auto-Type
|
||||
/*
|
||||
if (database?.allowAutoType() == true) {
|
||||
autoTypeLabelView.visibility = View.VISIBLE
|
||||
autoTypeView.visibility = View.VISIBLE
|
||||
} else {
|
||||
autoTypeLabelView.visibility = View.GONE
|
||||
autoTypeView.visibility = View.GONE
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_group, null)
|
||||
iconView = root.findViewById(R.id.group_icon)
|
||||
nameTextView = root.findViewById(R.id.group_name)
|
||||
tagsListView = root.findViewById(R.id.group_tags_list_view)
|
||||
notesTextLabelView = root.findViewById(R.id.group_note_label)
|
||||
notesTextView = root.findViewById(R.id.group_note)
|
||||
expirationView = root.findViewById(R.id.group_expiration)
|
||||
creationView = root.findViewById(R.id.group_created)
|
||||
modificationView = root.findViewById(R.id.group_modified)
|
||||
searchableLabelView = root.findViewById(R.id.group_searchable_label)
|
||||
searchableView = root.findViewById(R.id.group_searchable)
|
||||
autoTypeLabelView = root.findViewById(R.id.group_auto_type_label)
|
||||
autoTypeView = root.findViewById(R.id.group_auto_type)
|
||||
uuidContainerView = root.findViewById(R.id.group_UUID_container)
|
||||
uuidReferenceView = root.findViewById(R.id.group_UUID_reference)
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val ta = activity.theme.obtainStyledAttributes(intArrayOf(R.attr.colorSecondary))
|
||||
mIconColor = ta.getColor(0, Color.WHITE)
|
||||
ta.recycle()
|
||||
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
|
||||
mGroupInfo = savedInstanceState.getParcelableCompat(KEY_GROUP_INFO) ?: mGroupInfo
|
||||
} else {
|
||||
arguments?.apply {
|
||||
if (containsKey(KEY_GROUP_INFO)) {
|
||||
mGroupInfo = getParcelableCompat(KEY_GROUP_INFO) ?: mGroupInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// populate info in views
|
||||
val title = mGroupInfo.title
|
||||
if (title.isEmpty()) {
|
||||
nameTextView.visibility = View.GONE
|
||||
} else {
|
||||
nameTextView.text = title
|
||||
nameTextView.visibility = View.VISIBLE
|
||||
}
|
||||
tagsAdapter = TagsAdapter(activity)
|
||||
tagsListView.apply {
|
||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||
adapter = tagsAdapter
|
||||
}
|
||||
val tags = mGroupInfo.tags
|
||||
tagsListView.visibility = if (tags.isEmpty()) View.GONE else View.VISIBLE
|
||||
tagsAdapter?.setTags(tags)
|
||||
val notes = mGroupInfo.notes
|
||||
if (notes == null || notes.isEmpty()) {
|
||||
notesTextLabelView.visibility = View.GONE
|
||||
notesTextView.visibility = View.GONE
|
||||
} else {
|
||||
notesTextView.text = notes
|
||||
notesTextLabelView.visibility = View.VISIBLE
|
||||
notesTextView.visibility = View.VISIBLE
|
||||
}
|
||||
expirationView.activation = mGroupInfo.expires
|
||||
expirationView.dateTime = mGroupInfo.expiryTime
|
||||
creationView.text = mGroupInfo.creationTime.getDateTimeString(resources)
|
||||
modificationView.text = mGroupInfo.lastModificationTime.getDateTimeString(resources)
|
||||
searchableView.text = stringFromInheritableBoolean(mGroupInfo.searchable)
|
||||
autoTypeView.text = stringFromInheritableBoolean(mGroupInfo.enableAutoType,
|
||||
mGroupInfo.defaultAutoTypeSequence)
|
||||
val uuid = mGroupInfo.id?.asHexString()
|
||||
if (uuid == null || uuid.isEmpty()) {
|
||||
uuidContainerView.visibility = View.GONE
|
||||
} else {
|
||||
uuidReferenceView.text = uuid
|
||||
uuidContainerView.apply {
|
||||
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setView(root)
|
||||
.setPositiveButton(android.R.string.ok){ _, _ ->
|
||||
// Do nothing
|
||||
}
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun stringFromInheritableBoolean(enable: Boolean?, value: String? = null): String {
|
||||
val valueString = if (value != null && value.isNotEmpty()) " [$value]" else ""
|
||||
return when {
|
||||
enable == null -> getString(R.string.inherited) + valueString
|
||||
enable -> getString(R.string.enable) + valueString
|
||||
else -> getString(R.string.disable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putParcelable(KEY_GROUP_INFO, mGroupInfo)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
data class Error(val isError: Boolean, val messageId: Int?)
|
||||
|
||||
companion object {
|
||||
const val TAG_SHOW_GROUP = "TAG_SHOW_GROUP"
|
||||
private const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
||||
|
||||
fun launch(groupInfo: GroupInfo): GroupDialogFragment {
|
||||
val bundle = Bundle()
|
||||
bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
|
||||
val fragment = GroupDialogFragment()
|
||||
fragment.arguments = bundle
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,43 +20,56 @@
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.IconPickerActivity
|
||||
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.database.element.Database
|
||||
import com.kunzisoft.keepass.adapters.TagsProposalAdapter
|
||||
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.view.ExpirationView
|
||||
import org.joda.time.DateTime
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import com.kunzisoft.keepass.view.DateTimeEditFieldView
|
||||
import com.kunzisoft.keepass.view.InheritedCompletionView
|
||||
import com.kunzisoft.keepass.view.TagsCompletionView
|
||||
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
|
||||
import com.tokenautocomplete.FilteredArrayAdapter
|
||||
|
||||
class GroupEditDialogFragment : DialogFragment() {
|
||||
class GroupEditDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
private val mGroupEditViewModel: GroupEditViewModel by activityViewModels()
|
||||
|
||||
private var mEditGroupListener: EditGroupListener? = null
|
||||
|
||||
private var mEditGroupDialogAction = EditGroupDialogAction.NONE
|
||||
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
|
||||
private var mEditGroupDialogAction = NONE
|
||||
private var mGroupInfo = GroupInfo()
|
||||
private var mGroupNamesNotAllowed: List<String>? = null
|
||||
|
||||
private lateinit var iconButtonView: ImageView
|
||||
private var iconColor: Int = 0
|
||||
private var mIconColor: Int = 0
|
||||
private lateinit var nameTextLayoutView: TextInputLayout
|
||||
private lateinit var nameTextView: TextView
|
||||
private lateinit var notesTextLayoutView: TextInputLayout
|
||||
private lateinit var notesTextView: TextView
|
||||
private lateinit var expirationView: ExpirationView
|
||||
private lateinit var expirationView: DateTimeEditFieldView
|
||||
private lateinit var searchableContainerView: TextInputLayout
|
||||
private lateinit var searchableView: InheritedCompletionView
|
||||
private lateinit var autoTypeContainerView: ViewGroup
|
||||
private lateinit var autoTypeInheritedView: InheritedCompletionView
|
||||
private lateinit var autoTypeSequenceView: TextView
|
||||
private lateinit var tagsContainerView: TextInputLayout
|
||||
private lateinit var tagsCompletionView: TagsCompletionView
|
||||
private var tagsAdapter: FilteredArrayAdapter<String>? = null
|
||||
|
||||
enum class EditGroupDialogAction {
|
||||
CREATION, UPDATE, NONE;
|
||||
@@ -68,22 +81,63 @@ class GroupEditDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
// Verify that the host activity implements the callback interface
|
||||
try {
|
||||
// Instantiate the NoticeDialogListener so we can send events to the host
|
||||
mEditGroupListener = context as EditGroupListener
|
||||
} catch (e: ClassCastException) {
|
||||
// The activity doesn't implement the interface, throw exception
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + GroupEditDialogFragment::class.java.name)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
mGroupEditViewModel.onIconSelected.observe(this) { iconImage ->
|
||||
mGroupInfo.icon = iconImage
|
||||
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
||||
}
|
||||
|
||||
mGroupEditViewModel.onDateSelected.observe(this) { date ->
|
||||
// Save the date
|
||||
mGroupInfo.expiryTime.setDate(date.year, date.month, date.day)
|
||||
expirationView.dateTime = mGroupInfo.expiryTime
|
||||
if (expirationView.dateTime.type == DateInstant.Type.DATE_TIME) {
|
||||
// Trick to recall selection with time
|
||||
mGroupEditViewModel.requestDateTimeSelection(
|
||||
DateInstant(mGroupInfo.expiryTime.instant, DateInstant.Type.TIME)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
mGroupEditViewModel.onTimeSelected.observe(this) { viewModelTime ->
|
||||
// Save the time
|
||||
mGroupInfo.expiryTime.setTime(viewModelTime.hour, viewModelTime.minute)
|
||||
expirationView.dateTime = mGroupInfo.expiryTime
|
||||
}
|
||||
|
||||
mGroupEditViewModel.groupNamesNotAllowed.observe(this) { namesNotAllowed ->
|
||||
this.mGroupNamesNotAllowed = namesNotAllowed
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mEditGroupListener = null
|
||||
super.onDetach()
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
|
||||
mPopulateIconMethod = { imageView, icon ->
|
||||
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||
}
|
||||
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
||||
|
||||
searchableContainerView.visibility = if (database.allowCustomSearchableGroup()) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
if (database.allowAutoType()) {
|
||||
autoTypeContainerView.visibility = View.VISIBLE
|
||||
} else {
|
||||
autoTypeContainerView.visibility = View.GONE
|
||||
}
|
||||
|
||||
tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
|
||||
tagsCompletionView.apply {
|
||||
threshold = 1
|
||||
setAdapter(tagsAdapter)
|
||||
}
|
||||
tagsContainerView.visibility = if (database.allowTags()) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
@@ -95,57 +149,51 @@ class GroupEditDialogFragment : DialogFragment() {
|
||||
notesTextLayoutView = root.findViewById(R.id.group_edit_note_container)
|
||||
notesTextView = root.findViewById(R.id.group_edit_note)
|
||||
expirationView = root.findViewById(R.id.group_edit_expiration)
|
||||
searchableContainerView = root.findViewById(R.id.group_edit_searchable_container)
|
||||
searchableView = root.findViewById(R.id.group_edit_searchable)
|
||||
autoTypeContainerView = root.findViewById(R.id.group_edit_auto_type_container)
|
||||
autoTypeInheritedView = root.findViewById(R.id.group_edit_auto_type_inherited)
|
||||
autoTypeSequenceView = root.findViewById(R.id.group_edit_auto_type_sequence)
|
||||
tagsContainerView = root.findViewById(R.id.group_tags_label)
|
||||
tagsCompletionView = root.findViewById(R.id.group_tags_completion_view)
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
iconColor = ta.getColor(0, Color.WHITE)
|
||||
mIconColor = ta.getColor(0, Color.WHITE)
|
||||
ta.recycle()
|
||||
|
||||
// Init elements
|
||||
mDatabase = Database.getInstance()
|
||||
|
||||
if (savedInstanceState != null
|
||||
&& 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// populate info in views
|
||||
populateInfoToViews()
|
||||
expirationView.setOnDateClickListener = {
|
||||
expirationView.expiryTime.date.let { expiresDate ->
|
||||
val dateTime = DateTime(expiresDate)
|
||||
val defaultYear = dateTime.year
|
||||
val defaultMonth = dateTime.monthOfYear-1
|
||||
val defaultDay = dateTime.dayOfMonth
|
||||
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
|
||||
.show(parentFragmentManager, "DatePickerFragment")
|
||||
}
|
||||
populateInfoToViews(mGroupInfo)
|
||||
|
||||
iconButtonView.setOnClickListener { _ ->
|
||||
mGroupEditViewModel.requestIconSelection(mGroupInfo.icon)
|
||||
}
|
||||
expirationView.setOnDateClickListener = { dateInstant ->
|
||||
mGroupEditViewModel.requestDateTimeSelection(dateInstant)
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setView(root)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
retrieveGroupInfoFromViews()
|
||||
mEditGroupListener?.cancelEditGroup(
|
||||
mEditGroupDialogAction,
|
||||
mGroupInfo)
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
iconButtonView.setOnClickListener { _ ->
|
||||
IconPickerActivity.launch(activity, mGroupInfo.icon)
|
||||
}
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
@@ -155,60 +203,58 @@ class GroupEditDialogFragment : DialogFragment() {
|
||||
super.onResume()
|
||||
|
||||
// To prevent auto dismiss
|
||||
val d = dialog as AlertDialog?
|
||||
if (d != null) {
|
||||
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
|
||||
val alertDialog = dialog as AlertDialog?
|
||||
if (alertDialog != null) {
|
||||
val positiveButton = alertDialog.getButton(Dialog.BUTTON_POSITIVE) as Button
|
||||
positiveButton.setOnClickListener {
|
||||
retrieveGroupInfoFromViews()
|
||||
if (isValid()) {
|
||||
mEditGroupListener?.approveEditGroup(
|
||||
mEditGroupDialogAction,
|
||||
mGroupInfo)
|
||||
d.dismiss()
|
||||
when (mEditGroupDialogAction) {
|
||||
CREATION ->
|
||||
mGroupEditViewModel.approveGroupCreation(mGroupInfo)
|
||||
UPDATE ->
|
||||
mGroupEditViewModel.approveGroupUpdate(mGroupInfo)
|
||||
NONE -> {}
|
||||
}
|
||||
alertDialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getExpiryTime(): DateInstant {
|
||||
retrieveGroupInfoFromViews()
|
||||
return mGroupInfo.expiryTime
|
||||
}
|
||||
|
||||
fun setExpiryTime(expiryTime: DateInstant) {
|
||||
mGroupInfo.expiryTime = expiryTime
|
||||
populateInfoToViews()
|
||||
}
|
||||
|
||||
private fun populateInfoToViews() {
|
||||
assignIconView()
|
||||
nameTextView.text = mGroupInfo.title
|
||||
notesTextLayoutView.visibility = if (mGroupInfo.notes == null) View.GONE else View.VISIBLE
|
||||
mGroupInfo.notes?.let {
|
||||
private fun populateInfoToViews(groupInfo: GroupInfo) {
|
||||
mGroupEditViewModel.selectIcon(groupInfo.icon)
|
||||
nameTextView.text = groupInfo.title
|
||||
notesTextLayoutView.visibility = if (groupInfo.notes == null) View.GONE else View.VISIBLE
|
||||
groupInfo.notes?.let {
|
||||
notesTextView.text = it
|
||||
}
|
||||
expirationView.expires = mGroupInfo.expires
|
||||
expirationView.expiryTime = mGroupInfo.expiryTime
|
||||
expirationView.activation = groupInfo.expires
|
||||
expirationView.dateTime = groupInfo.expiryTime
|
||||
|
||||
// Set searchable
|
||||
searchableView.setValue(groupInfo.searchable)
|
||||
// Set auto-type
|
||||
autoTypeInheritedView.setValue(groupInfo.enableAutoType)
|
||||
autoTypeSequenceView.text = groupInfo.defaultAutoTypeSequence
|
||||
// Set Tags
|
||||
groupInfo.tags.let { tags ->
|
||||
tagsCompletionView.setText("")
|
||||
for (i in 0 until tags.size()) {
|
||||
tagsCompletionView.addObjectSync(tags.get(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun retrieveGroupInfoFromViews() {
|
||||
mGroupInfo.title = nameTextView.text.toString()
|
||||
// Only if there
|
||||
val newNotes = notesTextView.text.toString()
|
||||
if (newNotes.isNotEmpty()) {
|
||||
mGroupInfo.notes = newNotes
|
||||
}
|
||||
mGroupInfo.expires = expirationView.expires
|
||||
mGroupInfo.expiryTime = expirationView.expiryTime
|
||||
}
|
||||
|
||||
private fun assignIconView() {
|
||||
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconButtonView, mGroupInfo.icon, iconColor)
|
||||
}
|
||||
|
||||
fun setIcon(icon: IconImage) {
|
||||
mGroupInfo.icon = icon
|
||||
assignIconView()
|
||||
mGroupInfo.notes = notesTextView.text?.toString()
|
||||
mGroupInfo.expires = expirationView.activation
|
||||
mGroupInfo.expiryTime = expirationView.dateTime
|
||||
mGroupInfo.searchable = searchableView.getValue()
|
||||
mGroupInfo.enableAutoType = autoTypeInheritedView.getValue()
|
||||
mGroupInfo.defaultAutoTypeSequence = autoTypeSequenceView.text.toString()
|
||||
mGroupInfo.tags = tagsCompletionView.getTags()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@@ -219,25 +265,36 @@ class GroupEditDialogFragment : DialogFragment() {
|
||||
}
|
||||
|
||||
private fun isValid(): Boolean {
|
||||
if (nameTextView.text.toString().isEmpty()) {
|
||||
nameTextLayoutView.error = getString(R.string.error_no_name)
|
||||
return false
|
||||
val name = nameTextView.text.toString()
|
||||
val error = when {
|
||||
name.isEmpty() -> {
|
||||
Error(true, R.string.error_no_name)
|
||||
}
|
||||
mGroupNamesNotAllowed == null -> {
|
||||
Error(true, R.string.error_word_reserved)
|
||||
}
|
||||
mGroupNamesNotAllowed?.find { it.equals(name, ignoreCase = true) } != null -> {
|
||||
Error(true, R.string.error_word_reserved)
|
||||
}
|
||||
else -> {
|
||||
Error(false, null)
|
||||
}
|
||||
}
|
||||
return true
|
||||
error.messageId?.let { messageId ->
|
||||
nameTextLayoutView.error = getString(messageId)
|
||||
} ?: kotlin.run {
|
||||
nameTextLayoutView.error = null
|
||||
}
|
||||
return !error.isError
|
||||
}
|
||||
|
||||
interface EditGroupListener {
|
||||
fun approveEditGroup(action: EditGroupDialogAction,
|
||||
groupInfo: GroupInfo)
|
||||
fun cancelEditGroup(action: EditGroupDialogAction,
|
||||
groupInfo: GroupInfo)
|
||||
}
|
||||
data class Error(val isError: Boolean, val messageId: Int?)
|
||||
|
||||
companion object {
|
||||
|
||||
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
|
||||
const val KEY_ACTION_ID = "KEY_ACTION_ID"
|
||||
const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
||||
private const val KEY_ACTION_ID = "KEY_ACTION_ID"
|
||||
private const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
||||
|
||||
fun create(groupInfo: GroupInfo): GroupEditDialogFragment {
|
||||
val bundle = Bundle()
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||
|
||||
class IconEditDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private val mIconPickerViewModel: IconPickerViewModel by activityViewModels()
|
||||
|
||||
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
|
||||
private lateinit var iconView: ImageView
|
||||
private lateinit var nameTextLayoutView: TextInputLayout
|
||||
private lateinit var nameTextView: TextView
|
||||
|
||||
private var mCustomIcon: IconImageCustom? = null
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
super.onDatabaseRetrieved(database)
|
||||
mPopulateIconMethod = { imageView, icon ->
|
||||
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon)
|
||||
}
|
||||
mCustomIcon?.let { customIcon ->
|
||||
populateViewsWithCustomIcon(customIcon)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_icon_edit, null)
|
||||
iconView = root.findViewById(R.id.icon_edit_image)
|
||||
nameTextLayoutView = root.findViewById(R.id.icon_edit_name_container)
|
||||
nameTextView = root.findViewById(R.id.icon_edit_name)
|
||||
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(KEY_CUSTOM_ICON_ID)) {
|
||||
mCustomIcon = savedInstanceState.getParcelableCompat(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
|
||||
} else {
|
||||
arguments?.apply {
|
||||
if (containsKey(KEY_CUSTOM_ICON_ID)) {
|
||||
mCustomIcon = getParcelableCompat(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setView(root)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
retrieveIconInfoFromViews()
|
||||
mCustomIcon?.let { customIcon ->
|
||||
mIconPickerViewModel.updateCustomIcon(
|
||||
IconPickerViewModel.IconCustomState(customIcon, false)
|
||||
)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
// Do nothing
|
||||
mIconPickerViewModel.updateCustomIcon(
|
||||
IconPickerViewModel.IconCustomState(null, false)
|
||||
)
|
||||
}
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun populateViewsWithCustomIcon(customIcon: IconImageCustom) {
|
||||
mPopulateIconMethod?.invoke(iconView, customIcon.getIconImageToDraw())
|
||||
nameTextView.text = customIcon.name
|
||||
}
|
||||
|
||||
private fun retrieveIconInfoFromViews() {
|
||||
mCustomIcon?.name = nameTextView.text.toString()
|
||||
mCustomIcon?.lastModificationTime = DateInstant()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
retrieveIconInfoFromViews()
|
||||
outState.putParcelable(KEY_CUSTOM_ICON_ID, mCustomIcon)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val TAG_UPDATE_ICON = "TAG_UPDATE_ICON"
|
||||
const val KEY_CUSTOM_ICON_ID = "KEY_CUSTOM_ICON_ID"
|
||||
|
||||
fun update(customIcon: IconImageCustom): IconEditDialogFragment {
|
||||
val bundle = Bundle()
|
||||
bundle.putParcelable(KEY_CUSTOM_ICON_ID, IconImageCustom(customIcon))
|
||||
val fragment = IconEditDialogFragment()
|
||||
fragment.arguments = bundle
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright 2022 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import com.kunzisoft.keepass.view.MainCredentialView
|
||||
|
||||
class MainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mainCredentialView: MainCredentialView? = null
|
||||
|
||||
private var mListener: AskMainCredentialDialogListener? = null
|
||||
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
interface AskMainCredentialDialogListener {
|
||||
fun onAskMainCredentialDialogPositiveClick(databaseUri: Uri?, mainCredential: MainCredential)
|
||||
fun onAskMainCredentialDialogNegativeClick(databaseUri: Uri?, mainCredential: MainCredential)
|
||||
}
|
||||
|
||||
override fun onAttach(activity: Context) {
|
||||
super.onAttach(activity)
|
||||
try {
|
||||
mListener = activity as AskMainCredentialDialogListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(activity.toString()
|
||||
+ " must implement " + AskMainCredentialDialogListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
|
||||
var databaseUri: Uri? = null
|
||||
arguments?.apply {
|
||||
if (containsKey(KEY_ASK_CREDENTIAL_URI))
|
||||
databaseUri = getParcelableCompat(KEY_ASK_CREDENTIAL_URI)
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_main_credential, null)
|
||||
mainCredentialView = root.findViewById(R.id.main_credential_view)
|
||||
databaseUri?.let {
|
||||
root.findViewById<TextView>(R.id.title_database)?.text =
|
||||
it.getDocumentFile(requireContext())?.name
|
||||
}
|
||||
builder.setView(root)
|
||||
// Add action buttons
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mListener?.onAskMainCredentialDialogPositiveClick(
|
||||
databaseUri,
|
||||
retrieveMainCredential()
|
||||
)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
mListener?.onAskMainCredentialDialogNegativeClick(
|
||||
databaseUri,
|
||||
retrieveMainCredential()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
if (uri != null) {
|
||||
mainCredentialView?.populateKeyFileView(uri)
|
||||
}
|
||||
}
|
||||
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun retrieveMainCredential(): MainCredential {
|
||||
return mainCredentialView?.getMainCredential() ?: MainCredential()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_ASK_CREDENTIAL_URI = "KEY_ASK_CREDENTIAL_URI"
|
||||
const val TAG_ASK_MAIN_CREDENTIAL = "TAG_ASK_MAIN_CREDENTIAL"
|
||||
|
||||
fun getInstance(uri: Uri?): MainCredentialDialogFragment {
|
||||
val fragment = MainCredentialDialogFragment()
|
||||
val args = Bundle()
|
||||
args.putParcelable(KEY_ASK_CREDENTIAL_URI, uri)
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,14 +19,15 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
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)
|
||||
|
||||
@@ -25,14 +25,14 @@ import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
|
||||
/**
|
||||
* Custom Dialog to confirm big file to upload
|
||||
*/
|
||||
class ReplaceFileDialogFragment : DialogFragment() {
|
||||
class ReplaceFileDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mActionChooseListener: ActionChooseListener? = null
|
||||
|
||||
@@ -63,8 +63,8 @@ class ReplaceFileDialogFragment : DialogFragment() {
|
||||
})
|
||||
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()
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
|
||||
import com.kunzisoft.keepass.database.MainCredential
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
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.PasswordEditView
|
||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.security.SecureRandom
|
||||
|
||||
|
||||
class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mMasterPassword: String? = null
|
||||
private var mKeyFileUri: Uri? = null
|
||||
private var mHardwareKey: HardwareKey? = null
|
||||
|
||||
private lateinit var rootView: View
|
||||
|
||||
private lateinit var passwordCheckBox: CompoundButton
|
||||
private lateinit var passwordEditView: PasswordEditView
|
||||
private lateinit var passwordRepeatTextInputLayout: TextInputLayout
|
||||
private lateinit var passwordRepeatView: TextView
|
||||
|
||||
private lateinit var keyFileCheckBox: CompoundButton
|
||||
private lateinit var keyFileGenerateButton: View
|
||||
private lateinit var keyFileSelectionView: KeyFileSelectionView
|
||||
|
||||
private lateinit var hardwareKeyCheckBox: CompoundButton
|
||||
private lateinit var hardwareKeySelectionView: HardwareKeySelectionView
|
||||
|
||||
private var mListener: AssignMainCredentialDialogListener? = null
|
||||
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
private var mPasswordEntropyCalculator: PasswordEntropy? = null
|
||||
|
||||
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
|
||||
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
||||
private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null
|
||||
|
||||
private var mAllowNoMasterKey: Boolean = false
|
||||
|
||||
private val passwordTextWatcher = object : TextWatcher {
|
||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun afterTextChanged(editable: Editable) {
|
||||
passwordCheckBox.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
interface AssignMainCredentialDialogListener {
|
||||
fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential)
|
||||
fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential)
|
||||
}
|
||||
|
||||
override fun onAttach(activity: Context) {
|
||||
super.onAttach(activity)
|
||||
try {
|
||||
mListener = activity as AssignMainCredentialDialogListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(activity.toString()
|
||||
+ " must implement " + AssignMainCredentialDialogListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
mEmptyPasswordConfirmationDialog?.dismiss()
|
||||
mEmptyPasswordConfirmationDialog = null
|
||||
mNoKeyConfirmationDialog?.dismiss()
|
||||
mNoKeyConfirmationDialog = null
|
||||
mEmptyKeyFileConfirmationDialog?.dismiss()
|
||||
mEmptyKeyFileConfirmationDialog = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Create the password entropy object
|
||||
mPasswordEntropyCalculator = PasswordEntropy()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
|
||||
arguments?.apply {
|
||||
if (containsKey(ALLOW_NO_MASTER_KEY_ARG))
|
||||
mAllowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false)
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
val inflater = activity.layoutInflater
|
||||
|
||||
rootView = inflater.inflate(R.layout.fragment_set_main_credential, null)
|
||||
builder.setView(rootView)
|
||||
// Add action buttons
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
|
||||
rootView.findViewById<View>(R.id.credentials_information)?.setOnClickListener {
|
||||
activity.openUrl(R.string.credentials_explanation_url)
|
||||
}
|
||||
|
||||
passwordCheckBox = rootView.findViewById(R.id.password_checkbox)
|
||||
passwordEditView = rootView.findViewById(R.id.password_view)
|
||||
passwordRepeatTextInputLayout = rootView.findViewById(R.id.password_repeat_input_layout)
|
||||
passwordRepeatView = rootView.findViewById(R.id.password_confirmation)
|
||||
passwordRepeatView.applyFontVisibility()
|
||||
|
||||
keyFileCheckBox = rootView.findViewById(R.id.keyfile_checkbox)
|
||||
keyFileGenerateButton = rootView.findViewById(R.id.keyfile_generate)
|
||||
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?.buildCreateDocument { createdFileUri ->
|
||||
createdFileUri?.let { uri ->
|
||||
createKeyFile(uri)
|
||||
keyFileSelectionView.error = null
|
||||
keyFileCheckBox.isChecked = true
|
||||
keyFileSelectionView.uri = uri
|
||||
}
|
||||
}
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
uri?.let { pathUri ->
|
||||
pathUri.getDocumentFile(requireContext())?.length()?.let { lengthFile ->
|
||||
keyFileSelectionView.error = null
|
||||
keyFileCheckBox.isChecked = true
|
||||
keyFileSelectionView.uri = pathUri
|
||||
showLengthKeyFileConfirmationDialog(lengthFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
keyFileGenerateButton.setOnClickListener {
|
||||
mExternalFileHelper?.createDocument(DEFAULT_KEYFILE_NAME)
|
||||
}
|
||||
keyFileSelectionView.setOpenDocumentClickListener(mExternalFileHelper)
|
||||
|
||||
hardwareKeySelectionView.selectionListener = { hardwareKey ->
|
||||
hardwareKeyCheckBox.isChecked = true
|
||||
hardwareKeySelectionView.error =
|
||||
if (!HardwareKeyActivity.isHardwareKeyAvailable(requireActivity(), hardwareKey)) {
|
||||
// show hardware driver dialog if required
|
||||
getString(R.string.error_driver_required, hardwareKey.toString())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val dialog = builder.create()
|
||||
dialog.setOnShowListener { dialog1 ->
|
||||
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
positiveButton.setOnClickListener {
|
||||
|
||||
mMasterPassword = ""
|
||||
mKeyFileUri = null
|
||||
mHardwareKey = null
|
||||
|
||||
approveMainCredential()
|
||||
}
|
||||
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
|
||||
negativeButton.setOnClickListener {
|
||||
mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun createKeyFile(uri: Uri) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
activity?.contentResolver?.openOutputStream(uri)?.use { outputStream ->
|
||||
val randomBytes = ByteArray(DEFAULT_KEYFILE_SIZE)
|
||||
SecureRandom().nextBytes(randomBytes)
|
||||
outputStream.write(randomBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
) {
|
||||
// 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 = passwordEditView.passwordString
|
||||
val confPassword = passwordRepeatView.text.toString()
|
||||
|
||||
// Verify that passwords match
|
||||
if (mMasterPassword != confPassword) {
|
||||
error = true
|
||||
// Passwords do not match
|
||||
passwordRepeatTextInputLayout.error = getString(R.string.error_pass_match)
|
||||
}
|
||||
}
|
||||
return error
|
||||
}
|
||||
|
||||
private fun verifyKeyFile(): Boolean {
|
||||
var error = false
|
||||
keyFileSelectionView.error = null
|
||||
if (keyFileCheckBox.isChecked) {
|
||||
keyFileSelectionView.uri?.let { uri ->
|
||||
mKeyFileUri = uri
|
||||
} ?: run {
|
||||
error = true
|
||||
keyFileSelectionView.error = getString(R.string.error_nokeyfile)
|
||||
}
|
||||
}
|
||||
return error
|
||||
}
|
||||
|
||||
private fun verifyHardwareKey(): Boolean {
|
||||
var error = false
|
||||
hardwareKeySelectionView.error = null
|
||||
if (hardwareKeyCheckBox.isChecked) {
|
||||
hardwareKeySelectionView.hardwareKey?.let { hardwareKey ->
|
||||
mHardwareKey = hardwareKey
|
||||
} ?: run {
|
||||
error = true
|
||||
hardwareKeySelectionView.error = getString(R.string.error_no_hardware_key)
|
||||
}
|
||||
}
|
||||
return error
|
||||
}
|
||||
|
||||
private fun retrieveMainCredential(): MainCredential {
|
||||
val masterPassword = if (passwordCheckBox.isChecked) mMasterPassword else null
|
||||
val keyFileUri = if (keyFileCheckBox.isChecked) mKeyFileUri else null
|
||||
val hardwareKey = if (hardwareKeyCheckBox.isChecked) mHardwareKey else null
|
||||
return MainCredential(masterPassword, keyFileUri, hardwareKey)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// To check checkboxes if a text is present
|
||||
passwordEditView.addTextChangedListener(passwordTextWatcher)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
passwordEditView.removeTextChangedListener(passwordTextWatcher)
|
||||
}
|
||||
|
||||
private fun showEmptyPasswordConfirmationDialog() {
|
||||
activity?.let {
|
||||
val builder = AlertDialog.Builder(it)
|
||||
builder.setMessage(R.string.warning_empty_password)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||
this@SetMainCredentialDialogFragment.dismiss()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
mEmptyPasswordConfirmationDialog = builder.create()
|
||||
mEmptyPasswordConfirmationDialog?.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showNoKeyConfirmationDialog() {
|
||||
activity?.let {
|
||||
val builder = AlertDialog.Builder(it)
|
||||
builder.setMessage(R.string.warning_no_encryption_key)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||
this@SetMainCredentialDialogFragment.dismiss()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
mNoKeyConfirmationDialog = builder.create()
|
||||
mNoKeyConfirmationDialog?.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showLengthKeyFileConfirmationDialog(length: Long) {
|
||||
activity?.let {
|
||||
val builder = AlertDialog.Builder(it)
|
||||
builder.setMessage(SpannableStringBuilder().apply {
|
||||
append(getString(R.string.warning_empty_keyfile_explanation))
|
||||
var warning = false
|
||||
if (length <= 0L) {
|
||||
warning = true
|
||||
append("\n\n")
|
||||
append(getString(R.string.warning_empty_keyfile))
|
||||
} else if (length > 10485760L) {
|
||||
warning = true
|
||||
append("\n\n")
|
||||
append(getString(R.string.warning_large_keyfile))
|
||||
}
|
||||
if (warning) {
|
||||
append("\n\n")
|
||||
append(getString(R.string.warning_sure_add_file))
|
||||
}
|
||||
})
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
keyFileCheckBox.isChecked = false
|
||||
keyFileSelectionView.uri = null
|
||||
}
|
||||
mEmptyKeyFileConfirmationDialog = builder.create()
|
||||
mEmptyKeyFileConfirmationDialog?.show()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
|
||||
private const val DEFAULT_KEYFILE_NAME = "keyfile.bin"
|
||||
private const val DEFAULT_KEYFILE_SIZE = 128
|
||||
|
||||
fun getInstance(allowNoMasterKey: Boolean): SetMainCredentialDialogFragment {
|
||||
val fragment = SetMainCredentialDialogFragment()
|
||||
val args = Bundle()
|
||||
args.putBoolean(ALLOW_NO_MASTER_KEY_ARG, allowNoMasterKey)
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,11 +29,13 @@ import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.*
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.BuildConfig
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.model.OtpModel
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
@@ -42,14 +44,17 @@ import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_OTP_DIGITS
|
||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_TOTP_PERIOD
|
||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_HOTP_COUNTER
|
||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_DIGITS
|
||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_SECRET
|
||||
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 java.util.*
|
||||
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
|
||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||
import java.util.Locale
|
||||
|
||||
class SetOTPDialogFragment : DialogFragment() {
|
||||
class SetOTPDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mCreateOTPElementListener: CreateOtpListener? = null
|
||||
|
||||
@@ -80,11 +85,15 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
|
||||
if (!isFocus)
|
||||
mManualEvent = true
|
||||
else
|
||||
resetAppTimeout()
|
||||
}
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private var mOnTouchListener = View.OnTouchListener { _, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
mManualEvent = true
|
||||
resetAppTimeout()
|
||||
}
|
||||
}
|
||||
false
|
||||
@@ -95,6 +104,10 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
private var mPeriodWellFormed = false
|
||||
private var mDigitsWellFormed = false
|
||||
|
||||
override fun overrideTimeoutTouchAndFocusEvents(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
// Verify that the host activity implements the callback interface
|
||||
@@ -119,14 +132,14 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -197,9 +210,10 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
android.R.layout.simple_spinner_item, mHotpTokenTypeArray!!).apply {
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
}
|
||||
// Proprietary only on closed and full version
|
||||
// Proprietary only on full version
|
||||
mTotpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
|
||||
BuildConfig.CLOSED_STORE && BuildConfig.FULL_VERSION)
|
||||
activity.isContributingUser()
|
||||
)
|
||||
totpTokenTypeAdapter = ArrayAdapter(activity,
|
||||
android.R.layout.simple_spinner_item, mTotpTokenTypeArray!!).apply {
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
@@ -215,6 +229,9 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
}
|
||||
otpAlgorithmSpinner?.adapter = otpAlgorithmAdapter
|
||||
|
||||
// Ensure that the UX does not prevent user from hiding/unhiding text
|
||||
otpSecretContainer?.errorIconDrawable = null
|
||||
|
||||
// Set the default value of OTP element
|
||||
upgradeType()
|
||||
upgradeTokenType()
|
||||
@@ -225,13 +242,16 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.apply {
|
||||
setView(root)
|
||||
.setPositiveButton(android.R.string.ok) {_, _ -> }
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
resetAppTimeout()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
resetAppTimeout()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -298,11 +318,16 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
otpSecretTextView?.addTextChangedListener(object: TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
s?.toString()?.let { userString ->
|
||||
try {
|
||||
mOtpElement.setBase32Secret(userString.toUpperCase(Locale.ENGLISH))
|
||||
otpSecretContainer?.error = null
|
||||
} catch (exception: Exception) {
|
||||
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
|
||||
if (userString.length >= MIN_OTP_SECRET) {
|
||||
try {
|
||||
mOtpElement.setBase32Secret(userString.uppercase(Locale.ENGLISH))
|
||||
otpSecretContainer?.error = null
|
||||
} catch (exception: Exception) {
|
||||
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
|
||||
}
|
||||
} else {
|
||||
otpSecretContainer?.error = getString(R.string.error_otp_secret_length,
|
||||
MIN_OTP_SECRET)
|
||||
}
|
||||
mSecretWellFormed = otpSecretContainer?.error == null
|
||||
}
|
||||
|
||||
@@ -22,16 +22,15 @@ package com.kunzisoft.keepass.activities.dialogs
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.RadioGroup
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||
|
||||
class SortDialogFragment : DialogFragment() {
|
||||
class SortDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
private var mListener: SortSelectionListener? = null
|
||||
|
||||
@@ -177,21 +176,14 @@ class SortDialogFragment : DialogFragment() {
|
||||
return bundle
|
||||
}
|
||||
|
||||
fun getInstance(sortNodeEnum: SortNodeEnum,
|
||||
ascending: Boolean,
|
||||
groupsBefore: Boolean): SortDialogFragment {
|
||||
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
|
||||
val fragment = SortDialogFragment()
|
||||
fragment.arguments = bundle
|
||||
return fragment
|
||||
}
|
||||
|
||||
fun getInstance(sortNodeEnum: SortNodeEnum,
|
||||
ascending: Boolean,
|
||||
groupsBefore: Boolean,
|
||||
recycleBinBottom: Boolean): SortDialogFragment {
|
||||
recycleBinBottom: Boolean?): SortDialogFragment {
|
||||
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
|
||||
bundle.putBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY, recycleBinBottom)
|
||||
recycleBinBottom?.let {
|
||||
bundle.putBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY, recycleBinBottom)
|
||||
}
|
||||
val fragment = SortDialogFragment()
|
||||
fragment.arguments = bundle
|
||||
return fragment
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.DatePickerDialog
|
||||
import android.app.Dialog
|
||||
import android.app.TimePickerDialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateFormat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
|
||||
class TimePickerFragment : DialogFragment() {
|
||||
|
||||
private var defaultHour: Int = 0
|
||||
private var defaultMinute: Int = 0
|
||||
|
||||
private var mListener: TimePickerDialog.OnTimeSetListener? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
try {
|
||||
mListener = context as TimePickerDialog.OnTimeSetListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun 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() {
|
||||
|
||||
@@ -25,9 +25,8 @@ import android.text.SpannableStringBuilder
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.text.HtmlCompat
|
||||
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.
|
||||
@@ -40,31 +39,22 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
val stringBuilder = SpannableStringBuilder()
|
||||
if (BuildConfig.CLOSED_STORE) {
|
||||
if (BuildConfig.FULL_VERSION) {
|
||||
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_buy_pro), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||
builder.setPositiveButton(R.string.download) { _, _ ->
|
||||
UriUtil.gotoUrl(requireContext(), R.string.app_pro_url)
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||
}
|
||||
/*
|
||||
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()
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
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 androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
|
||||
|
||||
protected val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||
protected val mDatabase: ContextualDatabase?
|
||||
get() = mDatabaseViewModel.database
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
mDatabaseViewModel.actionState.collect { uiState ->
|
||||
when (uiState) {
|
||||
is DatabaseViewModel.ActionState.OnDatabaseActionFinished -> {
|
||||
onDatabaseActionFinished(
|
||||
uiState.database,
|
||||
uiState.actionTask,
|
||||
uiState.result
|
||||
)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
mDatabaseViewModel.databaseState.collect { database ->
|
||||
database?.let {
|
||||
onDatabaseRetrieved(database)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) {
|
||||
context?.let {
|
||||
view?.resetAppTimeoutWhenViewTouchedOrFocused(
|
||||
context = it,
|
||||
databaseLoaded = mDatabase?.loaded
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseActionFinished(
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
// Can be overridden by a subclass
|
||||
}
|
||||
}
|
||||
@@ -19,431 +19,306 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.fragments
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.EntryEditActivity
|
||||
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||
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.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
import com.kunzisoft.keepass.model.*
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.ExpirationView
|
||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||
import com.kunzisoft.keepass.database.element.template.Template
|
||||
import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
import com.kunzisoft.keepass.utils.getParcelableList
|
||||
import com.kunzisoft.keepass.utils.putParcelableList
|
||||
import com.kunzisoft.keepass.view.TagsCompletionView
|
||||
import com.kunzisoft.keepass.view.TemplateEditView
|
||||
import com.kunzisoft.keepass.view.collapse
|
||||
import com.kunzisoft.keepass.view.expand
|
||||
import com.kunzisoft.keepass.view.showByFading
|
||||
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
||||
import com.tokenautocomplete.FilteredArrayAdapter
|
||||
|
||||
class EntryEditFragment: StylishFragment() {
|
||||
|
||||
private lateinit var entryTitleLayoutView: TextInputLayout
|
||||
private lateinit var entryTitleView: EditText
|
||||
private lateinit var entryIconView: ImageView
|
||||
private lateinit var entryUserNameView: EditText
|
||||
private lateinit var entryUrlView: EditText
|
||||
private lateinit var entryPasswordLayoutView: TextInputLayout
|
||||
private lateinit var entryPasswordView: EditText
|
||||
private lateinit var entryPasswordGeneratorView: View
|
||||
private lateinit var entryExpirationView: ExpirationView
|
||||
private lateinit var entryNotesView: EditText
|
||||
private lateinit var extraFieldsContainerView: View
|
||||
private lateinit var extraFieldsListView: ViewGroup
|
||||
private lateinit var attachmentsContainerView: View
|
||||
class EntryEditFragment: DatabaseFragment() {
|
||||
|
||||
private val mEntryEditViewModel: EntryEditViewModel by activityViewModels()
|
||||
|
||||
private lateinit var rootView: View
|
||||
private lateinit var templateView: TemplateEditView
|
||||
private lateinit var attachmentsContainerView: ViewGroup
|
||||
private lateinit var attachmentsListView: RecyclerView
|
||||
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
|
||||
private lateinit var tagsContainerView: TextInputLayout
|
||||
private lateinit var tagsCompletionView: TagsCompletionView
|
||||
private var tagsAdapter: FilteredArrayAdapter<String>? = null
|
||||
|
||||
private lateinit var attachmentsAdapter: EntryAttachmentsItemsAdapter
|
||||
private var mTemplate: Template? = null
|
||||
private var mAllowMultipleAttachments: Boolean = false
|
||||
|
||||
private var fontInVisibility: Boolean = false
|
||||
private var iconColor: Int = 0
|
||||
private var mIconColor: Int = 0
|
||||
|
||||
var drawFactory: IconDrawableFactory? = null
|
||||
var setOnDateClickListener: (() -> Unit)? = null
|
||||
var setOnPasswordGeneratorClickListener: View.OnClickListener? = null
|
||||
var setOnIconViewClickListener: ((IconImage) -> Unit)? = null
|
||||
var setOnEditCustomField: ((Field) -> Unit)? = null
|
||||
var setOnRemoveAttachment: ((Attachment) -> Unit)? = null
|
||||
|
||||
// Elements to modify the current entry
|
||||
private var mEntryInfo = EntryInfo()
|
||||
private var mLastFocusedEditField: FocusedEditField? = null
|
||||
private var mExtraViewToRequestFocus: EditText? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
override fun onCreateView(inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
val rootView = inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.fragment_entry_edit_contents, container, false)
|
||||
// Retrieve the textColor to tint the icon
|
||||
val taIconColor = context?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
mIconColor = taIconColor?.getColor(0, Color.BLACK) ?: Color.BLACK
|
||||
taIconColor?.recycle()
|
||||
|
||||
fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(requireContext())
|
||||
return inflater.inflate(R.layout.fragment_entry_edit, container, false)
|
||||
}
|
||||
|
||||
entryTitleLayoutView = rootView.findViewById(R.id.entry_edit_container_title)
|
||||
entryTitleView = rootView.findViewById(R.id.entry_edit_title)
|
||||
entryIconView = rootView.findViewById(R.id.entry_edit_icon_button)
|
||||
entryIconView.setOnClickListener {
|
||||
setOnIconViewClickListener?.invoke(mEntryInfo.icon)
|
||||
override fun onViewCreated(view: View,
|
||||
savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
rootView = view
|
||||
// Hide only the first time
|
||||
if (savedInstanceState == null) {
|
||||
view.isVisible = false
|
||||
}
|
||||
templateView = view.findViewById(R.id.template_view)
|
||||
attachmentsContainerView = view.findViewById(R.id.entry_attachments_container)
|
||||
attachmentsListView = view.findViewById(R.id.entry_attachments_list)
|
||||
tagsContainerView = view.findViewById(R.id.entry_tags_label)
|
||||
tagsCompletionView = view.findViewById(R.id.entry_tags_completion_view)
|
||||
|
||||
entryUserNameView = rootView.findViewById(R.id.entry_edit_user_name)
|
||||
entryUrlView = rootView.findViewById(R.id.entry_edit_url)
|
||||
entryPasswordLayoutView = rootView.findViewById(R.id.entry_edit_container_password)
|
||||
entryPasswordView = rootView.findViewById(R.id.entry_edit_password)
|
||||
entryPasswordGeneratorView = rootView.findViewById(R.id.entry_edit_password_generator_button)
|
||||
entryPasswordGeneratorView.setOnClickListener {
|
||||
setOnPasswordGeneratorClickListener?.onClick(it)
|
||||
}
|
||||
entryExpirationView = rootView.findViewById(R.id.entry_edit_expiration)
|
||||
entryExpirationView.setOnDateClickListener = setOnDateClickListener
|
||||
|
||||
entryNotesView = rootView.findViewById(R.id.entry_edit_notes)
|
||||
|
||||
extraFieldsContainerView = rootView.findViewById(R.id.extra_fields_container)
|
||||
extraFieldsListView = rootView.findViewById(R.id.extra_fields_list)
|
||||
|
||||
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
|
||||
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
|
||||
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
|
||||
// TODO retrieve current database with its unique key
|
||||
attachmentsAdapter.database = Database.getInstance()
|
||||
//attachmentsAdapter.database = arguments?.getInt(KEY_DATABASE)
|
||||
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
|
||||
if (previousSize > 0 && newSize == 0) {
|
||||
attachmentsContainerView.collapse(true)
|
||||
} else if (previousSize == 0 && newSize == 1) {
|
||||
attachmentsContainerView.expand(true)
|
||||
}
|
||||
}
|
||||
attachmentsListView.apply {
|
||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
adapter = attachmentsAdapter
|
||||
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
}
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
|
||||
taIconColor?.recycle()
|
||||
|
||||
rootView?.resetAppTimeoutWhenViewFocusedOrChanged(requireContext())
|
||||
|
||||
// Retrieve the new entry after an orientation change
|
||||
if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true)
|
||||
mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
|
||||
else if (savedInstanceState?.containsKey(KEY_TEMP_ENTRY_INFO) == true) {
|
||||
mEntryInfo = savedInstanceState.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
|
||||
templateView.apply {
|
||||
setOnIconClickListener {
|
||||
mEntryEditViewModel.requestIconSelection(templateView.getIcon())
|
||||
}
|
||||
setOnBackgroundColorClickListener {
|
||||
mEntryEditViewModel.requestBackgroundColorSelection(templateView.getBackgroundColor())
|
||||
}
|
||||
setOnForegroundColorClickListener {
|
||||
mEntryEditViewModel.requestForegroundColorSelection(templateView.getForegroundColor())
|
||||
}
|
||||
setOnCustomEditionActionClickListener { field ->
|
||||
mEntryEditViewModel.requestCustomFieldEdition(field)
|
||||
}
|
||||
setOnPasswordGenerationActionClickListener { field ->
|
||||
mEntryEditViewModel.requestPasswordSelection(field)
|
||||
}
|
||||
setOnDateInstantClickListener { dateInstant ->
|
||||
mEntryEditViewModel.requestDateTimeSelection(dateInstant)
|
||||
}
|
||||
}
|
||||
|
||||
if (savedInstanceState?.containsKey(KEY_LAST_FOCUSED_FIELD) == true) {
|
||||
mLastFocusedEditField = savedInstanceState.getParcelable(KEY_LAST_FOCUSED_FIELD) ?: mLastFocusedEditField
|
||||
if (savedInstanceState != null) {
|
||||
val attachments: List<Attachment> =
|
||||
savedInstanceState.getParcelableList(ATTACHMENTS_TAG) ?: listOf()
|
||||
setAttachments(attachments)
|
||||
}
|
||||
|
||||
populateViewsWithEntry()
|
||||
mEntryEditViewModel.onTemplateChanged.observe(viewLifecycleOwner) { template ->
|
||||
this.mTemplate = template
|
||||
templateView.setTemplate(template)
|
||||
}
|
||||
|
||||
return rootView
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
super.onDetach()
|
||||
|
||||
drawFactory = null
|
||||
setOnDateClickListener = null
|
||||
setOnPasswordGeneratorClickListener = null
|
||||
setOnIconViewClickListener = null
|
||||
setOnRemoveAttachment = null
|
||||
setOnEditCustomField = null
|
||||
}
|
||||
|
||||
fun getEntryInfo(): EntryInfo {
|
||||
populateEntryWithViews()
|
||||
return mEntryInfo
|
||||
}
|
||||
|
||||
fun generatePasswordEducationPerformed(entryEditActivityEducation: EntryEditActivityEducation): Boolean {
|
||||
return entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
||||
entryPasswordGeneratorView,
|
||||
{
|
||||
GeneratePasswordDialogFragment().show(parentFragmentManager, "PasswordGeneratorFragment")
|
||||
},
|
||||
{
|
||||
try {
|
||||
(activity as? EntryEditActivity?)?.performedNextEducation(entryEditActivityEducation)
|
||||
} catch (ignore: Exception) {}
|
||||
mEntryEditViewModel.templatesEntry.observe(viewLifecycleOwner) { templateEntry ->
|
||||
if (templateEntry != null) {
|
||||
val selectedTemplate = if (mTemplate != null)
|
||||
mTemplate
|
||||
else
|
||||
templateEntry.defaultTemplate
|
||||
templateView.setTemplate(selectedTemplate)
|
||||
// Load entry info only the first time to keep change locally
|
||||
if (savedInstanceState == null) {
|
||||
assignEntryInfo(templateEntry.entryInfo)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun populateViewsWithEntry() {
|
||||
// Set info in view
|
||||
icon = mEntryInfo.icon
|
||||
title = mEntryInfo.title
|
||||
username = mEntryInfo.username
|
||||
url = mEntryInfo.url
|
||||
password = mEntryInfo.password
|
||||
expires = mEntryInfo.expires
|
||||
expiryTime = mEntryInfo.expiryTime
|
||||
notes = mEntryInfo.notes
|
||||
assignExtraFields(mEntryInfo.customFields) { fields ->
|
||||
setOnEditCustomField?.invoke(fields)
|
||||
}
|
||||
assignAttachments(mEntryInfo.attachments, StreamDirection.UPLOAD) { attachment ->
|
||||
setOnRemoveAttachment?.invoke(attachment)
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateEntryWithViews() {
|
||||
// Icon already populate
|
||||
mEntryInfo.title = title
|
||||
mEntryInfo.username = username
|
||||
mEntryInfo.url = url
|
||||
mEntryInfo.password = password
|
||||
mEntryInfo.expires = expires
|
||||
mEntryInfo.expiryTime = expiryTime
|
||||
mEntryInfo.notes = notes
|
||||
mEntryInfo.customFields = getExtraFields()
|
||||
mEntryInfo.otpModel = OtpEntryFields.parseFields { key ->
|
||||
getExtraFields().firstOrNull { it.name == key }?.protectedValue?.toString()
|
||||
}?.otpModel
|
||||
mEntryInfo.attachments = getAttachments()
|
||||
}
|
||||
|
||||
var title: String
|
||||
get() {
|
||||
return entryTitleView.text.toString()
|
||||
}
|
||||
set(value) {
|
||||
entryTitleView.setText(value)
|
||||
if (fontInVisibility)
|
||||
entryTitleView.applyFontVisibility()
|
||||
}
|
||||
|
||||
var icon: IconImage
|
||||
get() {
|
||||
return mEntryInfo.icon
|
||||
}
|
||||
set(value) {
|
||||
mEntryInfo.icon = value
|
||||
drawFactory?.assignDatabaseIcon(entryIconView, value, iconColor)
|
||||
}
|
||||
|
||||
var username: String
|
||||
get() {
|
||||
return entryUserNameView.text.toString()
|
||||
}
|
||||
set(value) {
|
||||
entryUserNameView.setText(value)
|
||||
if (fontInVisibility)
|
||||
entryUserNameView.applyFontVisibility()
|
||||
}
|
||||
|
||||
var url: String
|
||||
get() {
|
||||
return entryUrlView.text.toString()
|
||||
}
|
||||
set(value) {
|
||||
entryUrlView.setText(value)
|
||||
if (fontInVisibility)
|
||||
entryUrlView.applyFontVisibility()
|
||||
}
|
||||
|
||||
var password: String
|
||||
get() {
|
||||
return entryPasswordView.text.toString()
|
||||
}
|
||||
set(value) {
|
||||
entryPasswordView.setText(value)
|
||||
if (fontInVisibility) {
|
||||
entryPasswordView.applyFontVisibility()
|
||||
// To prevent flickering
|
||||
rootView.showByFading()
|
||||
// Apply timeout reset
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
|
||||
}
|
||||
}
|
||||
|
||||
var expires: Boolean
|
||||
get() {
|
||||
return entryExpirationView.expires
|
||||
}
|
||||
set(value) {
|
||||
entryExpirationView.expires = value
|
||||
mEntryEditViewModel.requestEntryInfoUpdate.observe(viewLifecycleOwner) {
|
||||
val entryInfo = retrieveEntryInfo()
|
||||
mEntryEditViewModel.saveEntryInfo(it.database, it.entry, it.parent, entryInfo)
|
||||
}
|
||||
|
||||
var expiryTime: DateInstant
|
||||
get() {
|
||||
return entryExpirationView.expiryTime
|
||||
}
|
||||
set(value) {
|
||||
entryExpirationView.expiryTime = value
|
||||
mEntryEditViewModel.onIconSelected.observe(viewLifecycleOwner) { iconImage ->
|
||||
templateView.setIcon(iconImage)
|
||||
}
|
||||
|
||||
var notes: String
|
||||
get() {
|
||||
return entryNotesView.text.toString()
|
||||
}
|
||||
set(value) {
|
||||
entryNotesView.setText(value)
|
||||
if (fontInVisibility)
|
||||
entryNotesView.applyFontVisibility()
|
||||
mEntryEditViewModel.onBackgroundColorSelected.observe(viewLifecycleOwner) { color ->
|
||||
templateView.setBackgroundColor(color)
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* Extra Fields
|
||||
* -------------
|
||||
*/
|
||||
mEntryEditViewModel.onForegroundColorSelected.observe(viewLifecycleOwner) { color ->
|
||||
templateView.setForegroundColor(color)
|
||||
}
|
||||
|
||||
private var mExtraFieldsList: MutableList<Field> = ArrayList()
|
||||
private var mOnEditButtonClickListener: ((item: Field)->Unit)? = null
|
||||
mEntryEditViewModel.onPasswordSelected.observe(viewLifecycleOwner) { passwordField ->
|
||||
templateView.setPasswordField(passwordField)
|
||||
}
|
||||
|
||||
private fun buildViewFromField(extraField: Field): View? {
|
||||
val inflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
val itemView: View? = inflater?.inflate(R.layout.item_entry_edit_extra_field, extraFieldsListView, false)
|
||||
itemView?.id = View.NO_ID
|
||||
mEntryEditViewModel.onDateSelected.observe(viewLifecycleOwner) { viewModelDate ->
|
||||
// Save the date
|
||||
templateView.setCurrentDateTimeValue(viewModelDate)
|
||||
}
|
||||
|
||||
val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container)
|
||||
extraFieldValueContainer?.endIconMode = if (extraField.protectedValue.isProtected)
|
||||
TextInputLayout.END_ICON_PASSWORD_TOGGLE else TextInputLayout.END_ICON_NONE
|
||||
extraFieldValueContainer?.hint = extraField.name
|
||||
extraFieldValueContainer?.id = View.NO_ID
|
||||
mEntryEditViewModel.onTimeSelected.observe(viewLifecycleOwner) { viewModelTime ->
|
||||
// Save the time
|
||||
templateView.setCurrentTimeValue(viewModelTime)
|
||||
}
|
||||
|
||||
val extraFieldValue: TextInputEditText? = itemView?.findViewById(R.id.entry_extra_field_value)
|
||||
extraFieldValue?.apply {
|
||||
if (extraField.protectedValue.isProtected) {
|
||||
inputType = extraFieldValue.inputType or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
|
||||
mEntryEditViewModel.onCustomFieldEdited.observe(viewLifecycleOwner) { fieldAction ->
|
||||
val oldField = fieldAction.oldField
|
||||
val newField = fieldAction.newField
|
||||
// Field to add
|
||||
if (oldField == null) {
|
||||
newField?.let {
|
||||
if (!templateView.putCustomField(it)) {
|
||||
mEntryEditViewModel.showCustomFieldEditionError()
|
||||
}
|
||||
}
|
||||
}
|
||||
setText(extraField.protectedValue.toString())
|
||||
if (fontInVisibility)
|
||||
applyFontVisibility()
|
||||
}
|
||||
extraFieldValue?.id = View.NO_ID
|
||||
extraFieldValue?.tag = "FIELD_VALUE_TAG"
|
||||
if (mLastFocusedEditField?.field == extraField) {
|
||||
mExtraViewToRequestFocus = extraFieldValue
|
||||
}
|
||||
|
||||
val extraFieldEditButton: View? = itemView?.findViewById(R.id.entry_extra_field_edit)
|
||||
extraFieldEditButton?.setOnClickListener {
|
||||
mOnEditButtonClickListener?.invoke(extraField)
|
||||
}
|
||||
extraFieldEditButton?.id = View.NO_ID
|
||||
|
||||
return itemView
|
||||
}
|
||||
|
||||
fun getExtraFields(): List<Field> {
|
||||
mLastFocusedEditField = null
|
||||
for (index in 0 until extraFieldsListView.childCount) {
|
||||
val extraFieldValue: EditText = extraFieldsListView.getChildAt(index)
|
||||
.findViewWithTag("FIELD_VALUE_TAG")
|
||||
val extraField = mExtraFieldsList[index]
|
||||
extraField.protectedValue.stringValue = extraFieldValue.text?.toString() ?: ""
|
||||
if (extraFieldValue.isFocused) {
|
||||
mLastFocusedEditField = FocusedEditField().apply {
|
||||
field = extraField
|
||||
cursorSelectionStart = extraFieldValue.selectionStart
|
||||
cursorSelectionEnd = extraFieldValue.selectionEnd
|
||||
// Field to replace
|
||||
oldField?.let {
|
||||
newField?.let {
|
||||
if (!templateView.replaceCustomField(oldField, newField)) {
|
||||
mEntryEditViewModel.showCustomFieldEditionError()
|
||||
}
|
||||
}
|
||||
}
|
||||
// Field to remove
|
||||
if (newField == null) {
|
||||
oldField?.let {
|
||||
templateView.removeCustomField(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
return mExtraFieldsList
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all children and add new views for each field
|
||||
*/
|
||||
fun assignExtraFields(fields: List<Field>,
|
||||
onEditButtonClickListener: ((item: Field)->Unit)?) {
|
||||
extraFieldsContainerView.visibility = if (fields.isEmpty()) View.GONE else View.VISIBLE
|
||||
// Reinit focused field
|
||||
mExtraFieldsList.clear()
|
||||
mExtraFieldsList.addAll(fields)
|
||||
extraFieldsListView.removeAllViews()
|
||||
fields.forEach {
|
||||
extraFieldsListView.addView(buildViewFromField(it))
|
||||
mEntryEditViewModel.requestSetupOtp.observe(viewLifecycleOwner) {
|
||||
// Retrieve the current otpElement if exists
|
||||
// and open the dialog to set up the OTP
|
||||
SetOTPDialogFragment.build(templateView.getEntryInfo().otpModel)
|
||||
.show(parentFragmentManager, "addOTPDialog")
|
||||
}
|
||||
// Request last focus
|
||||
mLastFocusedEditField?.let { focusField ->
|
||||
mExtraViewToRequestFocus?.apply {
|
||||
requestFocus()
|
||||
setSelection(focusField.cursorSelectionStart,
|
||||
focusField.cursorSelectionEnd)
|
||||
}
|
||||
|
||||
mEntryEditViewModel.onOtpCreated.observe(viewLifecycleOwner) {
|
||||
// Update the otp field with otpauth:// url
|
||||
templateView.putOtpElement(it)
|
||||
}
|
||||
mLastFocusedEditField = null
|
||||
mOnEditButtonClickListener = onEditButtonClickListener
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an extra field or create a new one if doesn't exists, the old value is lost
|
||||
*/
|
||||
fun putExtraField(extraField: Field) {
|
||||
extraFieldsContainerView.visibility = View.VISIBLE
|
||||
val oldField = mExtraFieldsList.firstOrNull { it.name == extraField.name }
|
||||
oldField?.let {
|
||||
val index = mExtraFieldsList.indexOf(oldField)
|
||||
mExtraFieldsList.removeAt(index)
|
||||
mExtraFieldsList.add(index, extraField)
|
||||
extraFieldsListView.removeViewAt(index)
|
||||
val newView = buildViewFromField(extraField)
|
||||
extraFieldsListView.addView(newView, index)
|
||||
newView?.requestFocus()
|
||||
} ?: kotlin.run {
|
||||
mExtraFieldsList.add(extraField)
|
||||
val newView = buildViewFromField(extraField)
|
||||
extraFieldsListView.addView(newView)
|
||||
newView?.requestFocus()
|
||||
}
|
||||
}
|
||||
mEntryEditViewModel.onBuildNewAttachment.observe(viewLifecycleOwner) {
|
||||
val attachmentToUploadUri = it.attachmentToUploadUri
|
||||
val fileName = it.fileName
|
||||
|
||||
/**
|
||||
* Update an extra field and keep the old value
|
||||
*/
|
||||
fun replaceExtraField(oldExtraField: Field, newExtraField: Field) {
|
||||
extraFieldsContainerView.visibility = View.VISIBLE
|
||||
val index = mExtraFieldsList.indexOf(oldExtraField)
|
||||
val oldValueEditText: EditText = extraFieldsListView.getChildAt(index)
|
||||
.findViewWithTag("FIELD_VALUE_TAG")
|
||||
val oldValue = oldValueEditText.text.toString()
|
||||
val newExtraFieldWithOldValue = Field(newExtraField).apply {
|
||||
this.protectedValue.stringValue = oldValue
|
||||
}
|
||||
mExtraFieldsList.removeAt(index)
|
||||
mExtraFieldsList.add(index, newExtraFieldWithOldValue)
|
||||
extraFieldsListView.removeViewAt(index)
|
||||
val newView = buildViewFromField(newExtraFieldWithOldValue)
|
||||
extraFieldsListView.addView(newView, index)
|
||||
newView?.requestFocus()
|
||||
}
|
||||
|
||||
fun removeExtraField(oldExtraField: Field) {
|
||||
val previousSize = mExtraFieldsList.size
|
||||
val index = mExtraFieldsList.indexOf(oldExtraField)
|
||||
extraFieldsListView.getChildAt(index)?.let {
|
||||
it.collapse(true) {
|
||||
mExtraFieldsList.removeAt(index)
|
||||
extraFieldsListView.removeViewAt(index)
|
||||
val newSize = mExtraFieldsList.size
|
||||
|
||||
if (previousSize > 0 && newSize == 0) {
|
||||
extraFieldsContainerView.collapse(true)
|
||||
} else if (previousSize == 0 && newSize == 1) {
|
||||
extraFieldsContainerView.expand(true)
|
||||
mDatabaseViewModel.buildNewAttachment()?.let { binaryAttachment ->
|
||||
val entryAttachment = Attachment(fileName, binaryAttachment)
|
||||
// Ask to replace the current attachment
|
||||
if ((!mAllowMultipleAttachments
|
||||
&& containsAttachment()) ||
|
||||
containsAttachment(EntryAttachmentState(entryAttachment, StreamDirection.UPLOAD))) {
|
||||
ReplaceFileDialogFragment.build(attachmentToUploadUri, entryAttachment)
|
||||
.show(parentFragmentManager, "replacementFileFragment")
|
||||
} else {
|
||||
mEntryEditViewModel.startUploadAttachment(attachmentToUploadUri, entryAttachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mEntryEditViewModel.onAttachmentAction.observe(viewLifecycleOwner) { entryAttachmentState ->
|
||||
when (entryAttachmentState?.downloadState) {
|
||||
AttachmentState.START -> {
|
||||
putAttachment(entryAttachmentState)
|
||||
getAttachmentViewPosition(entryAttachmentState) { attachment, position ->
|
||||
mEntryEditViewModel.binaryPreviewLoaded(attachment, position)
|
||||
}
|
||||
}
|
||||
AttachmentState.IN_PROGRESS -> {
|
||||
putAttachment(entryAttachmentState)
|
||||
}
|
||||
AttachmentState.COMPLETE -> {
|
||||
putAttachment(entryAttachmentState) { entryAttachment ->
|
||||
getAttachmentViewPosition(entryAttachment) { attachment, position ->
|
||||
mEntryEditViewModel.binaryPreviewLoaded(attachment, position)
|
||||
}
|
||||
}
|
||||
mEntryEditViewModel.onAttachmentAction(null)
|
||||
}
|
||||
AttachmentState.CANCELED,
|
||||
AttachmentState.ERROR -> {
|
||||
removeAttachment(entryAttachmentState)
|
||||
mEntryEditViewModel.onAttachmentAction(null)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
|
||||
templateView.populateIconMethod = { imageView, icon ->
|
||||
database.iconDrawableFactory.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||
}
|
||||
|
||||
mAllowMultipleAttachments = database.allowMultipleAttachments == true
|
||||
|
||||
attachmentsAdapter?.database = database
|
||||
attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize ->
|
||||
if (previousSize > 0 && newSize == 0) {
|
||||
attachmentsContainerView.collapse(true)
|
||||
} else if (previousSize == 0 && newSize == 1) {
|
||||
attachmentsContainerView.expand(true)
|
||||
}
|
||||
}
|
||||
|
||||
tagsAdapter = TagsProposalAdapter(requireContext(), database.tagPool)
|
||||
tagsCompletionView.apply {
|
||||
threshold = 1
|
||||
setAdapter(tagsAdapter)
|
||||
}
|
||||
tagsContainerView.visibility = if (database.allowTags()) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun assignEntryInfo(entryInfo: EntryInfo?) {
|
||||
// Populate entry views
|
||||
templateView.setEntryInfo(entryInfo)
|
||||
|
||||
// Set Tags
|
||||
entryInfo?.tags?.let { tags ->
|
||||
tagsCompletionView.setText("")
|
||||
for (i in 0 until tags.size()) {
|
||||
tagsCompletionView.addObjectSync(tags.get(i))
|
||||
}
|
||||
}
|
||||
|
||||
// Manage attachments
|
||||
setAttachments(entryInfo?.attachments ?: listOf())
|
||||
}
|
||||
|
||||
private fun retrieveEntryInfo(): EntryInfo {
|
||||
val entryInfo = templateView.getEntryInfo()
|
||||
entryInfo.tags = tagsCompletionView.getTags()
|
||||
entryInfo.attachments = getAttachments().toMutableList()
|
||||
return entryInfo
|
||||
}
|
||||
|
||||
/* -------------
|
||||
@@ -451,78 +326,84 @@ class EntryEditFragment: StylishFragment() {
|
||||
* -------------
|
||||
*/
|
||||
|
||||
fun getAttachments(): List<Attachment> {
|
||||
return attachmentsAdapter.itemsList.map { it.attachment }
|
||||
private fun getAttachments(): List<Attachment> {
|
||||
return attachmentsAdapter?.itemsList?.map { it.attachment } ?: listOf()
|
||||
}
|
||||
|
||||
fun assignAttachments(attachments: List<Attachment>,
|
||||
streamDirection: StreamDirection,
|
||||
onDeleteItem: (attachment: Attachment)->Unit) {
|
||||
private fun setAttachments(attachments: List<Attachment>) {
|
||||
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
|
||||
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
|
||||
attachmentsAdapter.onDeleteButtonClickListener = { item ->
|
||||
onDeleteItem.invoke(item.attachment)
|
||||
attachmentsAdapter?.assignItems(attachments.map {
|
||||
EntryAttachmentState(it, StreamDirection.UPLOAD)
|
||||
})
|
||||
attachmentsAdapter?.onDeleteButtonClickListener = { item ->
|
||||
val attachment = item.attachment
|
||||
removeAttachment(EntryAttachmentState(attachment, StreamDirection.DOWNLOAD))
|
||||
mEntryEditViewModel.deleteAttachment(attachment)
|
||||
}
|
||||
}
|
||||
|
||||
fun containsAttachment(): Boolean {
|
||||
return !attachmentsAdapter.isEmpty()
|
||||
private fun containsAttachment(): Boolean {
|
||||
return attachmentsAdapter?.isEmpty() != true
|
||||
}
|
||||
|
||||
fun containsAttachment(attachment: EntryAttachmentState): Boolean {
|
||||
return attachmentsAdapter.contains(attachment)
|
||||
private fun containsAttachment(attachment: EntryAttachmentState): Boolean {
|
||||
return attachmentsAdapter?.contains(attachment) ?: false
|
||||
}
|
||||
|
||||
fun putAttachment(attachment: EntryAttachmentState,
|
||||
onPreviewLoaded: (()-> Unit)? = null) {
|
||||
private fun putAttachment(attachment: EntryAttachmentState,
|
||||
onPreviewLoaded: ((attachment: EntryAttachmentState) -> Unit)? = null) {
|
||||
// When only one attachment is allowed
|
||||
if (!mAllowMultipleAttachments
|
||||
&& attachment.downloadState == AttachmentState.START) {
|
||||
attachmentsAdapter?.clear()
|
||||
}
|
||||
attachmentsContainerView.visibility = View.VISIBLE
|
||||
attachmentsAdapter.putItem(attachment)
|
||||
attachmentsAdapter.onBinaryPreviewLoaded = {
|
||||
onPreviewLoaded?.invoke()
|
||||
attachmentsAdapter?.putItem(attachment)
|
||||
attachmentsAdapter?.onBinaryPreviewLoaded = {
|
||||
onPreviewLoaded?.invoke(attachment)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAttachment(attachment: EntryAttachmentState) {
|
||||
attachmentsAdapter.removeItem(attachment)
|
||||
private fun removeAttachment(attachment: EntryAttachmentState) {
|
||||
attachmentsAdapter?.removeItem(attachment)
|
||||
}
|
||||
|
||||
fun clearAttachments() {
|
||||
attachmentsAdapter.clear()
|
||||
}
|
||||
|
||||
fun getAttachmentViewPosition(attachment: EntryAttachmentState, position: (Float) -> Unit) {
|
||||
private fun getAttachmentViewPosition(attachment: EntryAttachmentState,
|
||||
position: (attachment: EntryAttachmentState, Float) -> Unit) {
|
||||
attachmentsListView.postDelayed({
|
||||
position.invoke(attachmentsContainerView.y
|
||||
+ attachmentsListView.y
|
||||
+ (attachmentsListView.getChildAt(attachmentsAdapter.indexOf(attachment))?.y
|
||||
?: 0F)
|
||||
)
|
||||
attachmentsAdapter?.indexOf(attachment)?.let { index ->
|
||||
position.invoke(attachment,
|
||||
attachmentsContainerView.y
|
||||
+ attachmentsListView.y
|
||||
+ (attachmentsListView.getChildAt(index)?.y
|
||||
?: 0F)
|
||||
)
|
||||
}
|
||||
}, 250)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
populateEntryWithViews()
|
||||
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
|
||||
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
|
||||
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putParcelableList(ATTACHMENTS_TAG, getAttachments())
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* Education
|
||||
* -------------
|
||||
*/
|
||||
|
||||
fun getActionImageView(): View? {
|
||||
return templateView.getActionImageView()
|
||||
}
|
||||
|
||||
fun launchGeneratePasswordEductionAction() {
|
||||
mEntryEditViewModel.requestPasswordSelection(templateView.getPasswordField())
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
|
||||
const val KEY_DATABASE = "KEY_DATABASE"
|
||||
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
|
||||
private val TAG = EntryEditFragment::class.java.name
|
||||
|
||||
fun getInstance(entryInfo: EntryInfo?): EntryEditFragment {
|
||||
//database: Database?): EntryEditFragment {
|
||||
return EntryEditFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
|
||||
// TODO Unique database key database.key
|
||||
putInt(KEY_DATABASE, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
private const val ATTACHMENTS_TAG = "ATTACHMENTS_TAG"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
package com.kunzisoft.keepass.activities.fragments
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.database.helper.getLocalizedName
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
||||
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||
import com.kunzisoft.keepass.view.TemplateView
|
||||
import com.kunzisoft.keepass.view.hideByFading
|
||||
import com.kunzisoft.keepass.view.showByFading
|
||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||
|
||||
class EntryFragment: DatabaseFragment() {
|
||||
|
||||
private lateinit var rootView: View
|
||||
private lateinit var mainSection: View
|
||||
private lateinit var advancedSection: View
|
||||
|
||||
private lateinit var templateView: TemplateView
|
||||
|
||||
private lateinit var creationDateView: TextView
|
||||
private lateinit var modificationDateView: TextView
|
||||
|
||||
private lateinit var attachmentsContainerView: View
|
||||
private lateinit var attachmentsListView: RecyclerView
|
||||
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
|
||||
|
||||
private lateinit var customDataView: TextView
|
||||
|
||||
private lateinit var uuidContainerView: View
|
||||
private lateinit var uuidReferenceView: TextView
|
||||
|
||||
private var mClipboardHelper: ClipboardHelper? = null
|
||||
|
||||
private val mEntryViewModel: EntryViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
return inflater.inflate(R.layout.fragment_entry, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View,
|
||||
savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
context?.let { context ->
|
||||
mClipboardHelper = ClipboardHelper(context)
|
||||
}
|
||||
|
||||
rootView = view
|
||||
// Hide only the first time
|
||||
if (savedInstanceState == null) {
|
||||
view.isVisible = false
|
||||
}
|
||||
|
||||
mainSection = view.findViewById(R.id.entry_section_main)
|
||||
advancedSection = view.findViewById(R.id.entry_section_advanced)
|
||||
|
||||
templateView = view.findViewById(R.id.entry_template)
|
||||
loadTemplateSettings()
|
||||
|
||||
attachmentsContainerView = view.findViewById(R.id.entry_attachments_container)
|
||||
attachmentsListView = view.findViewById(R.id.entry_attachments_list)
|
||||
attachmentsListView.apply {
|
||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
}
|
||||
|
||||
creationDateView = view.findViewById(R.id.entry_created)
|
||||
modificationDateView = view.findViewById(R.id.entry_modified)
|
||||
|
||||
// TODO Custom data
|
||||
// customDataView = view.findViewById(R.id.entry_custom_data)
|
||||
|
||||
uuidContainerView = view.findViewById(R.id.entry_UUID_container)
|
||||
uuidContainerView.apply {
|
||||
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
|
||||
}
|
||||
uuidReferenceView = view.findViewById(R.id.entry_UUID_reference)
|
||||
|
||||
mEntryViewModel.entryInfoHistory.observe(viewLifecycleOwner) { entryInfoHistory ->
|
||||
if (entryInfoHistory != null) {
|
||||
templateView.setTemplate(entryInfoHistory.template)
|
||||
assignEntryInfo(entryInfoHistory.entryInfo)
|
||||
// Smooth appearing
|
||||
rootView.showByFading()
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
|
||||
}
|
||||
}
|
||||
|
||||
mEntryViewModel.onAttachmentAction.observe(viewLifecycleOwner) { entryAttachmentState ->
|
||||
entryAttachmentState?.let {
|
||||
if (it.streamDirection != StreamDirection.UPLOAD) {
|
||||
putAttachment(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mEntryViewModel.sectionSelected.observe(viewLifecycleOwner) { entrySection ->
|
||||
when (entrySection ?: EntryViewModel.EntrySection.MAIN) {
|
||||
EntryViewModel.EntrySection.MAIN -> {
|
||||
mainSection.showByFading()
|
||||
advancedSection.hideByFading()
|
||||
}
|
||||
EntryViewModel.EntrySection.ADVANCED -> {
|
||||
mainSection.hideByFading()
|
||||
advancedSection.showByFading()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
context?.let { context ->
|
||||
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
|
||||
attachmentsAdapter?.database = database
|
||||
}
|
||||
|
||||
attachmentsListView.adapter = attachmentsAdapter
|
||||
}
|
||||
|
||||
private fun loadTemplateSettings() {
|
||||
context?.let { context ->
|
||||
templateView.setFirstTimeAskAllowCopyProtectedFields(PreferencesUtil.isFirstTimeAskAllowCopyProtectedFields(context))
|
||||
templateView.setAllowCopyProtectedFields(PreferencesUtil.allowCopyProtectedFields(context))
|
||||
}
|
||||
}
|
||||
|
||||
private fun assignEntryInfo(entryInfo: EntryInfo?) {
|
||||
// Set copy buttons
|
||||
templateView.apply {
|
||||
setOnAskCopySafeClickListener {
|
||||
showClipboardDialog()
|
||||
}
|
||||
|
||||
setOnCopyActionClickListener { field ->
|
||||
mClipboardHelper?.timeoutCopyToClipboard(
|
||||
TemplateField.getLocalizedName(context, field.name),
|
||||
field.protectedValue.stringValue,
|
||||
field.protectedValue.isProtected
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Populate entry views
|
||||
templateView.setEntryInfo(entryInfo)
|
||||
|
||||
// OTP timer updated
|
||||
templateView.setOnOtpElementUpdated { otpElementUpdated ->
|
||||
mEntryViewModel.onOtpElementUpdated(otpElementUpdated)
|
||||
}
|
||||
|
||||
// Manage attachments
|
||||
assignAttachments(entryInfo?.attachments ?: listOf())
|
||||
|
||||
// Assign dates
|
||||
creationDateView.text = entryInfo?.creationTime?.getDateTimeString(resources)
|
||||
modificationDateView.text = entryInfo?.lastModificationTime?.getDateTimeString(resources)
|
||||
|
||||
// TODO Custom data
|
||||
// customDataView.text = entryInfo?.customData?.toString()
|
||||
|
||||
// Assign special data
|
||||
uuidReferenceView.text = entryInfo?.id?.asHexString()
|
||||
}
|
||||
|
||||
private fun showClipboardDialog() {
|
||||
context?.let {
|
||||
AlertDialog.Builder(it)
|
||||
.setMessage(
|
||||
getString(R.string.allow_copy_password_warning) +
|
||||
"\n\n" +
|
||||
getString(R.string.clipboard_warning)
|
||||
)
|
||||
.create().apply {
|
||||
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) { dialog, _ ->
|
||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(context, true)
|
||||
finishDialog(dialog)
|
||||
}
|
||||
setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ ->
|
||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(context, false)
|
||||
finishDialog(dialog)
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun finishDialog(dialog: DialogInterface) {
|
||||
dialog.dismiss()
|
||||
loadTemplateSettings()
|
||||
templateView.reload()
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* Attachments
|
||||
* -------------
|
||||
*/
|
||||
|
||||
private fun assignAttachments(attachments: List<Attachment>) {
|
||||
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
|
||||
attachmentsAdapter?.assignItems(attachments.map {
|
||||
EntryAttachmentState(it, StreamDirection.DOWNLOAD)
|
||||
})
|
||||
attachmentsAdapter?.onItemClickListener = { item ->
|
||||
mEntryViewModel.onAttachmentSelected(item.attachment)
|
||||
}
|
||||
}
|
||||
|
||||
fun putAttachment(attachmentToDownload: EntryAttachmentState) {
|
||||
attachmentsAdapter?.putItem(attachmentToDownload)
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* Education
|
||||
* -------------
|
||||
*/
|
||||
|
||||
fun firstEntryFieldCopyView(): View? {
|
||||
return try {
|
||||
templateView.getActionImageView()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun launchEntryCopyEducationAction() {
|
||||
val appNameString = getString(R.string.app_name)
|
||||
mClipboardHelper?.timeoutCopyToClipboard(appNameString, appNameString)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun getInstance(): EntryFragment {
|
||||
return EntryFragment().apply {
|
||||
arguments = Bundle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.kunzisoft.keepass.activities.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.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.adapters.EntryHistoryAdapter
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||
|
||||
class EntryHistoryFragment: Fragment() {
|
||||
|
||||
private lateinit var historyContainerView: View
|
||||
private lateinit var historyListView: RecyclerView
|
||||
private var historyAdapter: EntryHistoryAdapter? = null
|
||||
|
||||
private val mEntryViewModel: EntryViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
return inflater.inflate(R.layout.fragment_entry_history, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
context?.let { context ->
|
||||
historyAdapter = EntryHistoryAdapter(context)
|
||||
}
|
||||
|
||||
historyContainerView = view.findViewById(R.id.entry_history_container)
|
||||
historyListView = view.findViewById(R.id.entry_history_list)
|
||||
historyListView.apply {
|
||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
|
||||
adapter = historyAdapter
|
||||
}
|
||||
|
||||
mEntryViewModel.entryHistory.observe(viewLifecycleOwner) {
|
||||
assignHistory(it)
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* History
|
||||
* -------------
|
||||
*/
|
||||
private fun assignHistory(history: List<EntryInfo>?) {
|
||||
historyAdapter?.clear()
|
||||
history?.let {
|
||||
historyAdapter?.entryHistoryList?.addAll(history)
|
||||
}
|
||||
historyAdapter?.onItemClickListener = { item, position ->
|
||||
mEntryViewModel.onHistorySelected(item, position)
|
||||
}
|
||||
historyContainerView.visibility = if (historyAdapter?.entryHistoryList?.isEmpty() != false)
|
||||
View.GONE
|
||||
else
|
||||
View.VISIBLE
|
||||
historyAdapter?.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -20,38 +20,49 @@
|
||||
package com.kunzisoft.keepass.activities.fragments
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||
import com.kunzisoft.keepass.adapters.NodeAdapter
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.adapters.NodesAdapter
|
||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.retrieveSpecialMode
|
||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||
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 java.util.*
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.KeyboardUtil.hideKeyboard
|
||||
import com.kunzisoft.keepass.viewmodels.GroupViewModel
|
||||
import java.util.LinkedList
|
||||
|
||||
class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener {
|
||||
class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListener {
|
||||
|
||||
private var nodeClickListener: NodeClickListener? = null
|
||||
private var onScrollListener: OnScrollListener? = null
|
||||
private var groupRefreshed: GroupRefreshedListener? = null
|
||||
|
||||
private var mNodesRecyclerView: RecyclerView? = null
|
||||
var mainGroup: Group? = null
|
||||
private set
|
||||
private var mAdapter: NodeAdapter? = null
|
||||
private var mLayoutManager: LinearLayoutManager? = null
|
||||
private var mAdapter: NodesAdapter? = null
|
||||
|
||||
private val mGroupViewModel: GroupViewModel by activityViewModels()
|
||||
|
||||
private var mCurrentGroup: Group? = null
|
||||
|
||||
var nodeActionSelectionMode = false
|
||||
private set
|
||||
@@ -63,21 +74,58 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
private var notFoundView: View? = null
|
||||
private var isASearchResult: Boolean = false
|
||||
|
||||
|
||||
private var readOnly: Boolean = false
|
||||
private var specialMode: SpecialMode = SpecialMode.DEFAULT
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
|
||||
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
super.onScrollStateChanged(recyclerView, newState)
|
||||
if (newState == SCROLL_STATE_IDLE) {
|
||||
mGroupViewModel.assignPosition(getFirstVisiblePosition())
|
||||
}
|
||||
}
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
onScrollListener?.onScrolled(dy)
|
||||
}
|
||||
}
|
||||
|
||||
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 =
|
||||
SortDialogFragment.getInstance(
|
||||
PreferencesUtil.getListSort(context),
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context),
|
||||
if (mDatabase?.isRecycleBinEnabled == true) {
|
||||
PreferencesUtil.getRecycleBinBottomSort(context)
|
||||
} else null
|
||||
)
|
||||
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
|
||||
// TODO Change to ViewModel
|
||||
try {
|
||||
nodeClickListener = context as NodeClickListener
|
||||
} catch (e: ClassCastException) {
|
||||
// The activity doesn't implement the interface, throw exception
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + NodeAdapter.NodeClickCallback::class.java.name)
|
||||
+ " must implement " + NodesAdapter.NodeClickCallback::class.java.name)
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -85,39 +133,32 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
} catch (e: ClassCastException) {
|
||||
onScrollListener = null
|
||||
// Context menu can be omit
|
||||
Log.w(TAG, context.toString()
|
||||
Log.w(
|
||||
TAG, context.toString()
|
||||
+ " must implement " + RecyclerView.OnScrollListener::class.java.name)
|
||||
}
|
||||
|
||||
try {
|
||||
groupRefreshed = context as GroupRefreshedListener
|
||||
} catch (e: ClassCastException) {
|
||||
// The activity doesn't implement the interface, throw exception
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + GroupRefreshedListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
nodeClickListener = null
|
||||
onScrollListener = null
|
||||
groupRefreshed = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, arguments)
|
||||
|
||||
arguments?.let { args ->
|
||||
// Contains all the group in element
|
||||
if (args.containsKey(GROUP_KEY)) {
|
||||
mainGroup = args.getParcelable(GROUP_KEY)
|
||||
}
|
||||
if (args.containsKey(IS_SEARCH)) {
|
||||
isASearchResult = args.getBoolean(IS_SEARCH)
|
||||
}
|
||||
}
|
||||
|
||||
contextThemed?.let { context ->
|
||||
mAdapter = NodeAdapter(context)
|
||||
mAdapter?.apply {
|
||||
setOnNodeClickListener(object : NodeAdapter.NodeClickCallback {
|
||||
override fun onNodeClick(node: Node) {
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
context?.let { context ->
|
||||
mAdapter = NodesAdapter(context, database).apply {
|
||||
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
|
||||
override fun onNodeClick(database: ContextualDatabase, node: Node) {
|
||||
if (nodeActionSelectionMode) {
|
||||
if (listActionNodes.contains(node)) {
|
||||
// Remove selected item if already selected
|
||||
@@ -126,103 +167,118 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
// Add selected item if not already selected
|
||||
listActionNodes.add(node)
|
||||
}
|
||||
nodeClickListener?.onNodeSelected(listActionNodes)
|
||||
nodeClickListener?.onNodeSelected(database, listActionNodes)
|
||||
setActionNodes(listActionNodes)
|
||||
notifyNodeChanged(node)
|
||||
} else {
|
||||
nodeClickListener?.onNodeClick(node)
|
||||
nodeClickListener?.onNodeClick(database, node)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNodeLongClick(node: Node): Boolean {
|
||||
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)
|
||||
|
||||
nodeClickListener?.onNodeSelected(listActionNodes)
|
||||
nodeClickListener?.onNodeSelected(database, listActionNodes)
|
||||
|
||||
setActionNodes(listActionNodes)
|
||||
notifyNodeChanged(node)
|
||||
activity?.hideKeyboard()
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
mNodesRecyclerView?.adapter = mAdapter
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
||||
super.onSaveInstanceState(outState)
|
||||
override fun onDatabaseActionFinished(
|
||||
database: ContextualDatabase,
|
||||
actionTask: String,
|
||||
result: ActionRunnable.Result
|
||||
) {
|
||||
super.onDatabaseActionFinished(database, actionTask, result)
|
||||
|
||||
// Too many special cases to make specific additions or deletions,
|
||||
// rebuilt the list works well.
|
||||
if (result.isSuccess) {
|
||||
rebuildList()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
// To apply theme
|
||||
val rootView = inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.fragment_list_nodes, container, false)
|
||||
mNodesRecyclerView = rootView.findViewById(R.id.nodes_list)
|
||||
notFoundView = rootView.findViewById(R.id.not_found_container)
|
||||
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)
|
||||
|
||||
mLayoutManager = LinearLayoutManager(context)
|
||||
mNodesRecyclerView?.apply {
|
||||
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
layoutManager = mLayoutManager
|
||||
adapter = mAdapter
|
||||
}
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||
|
||||
onScrollListener?.let { onScrollListener ->
|
||||
mNodesRecyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
onScrollListener.onScrolled(dy)
|
||||
}
|
||||
})
|
||||
mGroupViewModel.group.observe(viewLifecycleOwner) {
|
||||
mCurrentGroup = it.group
|
||||
isASearchResult = it.group.isVirtual
|
||||
rebuildList()
|
||||
it.showFromPosition?.let { position ->
|
||||
mNodesRecyclerView?.scrollToPosition(position)
|
||||
}
|
||||
}
|
||||
|
||||
return rootView
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener)
|
||||
activity?.intent?.let {
|
||||
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
|
||||
}
|
||||
|
||||
// Refresh data
|
||||
try {
|
||||
rebuildList()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to rebuild the list during resume")
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
|
||||
// To show the " no search entry found "
|
||||
mNodesRecyclerView?.visibility = View.GONE
|
||||
notFoundView?.visibility = View.VISIBLE
|
||||
} else {
|
||||
mNodesRecyclerView?.visibility = View.VISIBLE
|
||||
notFoundView?.visibility = View.GONE
|
||||
specialMode = it.retrieveSpecialMode()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun rebuildList() {
|
||||
// Add elements to the list
|
||||
mainGroup?.let { mainGroup ->
|
||||
mAdapter?.apply {
|
||||
override fun onPause() {
|
||||
|
||||
mNodesRecyclerView?.removeOnScrollListener(mRecycleViewScrollListener)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
fun getFirstVisiblePosition(): Int {
|
||||
return mLayoutManager?.findFirstVisibleItemPosition() ?: 0
|
||||
}
|
||||
|
||||
private fun rebuildList() {
|
||||
try {
|
||||
// Add elements to the list
|
||||
mCurrentGroup?.let { currentGroup ->
|
||||
// Thrown an exception when sort cannot be performed
|
||||
rebuildList(mainGroup)
|
||||
// To visually change the elements
|
||||
if (PreferencesUtil.APPEARANCE_CHANGED) {
|
||||
notifyDataSetChanged()
|
||||
PreferencesUtil.APPEARANCE_CHANGED = false
|
||||
}
|
||||
mAdapter?.rebuildList(currentGroup)
|
||||
}
|
||||
} catch (e:Exception) {
|
||||
Log.e(TAG, "Unable to rebuild the list", e)
|
||||
}
|
||||
|
||||
if (isASearchResult && mAdapter != null && mAdapter!!.isEmpty) {
|
||||
// To show the " no search entry found "
|
||||
notFoundView?.visibility = View.VISIBLE
|
||||
} else {
|
||||
notFoundView?.visibility = View.GONE
|
||||
}
|
||||
|
||||
groupRefreshed?.onGroupRefreshed()
|
||||
}
|
||||
|
||||
override fun onSortSelected(sortNodeEnum: SortNodeEnum,
|
||||
@@ -237,73 +293,40 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters)
|
||||
rebuildList()
|
||||
} catch (e:Exception) {
|
||||
Log.e(TAG, "Unable to rebuild the list with the sort")
|
||||
e.printStackTrace()
|
||||
Log.e(TAG, "Unable to sort the list", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.tree, menu)
|
||||
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
private fun containsRecycleBin(database: ContextualDatabase?, nodes: List<Node>): Boolean {
|
||||
return database?.isRecycleBinEnabled == true
|
||||
&& nodes.any { it == database.recycleBin }
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
|
||||
R.id.menu_sort -> {
|
||||
context?.let { context ->
|
||||
val sortDialogFragment: SortDialogFragment =
|
||||
if (Database.getInstance().isRecycleBinEnabled) {
|
||||
SortDialogFragment.getInstance(
|
||||
PreferencesUtil.getListSort(context),
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context),
|
||||
PreferencesUtil.getRecycleBinBottomSort(context))
|
||||
} else {
|
||||
SortDialogFragment.getInstance(
|
||||
PreferencesUtil.getListSort(context),
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context))
|
||||
}
|
||||
|
||||
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
fun actionNodesCallback(nodes: List<Node>,
|
||||
fun actionNodesCallback(database: ContextualDatabase,
|
||||
nodes: List<Node>,
|
||||
menuListener: NodesActionMenuListener?,
|
||||
actionModeCallback: ActionMode.Callback) : ActionMode.Callback {
|
||||
onDestroyActionMode: (mode: ActionMode?) -> Unit) : ActionMode.Callback {
|
||||
|
||||
return object : ActionMode.Callback {
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
nodeActionSelectionMode = false
|
||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||
return actionModeCallback.onCreateActionMode(mode, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
menu?.clear()
|
||||
|
||||
if (nodeActionPasteMode != PasteMode.UNDEFINED) {
|
||||
mode?.menuInflater?.inflate(R.menu.node_paste_menu, menu)
|
||||
} else {
|
||||
nodeActionSelectionMode = true
|
||||
mode?.menuInflater?.inflate(R.menu.node_menu, menu)
|
||||
|
||||
val database = Database.getInstance()
|
||||
|
||||
// Open and Edit for a single item
|
||||
if (nodes.size == 1) {
|
||||
// Edition
|
||||
if (readOnly
|
||||
|| (database.isRecycleBinEnabled && nodes[0] == database.recycleBin)) {
|
||||
if (database.isReadOnly || containsRecycleBin(database, nodes)) {
|
||||
menu?.removeItem(R.id.menu_edit)
|
||||
}
|
||||
} else {
|
||||
@@ -311,56 +334,56 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
menu?.removeItem(R.id.menu_edit)
|
||||
}
|
||||
|
||||
// Copy and Move (not for groups)
|
||||
if (readOnly
|
||||
|| isASearchResult
|
||||
|| nodes.any { it.type == Type.GROUP }) {
|
||||
// TODO Copy For Group
|
||||
menu?.removeItem(R.id.menu_copy)
|
||||
// Move
|
||||
if (database.isReadOnly) {
|
||||
menu?.removeItem(R.id.menu_move)
|
||||
}
|
||||
|
||||
// Copy (not allowed for group)
|
||||
if (database.isReadOnly
|
||||
|| nodes.any { it.type == Type.GROUP }) {
|
||||
menu?.removeItem(R.id.menu_copy)
|
||||
}
|
||||
|
||||
// Deletion
|
||||
if (readOnly
|
||||
|| (database.isRecycleBinEnabled && nodes.any { it == database.recycleBin })) {
|
||||
if (database.isReadOnly || containsRecycleBin(database, nodes)) {
|
||||
menu?.removeItem(R.id.menu_delete)
|
||||
}
|
||||
}
|
||||
|
||||
// Add the number of items selected in title
|
||||
mode?.title = nodes.size.toString()
|
||||
|
||||
return actionModeCallback.onPrepareActionMode(mode, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
||||
if (menuListener == null)
|
||||
return false
|
||||
return when (item?.itemId) {
|
||||
R.id.menu_open -> menuListener.onOpenMenuClick(nodes[0])
|
||||
R.id.menu_edit -> menuListener.onEditMenuClick(nodes[0])
|
||||
R.id.menu_open -> menuListener.onOpenMenuClick(database, nodes[0])
|
||||
R.id.menu_edit -> menuListener.onEditMenuClick(database, nodes[0])
|
||||
R.id.menu_copy -> {
|
||||
nodeActionPasteMode = PasteMode.PASTE_FROM_COPY
|
||||
mAdapter?.unselectActionNodes()
|
||||
val returnValue = menuListener.onCopyMenuClick(nodes)
|
||||
val returnValue = menuListener.onCopyMenuClick(database, nodes)
|
||||
nodeActionSelectionMode = false
|
||||
returnValue
|
||||
}
|
||||
R.id.menu_move -> {
|
||||
nodeActionPasteMode = PasteMode.PASTE_FROM_MOVE
|
||||
mAdapter?.unselectActionNodes()
|
||||
val returnValue = menuListener.onMoveMenuClick(nodes)
|
||||
val returnValue = menuListener.onMoveMenuClick(database, nodes)
|
||||
nodeActionSelectionMode = false
|
||||
returnValue
|
||||
}
|
||||
R.id.menu_delete -> menuListener.onDeleteMenuClick(nodes)
|
||||
R.id.menu_delete -> menuListener.onDeleteMenuClick(database, nodes)
|
||||
R.id.menu_paste -> {
|
||||
val returnValue = menuListener.onPasteMenuClick(nodeActionPasteMode, nodes)
|
||||
val returnValue = menuListener.onPasteMenuClick(database, nodeActionPasteMode, nodes)
|
||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||
nodeActionSelectionMode = false
|
||||
returnValue
|
||||
}
|
||||
else -> actionModeCallback.onActionItemClicked(mode, item)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,83 +393,29 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
mAdapter?.unselectActionNodes()
|
||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||
nodeActionSelectionMode = false
|
||||
actionModeCallback.onDestroyActionMode(mode)
|
||||
onDestroyActionMode(mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
when (requestCode) {
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE
|
||||
|| resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
|
||||
data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { changedNode ->
|
||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
|
||||
addNode(changedNode)
|
||||
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE)
|
||||
mAdapter?.notifyDataSetChanged()
|
||||
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun contains(node: Node): Boolean {
|
||||
return mAdapter?.contains(node) ?: false
|
||||
}
|
||||
|
||||
fun addNode(newNode: Node) {
|
||||
mAdapter?.addNode(newNode)
|
||||
}
|
||||
|
||||
fun addNodes(newNodes: List<Node>) {
|
||||
mAdapter?.addNodes(newNodes)
|
||||
}
|
||||
|
||||
fun updateNode(oldNode: Node, newNode: Node? = null) {
|
||||
mAdapter?.updateNode(oldNode, newNode ?: oldNode)
|
||||
}
|
||||
|
||||
fun updateNodes(oldNodes: List<Node>, newNodes: List<Node>) {
|
||||
mAdapter?.updateNodes(oldNodes, newNodes)
|
||||
}
|
||||
|
||||
fun removeNode(pwNode: Node) {
|
||||
mAdapter?.removeNode(pwNode)
|
||||
}
|
||||
|
||||
fun removeNodes(nodes: List<Node>) {
|
||||
mAdapter?.removeNodes(nodes)
|
||||
}
|
||||
|
||||
fun removeNodeAt(position: Int) {
|
||||
mAdapter?.removeNodeAt(position)
|
||||
}
|
||||
|
||||
fun removeNodesAt(positions: IntArray) {
|
||||
mAdapter?.removeNodesAt(positions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback listener to redefine to do an action when a node is click
|
||||
*/
|
||||
interface NodeClickListener {
|
||||
fun onNodeClick(node: Node)
|
||||
fun onNodeSelected(nodes: List<Node>): Boolean
|
||||
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(node: Node): Boolean
|
||||
fun onEditMenuClick(node: Node): Boolean
|
||||
fun onCopyMenuClick(nodes: List<Node>): Boolean
|
||||
fun onMoveMenuClick(nodes: List<Node>): Boolean
|
||||
fun onDeleteMenuClick(nodes: List<Node>): Boolean
|
||||
fun onPasteMenuClick(pasteMode: PasteMode?, nodes: List<Node>): Boolean
|
||||
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 {
|
||||
@@ -464,23 +433,11 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
fun onScrolled(dy: Int)
|
||||
}
|
||||
|
||||
interface GroupRefreshedListener {
|
||||
fun onGroupRefreshed()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = ListNodesFragment::class.java.name
|
||||
|
||||
private const val GROUP_KEY = "GROUP_KEY"
|
||||
private const val IS_SEARCH = "IS_SEARCH"
|
||||
|
||||
fun newInstance(group: Group?, readOnly: Boolean, isASearch: Boolean): ListNodesFragment {
|
||||
val bundle = Bundle()
|
||||
if (group != null) {
|
||||
bundle.putParcelable(GROUP_KEY, group)
|
||||
}
|
||||
bundle.putBoolean(IS_SEARCH, isASearch)
|
||||
ReadOnlyHelper.putReadOnlyInBundle(bundle, readOnly)
|
||||
val listNodesFragment = ListNodesFragment()
|
||||
listNodesFragment.arguments = bundle
|
||||
return listNodesFragment
|
||||
}
|
||||
private val TAG = GroupFragment::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -22,6 +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.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
|
||||
|
||||
@@ -31,8 +32,8 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
|
||||
return R.layout.fragment_icon_grid
|
||||
}
|
||||
|
||||
override fun defineIconList() {
|
||||
mDatabase?.doForEachCustomIcons { customIcon, _ ->
|
||||
override fun defineIconList(database: ContextualDatabase?) {
|
||||
database?.doForEachCustomIcons { customIcon, _ ->
|
||||
iconPickerAdapter.addIcon(customIcon, false)
|
||||
}
|
||||
}
|
||||
@@ -54,8 +55,10 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
|
||||
iconCustomAdded?.iconCustom?.let { icon ->
|
||||
iconPickerAdapter.addIcon(icon)
|
||||
iconCustomAdded.iconCustom = null
|
||||
try {
|
||||
iconsGridView.smoothScrollToPosition(iconPickerAdapter.lastPosition)
|
||||
} catch (ignore: Exception) {}
|
||||
}
|
||||
iconsGridView.smoothScrollToPosition(iconPickerAdapter.lastPosition)
|
||||
}
|
||||
}
|
||||
iconPickerViewModel.customIconRemoved.observe(viewLifecycleOwner) { iconCustomRemoved ->
|
||||
@@ -66,6 +69,14 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
|
||||
}
|
||||
}
|
||||
}
|
||||
iconPickerViewModel.customIconUpdated.observe(viewLifecycleOwner) { iconCustomUpdated ->
|
||||
if (!iconCustomUpdated.error) {
|
||||
iconCustomUpdated?.iconCustom?.let { icon ->
|
||||
iconPickerAdapter.updateIcon(icon)
|
||||
iconCustomUpdated.iconCustom = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIconClickListener(icon: IconImageCustom) {
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.fragments
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
@@ -28,9 +27,8 @@ import android.view.ViewGroup
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||
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
|
||||
@@ -38,39 +36,48 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
abstract class IconFragment<T: IconImageDraw> : StylishFragment(),
|
||||
abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
|
||||
IconPickerAdapter.IconPickerListener<T> {
|
||||
|
||||
protected lateinit var iconsGridView: RecyclerView
|
||||
protected lateinit var iconPickerAdapter: IconPickerAdapter<T>
|
||||
protected var iconActionSelectionMode = false
|
||||
|
||||
protected var mDatabase: Database? = null
|
||||
|
||||
protected val iconPickerViewModel: IconPickerViewModel by activityViewModels()
|
||||
|
||||
abstract fun retrieveMainLayoutId(): Int
|
||||
|
||||
abstract fun defineIconList()
|
||||
abstract fun defineIconList(database: ContextualDatabase?)
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
override fun onCreateView(inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View {
|
||||
return inflater.inflate(retrieveMainLayoutId(), container, false)
|
||||
}
|
||||
|
||||
mDatabase = Database.getInstance()
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
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()
|
||||
|
||||
iconPickerAdapter = IconPickerAdapter<T>(context, tintColor).apply {
|
||||
iconDrawableFactory = mDatabase?.iconDrawableFactory
|
||||
}
|
||||
iconsGridView = view.findViewById(R.id.icons_grid_view)
|
||||
iconPickerAdapter = IconPickerAdapter(requireContext(), tintColor)
|
||||
iconPickerAdapter.iconPickerListener = this
|
||||
iconsGridView.adapter = iconPickerAdapter
|
||||
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
iconPickerAdapter.iconDrawableFactory = database.iconDrawableFactory
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val populateList = launch {
|
||||
iconPickerAdapter.clear()
|
||||
defineIconList()
|
||||
defineIconList(database)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
populateList.join()
|
||||
@@ -79,21 +86,6 @@ abstract class IconFragment<T: IconImageDraw> : StylishFragment(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View {
|
||||
val root = inflater.inflate(retrieveMainLayoutId(), container, false)
|
||||
iconsGridView = root.findViewById(R.id.icons_grid_view)
|
||||
iconsGridView.adapter = iconPickerAdapter
|
||||
return root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
iconPickerAdapter.iconPickerListener = this
|
||||
}
|
||||
|
||||
fun onIconDeleteClicked() {
|
||||
iconActionSelectionMode = false
|
||||
}
|
||||
|
||||
@@ -9,42 +9,32 @@ 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.activities.stylish.StylishFragment
|
||||
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 : StylishFragment() {
|
||||
class IconPickerFragment : DatabaseFragment() {
|
||||
|
||||
private var iconPickerPagerAdapter: IconPickerPagerAdapter? = null
|
||||
private lateinit var viewPager: ViewPager2
|
||||
private lateinit var tabLayout: TabLayout
|
||||
|
||||
private val iconPickerViewModel: IconPickerViewModel by activityViewModels()
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
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?) {
|
||||
mDatabase = Database.getInstance()
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
viewPager = view.findViewById(R.id.icon_picker_pager)
|
||||
val tabLayout = view.findViewById<TabLayout>(R.id.icon_picker_tabs)
|
||||
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
|
||||
if (mDatabase?.allowCustomIcons == true) 2 else 1)
|
||||
viewPager.adapter = iconPickerPagerAdapter
|
||||
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
|
||||
tab.text = when (position) {
|
||||
1 -> getString(R.string.icon_section_custom)
|
||||
else -> getString(R.string.icon_section_standard)
|
||||
}
|
||||
}.attach()
|
||||
viewPager = view.findViewById(R.id.tabs_view_pager)
|
||||
tabLayout = view.findViewById(R.id.tabs_layout)
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||
|
||||
arguments?.apply {
|
||||
if (containsKey(ICON_TAB_ARG)) {
|
||||
@@ -58,6 +48,18 @@ class IconPickerFragment : StylishFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
|
||||
if (database.allowCustomIcons) 2 else 1)
|
||||
viewPager.adapter = iconPickerPagerAdapter
|
||||
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
|
||||
tab.text = when (position) {
|
||||
1 -> getString(R.string.icon_section_custom)
|
||||
else -> getString(R.string.icon_section_standard)
|
||||
}
|
||||
}.attach()
|
||||
}
|
||||
|
||||
enum class IconTab {
|
||||
STANDARD, CUSTOM
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
package com.kunzisoft.keepass.activities.fragments
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
|
||||
|
||||
@@ -29,8 +30,8 @@ class IconStandardFragment : IconFragment<IconImageStandard>() {
|
||||
return R.layout.fragment_icon_grid
|
||||
}
|
||||
|
||||
override fun defineIconList() {
|
||||
mDatabase?.doForEachStandardIcons { standardIcon ->
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||