mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
2576 Commits
2.5.0.0bet
...
2.10.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6a43fd8e5 | ||
|
|
9887b8142d | ||
|
|
4d92d6dc2b | ||
|
|
d8cd84ed9e | ||
|
|
6bc740e881 | ||
|
|
db348cc368 | ||
|
|
6ebf59d7ff | ||
|
|
d35e31d128 | ||
|
|
b9b6d3d2cb | ||
|
|
728b111ac9 | ||
|
|
9e5ce589ae | ||
|
|
f486150a0f | ||
|
|
075ee815f0 | ||
|
|
d321283b13 | ||
|
|
8be382fa7e | ||
|
|
13002f96f1 | ||
|
|
008ded4a5c | ||
|
|
d476574d05 | ||
|
|
371b3813d4 | ||
|
|
fe08d034bb | ||
|
|
18f4714410 | ||
|
|
1b6c416893 | ||
|
|
6153a28b4b | ||
|
|
9574cf16fb | ||
|
|
d309a67416 | ||
|
|
fb865af088 | ||
|
|
c1e7039357 | ||
|
|
0fd3b37641 | ||
|
|
cea91f7b2f | ||
|
|
3959896832 | ||
|
|
d55dccdeb1 | ||
|
|
c46c286b51 | ||
|
|
aa15d261f3 | ||
|
|
00a32463c7 | ||
|
|
dd60ff8b74 | ||
|
|
4588611cbf | ||
|
|
1460c1364a | ||
|
|
37f38fe988 | ||
|
|
cf025b9135 | ||
|
|
283ff7a280 | ||
|
|
e668f016b4 | ||
|
|
256c2c955a | ||
|
|
d560c3e8de | ||
|
|
4f8e8e6669 | ||
|
|
7cdc2e0915 | ||
|
|
74b236b317 | ||
|
|
89ffeaf03b | ||
|
|
8b779a0fca | ||
|
|
4b71dc8445 | ||
|
|
780875d5f2 | ||
|
|
7356d4b0e2 | ||
|
|
654dea6b7e | ||
|
|
1204374637 | ||
|
|
880fde2148 | ||
|
|
d776d76100 | ||
|
|
2a7af826a8 | ||
|
|
9d2fd53073 | ||
|
|
edc5985a7e | ||
|
|
96fc79103b | ||
|
|
76879f3a73 | ||
|
|
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 | ||
|
|
9f16f26347 | ||
|
|
69b4cacab4 | ||
|
|
b74b5040b1 | ||
|
|
a28decc854 | ||
|
|
cb59cef1b8 | ||
|
|
d9b600466c | ||
|
|
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 | ||
|
|
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 | ||
|
|
78d3b369bb | ||
|
|
bb3620680b | ||
|
|
d4a45655ca | ||
|
|
c9c739fd52 | ||
|
|
2b359cc592 | ||
|
|
151b7a323d | ||
|
|
1063dc2b63 | ||
|
|
f9f59a6eb1 | ||
|
|
73156cc337 | ||
|
|
7d53607f49 | ||
|
|
7539945465 | ||
|
|
51df8e7bb1 | ||
|
|
17029ce67c | ||
|
|
8cedc313cf | ||
|
|
5afe3acac1 | ||
|
|
9887b58b71 | ||
|
|
ec8363ba6a | ||
|
|
fcfb71f13b | ||
|
|
3a12e431ff | ||
|
|
bc4ed8e123 | ||
|
|
445e9540a5 | ||
|
|
bbc2a2a9dd | ||
|
|
5117bc78b6 | ||
|
|
10bf149a07 | ||
|
|
0a976bd012 | ||
|
|
df31c43e59 | ||
|
|
c5d30b9b23 | ||
|
|
2e18beff27 | ||
|
|
25e0cec2cc | ||
|
|
16cc4c5c97 | ||
|
|
e5cfb6b7eb | ||
|
|
a882ba07e9 | ||
|
|
801f3f99aa | ||
|
|
2338b9b57d | ||
|
|
8ba396c693 | ||
|
|
1164022765 | ||
|
|
b0c5519da5 | ||
|
|
f5073238d8 | ||
|
|
3ffa89bfaf | ||
|
|
26d8b2fa22 | ||
|
|
6b17502694 | ||
|
|
a69d57a4f4 | ||
|
|
430bc6150f | ||
|
|
b888615e0d | ||
|
|
9fae343668 | ||
|
|
db467889b0 | ||
|
|
153b8d1f37 | ||
|
|
87858762d4 | ||
|
|
50d3282a65 | ||
|
|
aee0500b38 | ||
|
|
f2ef6eb94e | ||
|
|
151a5a7e73 | ||
|
|
6a088c58de | ||
|
|
23e7bf9f89 | ||
|
|
59f24206ad | ||
|
|
e45ef019c0 | ||
|
|
2830d3c8fa | ||
|
|
088816dfab | ||
|
|
453a29b81c | ||
|
|
e5cb160aa4 | ||
|
|
844588a0d4 | ||
|
|
cfcfd47705 | ||
|
|
f416c0ec7d | ||
|
|
78f707c07c | ||
|
|
d28a59a2fe | ||
|
|
edf3525a3f | ||
|
|
2b7fe35305 | ||
|
|
d5819ea4d0 | ||
|
|
1099126def | ||
|
|
4ba3a797e3 | ||
|
|
51b9bb88e5 | ||
|
|
fa376148bd | ||
|
|
452b9677da | ||
|
|
02a779f9a2 | ||
|
|
ea60645247 | ||
|
|
b444a13285 | ||
|
|
0c6b2a13eb | ||
|
|
e10bdc1169 | ||
|
|
7512cffca3 | ||
|
|
203440e9b8 | ||
|
|
145030e854 | ||
|
|
2b81dfb100 | ||
|
|
c96ace5281 | ||
|
|
520c6b60be | ||
|
|
492382d552 | ||
|
|
7ca55dd531 | ||
|
|
ce4ba73fc4 | ||
|
|
5622d92cbb | ||
|
|
8de1c5fd36 | ||
|
|
6c84fea8dc | ||
|
|
0b94070086 | ||
|
|
2f209182f5 | ||
|
|
d7bc572f3e | ||
|
|
4985b49194 | ||
|
|
a792df2021 | ||
|
|
a42ec74723 | ||
|
|
de3dbe3b36 | ||
|
|
874fdb7da0 | ||
|
|
3bf0de3888 | ||
|
|
d4020c5e0f | ||
|
|
6175fc00ad | ||
|
|
76e996f429 | ||
|
|
fd222b73ce | ||
|
|
a8cc0b1edf | ||
|
|
ea2f3545a6 | ||
|
|
0cf136712a | ||
|
|
34a453873a | ||
|
|
0acac3b096 | ||
|
|
37141410e0 | ||
|
|
69b0e276e3 | ||
|
|
5872376f50 | ||
|
|
075f72d9f6 | ||
|
|
4441ec1b14 | ||
|
|
0735cc1a54 | ||
|
|
dea2ad6904 | ||
|
|
0f0b6b4a8a | ||
|
|
174e562dcb | ||
|
|
f080750545 | ||
|
|
621415fe51 | ||
|
|
428c2818a5 | ||
|
|
5aa1c70999 | ||
|
|
3508f47842 | ||
|
|
62d4993e6d | ||
|
|
ede6070e43 | ||
|
|
a61b1d4337 | ||
|
|
631d946dcf | ||
|
|
8945334f37 | ||
|
|
af2df11a56 | ||
|
|
6dc0c42b1e | ||
|
|
0328293746 | ||
|
|
ad406947cf | ||
|
|
6bb4c1171f | ||
|
|
0f8c71a9df | ||
|
|
faa39190fc | ||
|
|
7c0b925c96 | ||
|
|
1b8c453fd0 | ||
|
|
8cc8f595bd | ||
|
|
cb0b6e010d | ||
|
|
23dc7be1ab | ||
|
|
4b14ad07d2 | ||
|
|
9a5a8ae23a | ||
|
|
02b27e235c | ||
|
|
8a4bf7896f | ||
|
|
208ea29643 | ||
|
|
7c52ec731a | ||
|
|
c6ee38e435 | ||
|
|
65253cc5b9 | ||
|
|
d1a1a23cbc | ||
|
|
e8bb3a5ba7 | ||
|
|
22b8f82770 | ||
|
|
5874c5b9cb | ||
|
|
fad09b2cd5 | ||
|
|
cfb08afd7d | ||
|
|
16d939c601 | ||
|
|
af072648c1 | ||
|
|
45d2609494 | ||
|
|
f5ea65f18c | ||
|
|
e9fc6bed23 | ||
|
|
ccc8e4664d | ||
|
|
651ef04137 | ||
|
|
063aba333c | ||
|
|
a3a517ff89 | ||
|
|
40da29b681 | ||
|
|
684d81c895 | ||
|
|
c6ddd3b238 | ||
|
|
3186413bee | ||
|
|
aae1c4cf1c | ||
|
|
e64f264f12 | ||
|
|
08cf747f52 | ||
|
|
383437a3c7 | ||
|
|
c7cba3f50b | ||
|
|
7feb499d50 | ||
|
|
2e6c25b651 | ||
|
|
192903e8d7 | ||
|
|
6f72ade4d4 | ||
|
|
7e3fc0fa59 | ||
|
|
4ea896b57c | ||
|
|
073ccb9b52 | ||
|
|
f32c944d31 | ||
|
|
acd1e3bdfc | ||
|
|
774cbdf0fe | ||
|
|
f5fd527590 | ||
|
|
7ac9a7e94a | ||
|
|
5735f7a945 | ||
|
|
f8f423b5c1 | ||
|
|
81ba7f0721 | ||
|
|
e6be8c23fb | ||
|
|
9cc1764a18 | ||
|
|
6a77adc313 | ||
|
|
e532572d5a | ||
|
|
dcae49c5f8 | ||
|
|
7321c01e8c | ||
|
|
a85b9998c3 | ||
|
|
30d2ce43d1 | ||
|
|
d46edfc9b7 | ||
|
|
79d11138e6 | ||
|
|
f5cd019b6c | ||
|
|
42c4de56fd | ||
|
|
44b3c28a2a | ||
|
|
e5184a1568 | ||
|
|
76d60ded4c | ||
|
|
d2e7e925f7 | ||
|
|
6357a30acb | ||
|
|
4b1fdd0e38 | ||
|
|
227fc060b9 | ||
|
|
32e5aba906 | ||
|
|
55013bb220 | ||
|
|
5544b20d7f | ||
|
|
d6c7f9c68b | ||
|
|
8b5004e500 | ||
|
|
d6cf11b87d | ||
|
|
d4c3a3be6b | ||
|
|
e724b188ef | ||
|
|
ebf92b1103 | ||
|
|
42e2a49af6 | ||
|
|
be7cd3275a | ||
|
|
966df11beb | ||
|
|
fad852f00d | ||
|
|
9c3e6eb823 | ||
|
|
8b88f72efc | ||
|
|
e0aab6cfbf | ||
|
|
29a2e60e05 | ||
|
|
12df74b3a7 | ||
|
|
22d943c9e2 | ||
|
|
5839f51f44 | ||
|
|
4ecb8d4483 | ||
|
|
a08035551a | ||
|
|
c2460d7262 | ||
|
|
4776eac07e | ||
|
|
4952d107dd | ||
|
|
b5d6ee9dee | ||
|
|
e7a30c6024 | ||
|
|
6578e52ec5 | ||
|
|
97508beb5c | ||
|
|
e063b0d6fc | ||
|
|
bb65dc0e81 | ||
|
|
09e00ec119 | ||
|
|
985f8fad3b | ||
|
|
a9accc8c42 | ||
|
|
41316d2bd3 | ||
|
|
2c172eb8d3 | ||
|
|
d49827f9f8 | ||
|
|
b2aafda2b1 | ||
|
|
b4c50e0262 | ||
|
|
fbe51c12c1 | ||
|
|
991959416b | ||
|
|
41f0e61f60 | ||
|
|
eab8cd101f | ||
|
|
97765d798c | ||
|
|
a7f76248ac | ||
|
|
8de670fcf2 | ||
|
|
744823cce4 | ||
|
|
d95a3e00aa | ||
|
|
ccc190f7b0 | ||
|
|
3e6cd98cb9 | ||
|
|
0e3b8fdbb6 | ||
|
|
5aa3f79616 | ||
|
|
42427d0690 | ||
|
|
1a7b32e6d1 | ||
|
|
83555bfdc5 | ||
|
|
03990c1dd9 | ||
|
|
b361be5cb0 | ||
|
|
d02f6d1e67 | ||
|
|
1e56c34e2f | ||
|
|
2a2f8dcecd | ||
|
|
b609ed3ad4 | ||
|
|
80521f8ec2 | ||
|
|
3a5df6a893 | ||
|
|
0e60c4f910 | ||
|
|
f5f2d3c883 | ||
|
|
3c830bfaf2 | ||
|
|
98caf9b5bf | ||
|
|
cfbb8fab1b | ||
|
|
3069e5e566 | ||
|
|
ac050a09e8 | ||
|
|
78406ccdbf | ||
|
|
2efea1bb00 | ||
|
|
0157a160f0 | ||
|
|
eb4084a6a4 | ||
|
|
63f5e5416f | ||
|
|
38e0433e8f | ||
|
|
fc5ffc5f62 | ||
|
|
460e3558a9 | ||
|
|
b06f223124 | ||
|
|
aabfb2adfd | ||
|
|
f9c47c9035 | ||
|
|
e9485ebf56 | ||
|
|
fcd7fd2889 | ||
|
|
279f4a347a | ||
|
|
b29fe23403 | ||
|
|
1972a551e9 | ||
|
|
7e2ffd2ec4 | ||
|
|
06793ae13e | ||
|
|
26e961d356 | ||
|
|
da956d3bd5 | ||
|
|
318a72a123 | ||
|
|
5c4b4864d2 | ||
|
|
4ab31fe21a | ||
|
|
c22a213635 | ||
|
|
aa166a0104 | ||
|
|
2a36626731 | ||
|
|
2f2360fd48 | ||
|
|
e466643229 | ||
|
|
a9044c3dc4 | ||
|
|
2d28cc21a0 | ||
|
|
ebb0e7e118 | ||
|
|
8205454858 | ||
|
|
e909112ab8 | ||
|
|
469923855a | ||
|
|
f2aca08886 | ||
|
|
9900f8fecb | ||
|
|
73224887b9 | ||
|
|
e31574015b | ||
|
|
ef26251469 | ||
|
|
798bce2759 | ||
|
|
88fee5f6de | ||
|
|
937695a1e5 | ||
|
|
a43b580d67 | ||
|
|
b8d0bff22b | ||
|
|
c180d38394 | ||
|
|
10f7d955ff | ||
|
|
143651099a | ||
|
|
63bb12269f | ||
|
|
00ee80184e | ||
|
|
60ba058515 | ||
|
|
1cf8131b6c | ||
|
|
1b38bd59ef | ||
|
|
2e409c3246 | ||
|
|
63fbca8029 | ||
|
|
b37966f79c | ||
|
|
bac5b0de5b | ||
|
|
5dac161553 | ||
|
|
528c167a88 | ||
|
|
cfe01aa996 | ||
|
|
0f53c975cc | ||
|
|
b7b99c77c8 | ||
|
|
1eea5412a5 | ||
|
|
4240465930 | ||
|
|
3dfbf7d2ad | ||
|
|
440490a4bb | ||
|
|
746382811b | ||
|
|
967a54dd50 | ||
|
|
289d9a2531 | ||
|
|
c56b4964fe | ||
|
|
79dbb942f9 | ||
|
|
a5bb5635d3 | ||
|
|
3efe43c0fe | ||
|
|
5fb5299d34 | ||
|
|
b988882251 | ||
|
|
85e82e3fb9 | ||
|
|
35cfe261d2 | ||
|
|
fe4faf9ebc | ||
|
|
751392d656 | ||
|
|
2e5ce5e94f | ||
|
|
535eeb2594 | ||
|
|
8f5e0e93ee | ||
|
|
6f17c5dcac | ||
|
|
4e7c7ba8ce | ||
|
|
6bd5b2345c | ||
|
|
63832ef8fd | ||
|
|
3719bf3593 | ||
|
|
7bca41ca72 | ||
|
|
3f6a9c3af5 | ||
|
|
309380bdd5 | ||
|
|
f135bdb905 | ||
|
|
2b926fd157 | ||
|
|
e911eea69c | ||
|
|
9d182b8299 | ||
|
|
61366e000f | ||
|
|
21cf49f4f8 | ||
|
|
30f0de83d3 | ||
|
|
42278a4b66 | ||
|
|
8752f92cea | ||
|
|
064c468e62 | ||
|
|
ef78fb749c | ||
|
|
a5d1db392b | ||
|
|
6234fc2ca3 | ||
|
|
c9cf90cdc9 | ||
|
|
5b033975b6 | ||
|
|
5663a153f7 | ||
|
|
4b73c45e65 | ||
|
|
ac248d8b73 | ||
|
|
726b0d0fa3 | ||
|
|
2d40164549 | ||
|
|
5203152f78 | ||
|
|
b064bb74cd | ||
|
|
843d8e8e77 | ||
|
|
c5f95b243d | ||
|
|
06f1f4c8ad | ||
|
|
9847f834c2 | ||
|
|
b744d58e6c | ||
|
|
c95358b344 | ||
|
|
2f15d6c9f2 | ||
|
|
d13aa047d5 | ||
|
|
7590d18c67 | ||
|
|
a689116c97 | ||
|
|
3bf7459f05 | ||
|
|
c70faaedd1 | ||
|
|
cb2417fbe4 | ||
|
|
65dd996f2e | ||
|
|
01790a6f31 | ||
|
|
8b84bb893d | ||
|
|
8cb99847c5 | ||
|
|
b3e01277d4 | ||
|
|
b10d407659 | ||
|
|
b7c5a5d238 | ||
|
|
90d1ce63e8 | ||
|
|
9e30b4e5f7 | ||
|
|
e541a8c629 | ||
|
|
0afe25c922 | ||
|
|
bca133430f | ||
|
|
9c925518a7 | ||
|
|
18e79b99e7 | ||
|
|
4e02846df9 | ||
|
|
2268b78bba | ||
|
|
ea8acd0677 | ||
|
|
9e931dd03f | ||
|
|
19e3aabca4 | ||
|
|
ae697d82d5 | ||
|
|
33382273c3 | ||
|
|
7d8466d77a | ||
|
|
5ca08a00d2 | ||
|
|
df3942697e | ||
|
|
5700ca5bcf | ||
|
|
54b2419d64 | ||
|
|
d8b1c94b78 | ||
|
|
2d1ffc23b9 | ||
|
|
e6b33d60c3 | ||
|
|
3fd06890d7 | ||
|
|
4af4ad7663 | ||
|
|
6ca8501e28 | ||
|
|
432b385f60 | ||
|
|
6cebdefa4a | ||
|
|
bc665eb83d | ||
|
|
cb187300fe | ||
|
|
da761614bd | ||
|
|
f34e007ecd | ||
|
|
3b6ad080b4 | ||
|
|
9919e90ba5 | ||
|
|
f4af44925b | ||
|
|
4bb366b568 | ||
|
|
7e7ab4ce19 | ||
|
|
4d833d25ce | ||
|
|
a9c508ecd9 | ||
|
|
ef4dbb8fdb | ||
|
|
9eb66face5 | ||
|
|
3fd13f3e3b | ||
|
|
319c9cad4b | ||
|
|
c12297c98d | ||
|
|
7c38361844 | ||
|
|
559554a975 | ||
|
|
7e2ffa2124 | ||
|
|
66dbac4bb2 | ||
|
|
8b6a843a85 | ||
|
|
976cff2751 | ||
|
|
f7c30fa8eb | ||
|
|
7757c8218b | ||
|
|
2928b7daa3 | ||
|
|
3a55dea276 | ||
|
|
2a25213d66 | ||
|
|
035ffd8135 | ||
|
|
b040487f1f | ||
|
|
6fc821aecf | ||
|
|
cdceb1fb6f | ||
|
|
07d185913d | ||
|
|
f2a245a9c8 | ||
|
|
33338f4759 | ||
|
|
f7a4370b29 | ||
|
|
77b7afedda | ||
|
|
caa13039e5 | ||
|
|
02845d93ed | ||
|
|
9ef4695cc7 | ||
|
|
d619e089c0 | ||
|
|
3c50348a79 | ||
|
|
167ea3b82b | ||
|
|
9eda3e62f7 | ||
|
|
99c4319b51 | ||
|
|
790b25db65 | ||
|
|
97d4972f9a | ||
|
|
8e6853756f | ||
|
|
6d3aae187b | ||
|
|
b8c7acf7ce | ||
|
|
17a356ae76 | ||
|
|
bd847e632d | ||
|
|
2bfb9b048d | ||
|
|
dc40b50b65 | ||
|
|
3e2271e596 | ||
|
|
4b4fd2a11d | ||
|
|
23468290df | ||
|
|
a276f6aa06 | ||
|
|
f2a58361a1 | ||
|
|
271023b528 | ||
|
|
e1771ca249 | ||
|
|
ca4f4bd151 | ||
|
|
d81454d618 | ||
|
|
fb43c1c624 | ||
|
|
9e060f878d | ||
|
|
bd9c21ee8a | ||
|
|
3e6d40e8da | ||
|
|
79683cb3fc | ||
|
|
52a2090a31 | ||
|
|
3dfe4ace7b | ||
|
|
bd0d17b134 | ||
|
|
6b0ccc1780 | ||
|
|
d75ac4b825 | ||
|
|
b60d610d02 | ||
|
|
f7a5c5d0ea | ||
|
|
28f79aec11 | ||
|
|
778d963fbf | ||
|
|
a765bc84e7 | ||
|
|
804ecc1baa | ||
|
|
d331c3dc03 | ||
|
|
7010d2f86a | ||
|
|
b1d6117eb2 | ||
|
|
f3b814388d | ||
|
|
b62996a57c | ||
|
|
a49e056f02 | ||
|
|
a6dece16bf | ||
|
|
8e3ddd64d2 | ||
|
|
45a847fa3e | ||
|
|
6b6f03b143 | ||
|
|
5446efca4a | ||
|
|
8d04a7f90b | ||
|
|
626495c19e | ||
|
|
e5a198f524 | ||
|
|
161524843f | ||
|
|
5550e7dea3 | ||
|
|
64f66c290c | ||
|
|
e8925b3c0b | ||
|
|
cf67ce04a8 | ||
|
|
84ee4ca2c7 | ||
|
|
27eb095720 | ||
|
|
d273f21819 | ||
|
|
455fd0cd6d | ||
|
|
c5a8650c81 | ||
|
|
b5f9bbed5e | ||
|
|
45da17adb8 | ||
|
|
e789741090 | ||
|
|
5c6d93bc57 | ||
|
|
697b672038 | ||
|
|
2d9e9c24a8 | ||
|
|
5fb281c800 | ||
|
|
96896c1c42 | ||
|
|
d7052bd9e6 | ||
|
|
8b23932788 | ||
|
|
50912c6966 | ||
|
|
53b51934b9 | ||
|
|
a8a3685965 | ||
|
|
149b67e28b | ||
|
|
a83032bffa | ||
|
|
a5d3a153bf | ||
|
|
4210c155eb | ||
|
|
4bf110a9b1 | ||
|
|
50f2684500 | ||
|
|
e95424b8f9 | ||
|
|
8462882707 | ||
|
|
a5c8d25f64 | ||
|
|
689ce2f9b3 | ||
|
|
54246533ac | ||
|
|
66e4b0fe47 | ||
|
|
3e8ae3e2e3 | ||
|
|
d856ef3772 | ||
|
|
5727880ac7 | ||
|
|
ec4302a780 | ||
|
|
d4203598a1 | ||
|
|
a278c8c718 | ||
|
|
faf5f4b51a | ||
|
|
b2f503b326 | ||
|
|
beb5484bf6 | ||
|
|
ec63d75349 | ||
|
|
4c0e79b245 | ||
|
|
50a77684c1 | ||
|
|
8bb84b486d | ||
|
|
4b05f2536f | ||
|
|
d6f968fe7e | ||
|
|
ed758edd44 | ||
|
|
94b7fce2e5 | ||
|
|
dbd9c6cbb7 | ||
|
|
0f6376fb80 | ||
|
|
9522328238 | ||
|
|
e6ad716119 | ||
|
|
440006bb08 | ||
|
|
ea289ef7cf | ||
|
|
352b171484 | ||
|
|
969ab56bf8 | ||
|
|
062a9852e5 | ||
|
|
dd77d7a5e6 | ||
|
|
f60e32522a | ||
|
|
070a91f19c | ||
|
|
0790e80670 | ||
|
|
20841e3d7b | ||
|
|
0caae233c3 | ||
|
|
5497d8fafb | ||
|
|
f69b43249c | ||
|
|
b606fd98f6 | ||
|
|
ba3b7b0f1f | ||
|
|
058d82dc36 | ||
|
|
6f0b0ac4fa | ||
|
|
35f87b0f94 | ||
|
|
0ead9ce9b4 | ||
|
|
80479a6a7c | ||
|
|
a7cea8201e | ||
|
|
081a7fa798 | ||
|
|
85782c4f93 | ||
|
|
d7b7df26d7 | ||
|
|
b6b1c8e31d | ||
|
|
17156f7ca2 | ||
|
|
0761d356b8 | ||
|
|
6da747ce6f | ||
|
|
87b1a1f527 | ||
|
|
72a8a55faf | ||
|
|
9a6a709746 | ||
|
|
428b53cc56 | ||
|
|
e688859e32 | ||
|
|
98336da116 | ||
|
|
c037e443b0 | ||
|
|
d339a50e0a | ||
|
|
7d836f2633 | ||
|
|
45d8470b4c | ||
|
|
1ca3bfe472 | ||
|
|
066da83d70 | ||
|
|
44ab881751 | ||
|
|
5ab3cf985a | ||
|
|
f271f2b181 | ||
|
|
91d75be0ea | ||
|
|
774dddca54 | ||
|
|
e18b3436c9 | ||
|
|
fcb1b5ae6b | ||
|
|
de980d030a | ||
|
|
0e859646fe | ||
|
|
059c7b7713 | ||
|
|
5fb7bf71c8 | ||
|
|
8b0133ff7f | ||
|
|
8d834946b8 | ||
|
|
2f646395d4 | ||
|
|
f6e79ba37b | ||
|
|
e633c7a861 | ||
|
|
dc02a8d78c | ||
|
|
baa9b88512 | ||
|
|
c522e87da8 | ||
|
|
ef5ebf2c15 | ||
|
|
4b147e770c | ||
|
|
157a5c0b05 | ||
|
|
f2288b0c64 | ||
|
|
d8506450aa | ||
|
|
f9b085e73f | ||
|
|
388cf6a91b | ||
|
|
9e6e77b363 | ||
|
|
ec33ca8173 | ||
|
|
6be0457947 | ||
|
|
f3b84aa845 | ||
|
|
bd0b5b0954 | ||
|
|
7dc93604ad | ||
|
|
0ab22698a6 | ||
|
|
c885ce7aaf | ||
|
|
92d1a7b901 | ||
|
|
6119054b45 | ||
|
|
e7aed72398 | ||
|
|
cee7fa50f5 | ||
|
|
39a38bb223 | ||
|
|
7159a993db | ||
|
|
23933e80e3 | ||
|
|
abc971b5cc | ||
|
|
7dedcc8a21 | ||
|
|
10d46e5dee | ||
|
|
139f7eb36d | ||
|
|
1ddfa894b6 | ||
|
|
d1695ab8c2 | ||
|
|
f27979e729 | ||
|
|
6e61e8172a | ||
|
|
21890894ae | ||
|
|
1feecd559d | ||
|
|
6ea4afe75b | ||
|
|
fd96f6367d | ||
|
|
8ce183c4c9 | ||
|
|
407a1db101 | ||
|
|
622d096e31 | ||
|
|
bf27fb1f89 | ||
|
|
860b9055c5 | ||
|
|
b3ae3a4148 | ||
|
|
0abd7d5762 | ||
|
|
0aac2bc55b | ||
|
|
fa08dc5cfb | ||
|
|
8d18970b4c | ||
|
|
173f5ce979 | ||
|
|
2e7088310a | ||
|
|
c75d99030c | ||
|
|
e4ba1d9bae | ||
|
|
e2886c342a | ||
|
|
e600d8a56c | ||
|
|
caeb305475 | ||
|
|
3d3a9d9bad | ||
|
|
5499ad5b94 | ||
|
|
0e29cd0cee | ||
|
|
24fb1b1a8f | ||
|
|
03fb4cbf0c | ||
|
|
e909280d5b | ||
|
|
d41ddf60b4 | ||
|
|
1e01a74986 | ||
|
|
96a007aace | ||
|
|
9f23bb6129 | ||
|
|
b7e8559773 | ||
|
|
5b247575c8 | ||
|
|
eb0e5b478f | ||
|
|
08906ae1da | ||
|
|
395a5efecd | ||
|
|
0452dd14f6 | ||
|
|
3906df314d | ||
|
|
ce49aa2ebd | ||
|
|
f2cb062b1e | ||
|
|
f25819a940 | ||
|
|
3075a9f9f4 | ||
|
|
52f1a672c8 | ||
|
|
45785fde1c | ||
|
|
6a7649e1d7 | ||
|
|
8b3831eb2b | ||
|
|
73e7f4669c | ||
|
|
c9f7bbbd25 | ||
|
|
ee67238133 | ||
|
|
b425da8d0f | ||
|
|
754a7f70bc | ||
|
|
a857ffa987 | ||
|
|
391ce2ebba | ||
|
|
086723adf4 | ||
|
|
e993279c35 | ||
|
|
aa64310875 | ||
|
|
795baf2c01 | ||
|
|
68ac453100 | ||
|
|
79d1f512e5 | ||
|
|
e739211314 | ||
|
|
d3f6374bb4 | ||
|
|
5add632cbc | ||
|
|
d210d1bcce | ||
|
|
6d6422cd63 | ||
|
|
66e8b7702b | ||
|
|
b75502ad87 | ||
|
|
3fba96d11f | ||
|
|
58d10672ea | ||
|
|
3571905705 | ||
|
|
acf0e2a1cb | ||
|
|
9e7dcb0d7c | ||
|
|
3c261e3cf7 | ||
|
|
b6f324f399 | ||
|
|
f2459489fa | ||
|
|
f8691cf285 | ||
|
|
2e631d3c42 | ||
|
|
1044dca936 | ||
|
|
56c3f495d5 | ||
|
|
0f3036dd9c | ||
|
|
af445ef157 | ||
|
|
25eb09f11c | ||
|
|
16f255aeca | ||
|
|
d0b340837d | ||
|
|
893828ac44 | ||
|
|
a3ca03636a | ||
|
|
582ffe3f23 | ||
|
|
3caad2cceb | ||
|
|
618dcf014d | ||
|
|
d88e20bb56 | ||
|
|
8a8b2b027e | ||
|
|
41cb223099 | ||
|
|
b93ea5e662 | ||
|
|
31c35939fd | ||
|
|
20a35f4221 | ||
|
|
bc6aeb2e93 | ||
|
|
a561299809 | ||
|
|
76efb938ab | ||
|
|
56abf73eaf | ||
|
|
2bc068d65a | ||
|
|
f191259f37 | ||
|
|
1384c6661d | ||
|
|
c047621548 | ||
|
|
017aaf2e54 | ||
|
|
e4b2b930af | ||
|
|
2646c0f0ee | ||
|
|
4afadb779c | ||
|
|
ad6e4daa22 | ||
|
|
d5cd07fe76 | ||
|
|
364065ed51 | ||
|
|
b4f05d4da7 | ||
|
|
3d12a0e8e9 | ||
|
|
e29f3194f3 | ||
|
|
6bac86638b | ||
|
|
d6ee1cdf6e | ||
|
|
e9d0efaf93 | ||
|
|
85467fa15b | ||
|
|
84bb47aa53 | ||
|
|
75f245c7dc | ||
|
|
6c5be88432 | ||
|
|
590b22de69 | ||
|
|
4770269f6f | ||
|
|
823f591aa8 | ||
|
|
c88c489633 | ||
|
|
8afb58a044 | ||
|
|
428fa8a61b | ||
|
|
3ba5e1ee79 | ||
|
|
d46d0cb384 | ||
|
|
f841884557 | ||
|
|
d381ab5316 | ||
|
|
a3e8e7ae77 | ||
|
|
a405753827 | ||
|
|
8df74d2c4b | ||
|
|
cbe0ffe52a | ||
|
|
a5631a0476 | ||
|
|
e4bd704a53 | ||
|
|
81a53440bc | ||
|
|
3466de1990 | ||
|
|
253b053c2c | ||
|
|
928d012046 | ||
|
|
19367406c6 | ||
|
|
f4e3717dd3 | ||
|
|
c01e1d91c5 | ||
|
|
6983f9f0b6 | ||
|
|
c13cf1a86c | ||
|
|
2a3dafe07f | ||
|
|
0104d02442 | ||
|
|
8cef4fde82 | ||
|
|
3dc02516ea | ||
|
|
3614529fda | ||
|
|
80d4f06e56 | ||
|
|
9ff4f395b5 | ||
|
|
61b8fa116a | ||
|
|
22a3541b7b | ||
|
|
c57515fed5 | ||
|
|
eec6199413 | ||
|
|
fe48955b94 | ||
|
|
03047ae6dd | ||
|
|
856d4867b4 | ||
|
|
b1b1aa0e13 | ||
|
|
ab68472698 | ||
|
|
5ae4ee1411 | ||
|
|
f1989bac21 | ||
|
|
1b20188b98 | ||
|
|
140a79d18c | ||
|
|
a9610ced0e | ||
|
|
17faee7719 | ||
|
|
120ca1c02c | ||
|
|
ef2d1ebe4f | ||
|
|
b9c931c97f | ||
|
|
60dd963d7d | ||
|
|
c414fac815 | ||
|
|
95041d6a0c | ||
|
|
1fab9c3279 | ||
|
|
3b8661249e | ||
|
|
2fba831851 | ||
|
|
fd4ac14ab3 | ||
|
|
1d9b9e9bfa | ||
|
|
14b2277313 | ||
|
|
0f57bf5235 | ||
|
|
4bb7fae1e3 | ||
|
|
ec0b8ebc92 | ||
|
|
1727633f5d | ||
|
|
668a77cb5a | ||
|
|
e87ee5e091 | ||
|
|
b7acc0e6da | ||
|
|
109f34680f | ||
|
|
fd57ccc1a7 | ||
|
|
e3dfae246a | ||
|
|
7186bbbe6c | ||
|
|
c7b2ce37b1 | ||
|
|
5b3a602911 | ||
|
|
6d51edd94d | ||
|
|
dcb68a6538 | ||
|
|
d549b86c81 | ||
|
|
ac415f1384 | ||
|
|
80ed5800a0 | ||
|
|
291ed44621 | ||
|
|
a7e8915ea0 | ||
|
|
a027c76af3 | ||
|
|
ec375bd068 | ||
|
|
f5a28c83f0 | ||
|
|
813240e233 | ||
|
|
051ac0e669 | ||
|
|
5c798c4569 | ||
|
|
cc2146e397 | ||
|
|
8356514bf8 | ||
|
|
464bc10860 | ||
|
|
5436c8bed1 | ||
|
|
1b163b161b | ||
|
|
f0ee5fd946 | ||
|
|
066a9f871c | ||
|
|
e23841f5bb | ||
|
|
571e66fae5 | ||
|
|
402aa280e0 | ||
|
|
65de5df319 | ||
|
|
63168afc85 | ||
|
|
1c61f54df6 | ||
|
|
50b5ad1799 | ||
|
|
6457c02a35 | ||
|
|
7b242c9733 | ||
|
|
e0ab6137e7 | ||
|
|
49b23a33e7 | ||
|
|
c5e9d14199 | ||
|
|
f7e8662bdf | ||
|
|
7d356d1e34 | ||
|
|
f9bb70f395 | ||
|
|
4335809468 | ||
|
|
2a1b1d28bd | ||
|
|
38d69428d8 | ||
|
|
0c22a8135d | ||
|
|
2b50665f9e | ||
|
|
59607efa62 | ||
|
|
cd5cfbe009 | ||
|
|
84e6d96ce0 | ||
|
|
8f00b53fab | ||
|
|
e2164a1a9c | ||
|
|
6e5dcaf08d | ||
|
|
18a2aae66a | ||
|
|
4c0aab15fa | ||
|
|
75a7d4188b | ||
|
|
ff0da57aeb | ||
|
|
d716cba46b | ||
|
|
5d41e44141 | ||
|
|
281936ecd0 | ||
|
|
da23321c0d | ||
|
|
110e2b7580 | ||
|
|
546d4353e2 | ||
|
|
15b5d36cd6 | ||
|
|
6a935a49ea | ||
|
|
f7f515481f | ||
|
|
3aab37c0c0 | ||
|
|
123e626df6 | ||
|
|
758985675d | ||
|
|
631ebc657b | ||
|
|
d0ca714482 | ||
|
|
1fe3787186 | ||
|
|
c8a952616f | ||
|
|
efcbecc218 | ||
|
|
487bafa5cf | ||
|
|
93aed33e2a | ||
|
|
a7765cb635 | ||
|
|
e6a5be6c66 | ||
|
|
5443532266 | ||
|
|
990aca2e1a | ||
|
|
49297adf97 | ||
|
|
b4d26fd35a | ||
|
|
b31f580760 | ||
|
|
7dc33f1956 | ||
|
|
e3470dd68b | ||
|
|
410c653654 | ||
|
|
bef3cf4c93 | ||
|
|
d8961d2acb | ||
|
|
17873ef7aa | ||
|
|
1d6e7eabc1 | ||
|
|
ba956abb1c | ||
|
|
7a2c2df89e | ||
|
|
bd44e659a8 | ||
|
|
8a1a27f5a1 | ||
|
|
7825071f61 | ||
|
|
7047bcbb1e | ||
|
|
193ef74e63 | ||
|
|
8a193d4dcd | ||
|
|
35cc662b26 | ||
|
|
9173bcc742 | ||
|
|
267f4273ee | ||
|
|
f1e675d662 | ||
|
|
c228534218 | ||
|
|
181fa5f32a | ||
|
|
7d5c37ec33 | ||
|
|
2eb9736d23 | ||
|
|
9f89ad2a08 | ||
|
|
490db3a026 | ||
|
|
2428f073bc | ||
|
|
e1231585ce | ||
|
|
839adaf559 | ||
|
|
839e004c08 | ||
|
|
ecf98828ff | ||
|
|
4dbc3c2353 | ||
|
|
c953a337fe | ||
|
|
6f026e6043 | ||
|
|
a775e29aef | ||
|
|
cb7b37fca4 | ||
|
|
cc79a67a9f | ||
|
|
72e7e3a3c4 | ||
|
|
4ffbeebd85 | ||
|
|
2e4b4e4736 | ||
|
|
a0acb0b658 | ||
|
|
feeaea4d64 | ||
|
|
0b7eb96e48 | ||
|
|
f0c4b628bf | ||
|
|
c66fc102ee | ||
|
|
9947a23343 | ||
|
|
abc204b313 | ||
|
|
425a0812bd | ||
|
|
030d466b11 | ||
|
|
27e9cd04a9 | ||
|
|
e42aea9444 | ||
|
|
3e1ee720f1 | ||
|
|
cb9f12482e | ||
|
|
bcf273a435 | ||
|
|
6230ada2cc | ||
|
|
db4de65683 | ||
|
|
931f7f07ca | ||
|
|
0f764c9400 | ||
|
|
89c98ab257 | ||
|
|
a49255c471 | ||
|
|
39d5a4908f | ||
|
|
fbaeaddff7 | ||
|
|
bc97db982e | ||
|
|
9a2f260bc5 | ||
|
|
954f1e3c7f | ||
|
|
0626f8d678 | ||
|
|
aa8c9676fe | ||
|
|
bb2ab768a8 | ||
|
|
8192c68f38 | ||
|
|
454561c2a1 | ||
|
|
deabcc9605 | ||
|
|
c3bdb9dd16 | ||
|
|
86d77c908f | ||
|
|
c5cbba5971 | ||
|
|
aea5493180 | ||
|
|
431832e50e | ||
|
|
daf239c93c | ||
|
|
2c669deae5 | ||
|
|
81969de76e | ||
|
|
5a29e7a83e | ||
|
|
9ed5aef40f | ||
|
|
9e025b4329 | ||
|
|
543da4fe24 | ||
|
|
e82e50a57a | ||
|
|
288d62d666 | ||
|
|
8ed224eeab | ||
|
|
22bafff943 | ||
|
|
02403cf566 | ||
|
|
eabbc6f037 | ||
|
|
27e60edaf4 | ||
|
|
f7d0b64dea | ||
|
|
b874484101 | ||
|
|
0ee72db2e9 | ||
|
|
4f07bac9ec | ||
|
|
80abaf70f2 | ||
|
|
d6acbc2fb1 | ||
|
|
caf93c019a | ||
|
|
7044efa7c3 | ||
|
|
5b4d8f971f | ||
|
|
75e8feb9b8 | ||
|
|
5d59419a06 | ||
|
|
ff2e47bf70 | ||
|
|
4721ed7c15 | ||
|
|
d9da328088 | ||
|
|
2ee506ac06 | ||
|
|
16c4e4dc5b | ||
|
|
28b8fb1b97 | ||
|
|
c8bae9fba6 | ||
|
|
920764ad33 | ||
|
|
a45d114527 | ||
|
|
62cca09045 | ||
|
|
ea126b90e2 | ||
|
|
777a20182e | ||
|
|
df62a9d32b | ||
|
|
47e211bf3c | ||
|
|
650ca3844e | ||
|
|
8de49fa027 | ||
|
|
35b7633f4d | ||
|
|
2c96a80280 | ||
|
|
c7000c8ac6 | ||
|
|
1dca2b4387 | ||
|
|
ccbe9ad21e | ||
|
|
1fe6f11ef4 | ||
|
|
9d4ab60fc4 | ||
|
|
ea8c5ad6e1 | ||
|
|
63a0f5f91c | ||
|
|
e8bacbdb6f | ||
|
|
bcc8226ccc | ||
|
|
ddec91a0c5 | ||
|
|
2c262bb29d | ||
|
|
ae0afb7b53 | ||
|
|
84e7fe5b60 | ||
|
|
ccb32b045a | ||
|
|
b2e29ac4bd | ||
|
|
7df6309a68 | ||
|
|
a203ad9f64 | ||
|
|
44d6e09337 | ||
|
|
7c59ec019a | ||
|
|
b457f64ec8 | ||
|
|
e152e59a61 | ||
|
|
2c620ad69a | ||
|
|
c08b405fc2 | ||
|
|
585e39c591 | ||
|
|
60e857dba9 | ||
|
|
9a4aa2b08f | ||
|
|
2900b08b70 | ||
|
|
b0936563a2 | ||
|
|
3d327b245a | ||
|
|
9a14de3448 | ||
|
|
59f83545cf | ||
|
|
d9da1ef085 | ||
|
|
279a27f740 | ||
|
|
6ba822ee48 | ||
|
|
e4445949c8 | ||
|
|
8178ff583d | ||
|
|
0e0e19a802 | ||
|
|
1e2e7d841f | ||
|
|
263c2f00eb | ||
|
|
39c0b57652 | ||
|
|
677baf549c | ||
|
|
848478c28b | ||
|
|
98ad33a589 | ||
|
|
3f33733f40 | ||
|
|
cdc4ae4fb3 | ||
|
|
3edfa8a6ce | ||
|
|
6e99b667af | ||
|
|
23c8735568 | ||
|
|
f39065044f | ||
|
|
7d2ae47b0f | ||
|
|
7247de6908 | ||
|
|
4c2aef8504 | ||
|
|
ac8717cf1f | ||
|
|
7761928064 | ||
|
|
198047406b | ||
|
|
1d57309db9 | ||
|
|
07af9f36b2 | ||
|
|
d157fea9be | ||
|
|
7692caf622 | ||
|
|
dc65a8823f | ||
|
|
2210932fdf | ||
|
|
0cb1bf4b7f | ||
|
|
44d175dd40 | ||
|
|
097feb6437 | ||
|
|
250c7e5f20 | ||
|
|
83740f77bb | ||
|
|
8267e13d22 | ||
|
|
a7e9396f35 | ||
|
|
0f6c8601d2 | ||
|
|
66e68092d5 | ||
|
|
09616a594c | ||
|
|
7ae58ea7ea | ||
|
|
9f1f85a3c4 | ||
|
|
9992cdfce0 | ||
|
|
f5fa08ce94 | ||
|
|
67f70f7f85 | ||
|
|
840bd56a3d | ||
|
|
c1e2d31cfd | ||
|
|
c35322d44e | ||
|
|
9867e46c3f | ||
|
|
946a120038 | ||
|
|
2cd1a17aa2 | ||
|
|
6e29fe2932 | ||
|
|
e6c6bf6613 | ||
|
|
72c53169df | ||
|
|
b78cce7b4f | ||
|
|
c40499ec31 | ||
|
|
ee158e517e | ||
|
|
70aebcf9aa | ||
|
|
b15870d441 | ||
|
|
fe7074736a | ||
|
|
69c523ffad | ||
|
|
6aeefdf43d | ||
|
|
5f4c8be3d3 | ||
|
|
3092e4c557 | ||
|
|
ea119068da | ||
|
|
14371ecf94 | ||
|
|
45b0fcfe15 | ||
|
|
00aa5f5586 | ||
|
|
79fd53fd4c | ||
|
|
357ee3daf0 | ||
|
|
c2f7897f10 | ||
|
|
75455c0c48 | ||
|
|
a394bb9f8e | ||
|
|
2f5a846493 | ||
|
|
90376b361d | ||
|
|
229cf6bf5f | ||
|
|
bc46737353 | ||
|
|
1db2243a2e | ||
|
|
7cf836b3cb | ||
|
|
f7bbd295d6 | ||
|
|
62dbd95b48 | ||
|
|
df07e9c719 | ||
|
|
9aaf72726e | ||
|
|
5289927619 | ||
|
|
fa8c686f75 | ||
|
|
df5f28b7c4 | ||
|
|
280d8368fa | ||
|
|
80dbff1f21 | ||
|
|
7ee68a8481 | ||
|
|
ac8dd42c45 | ||
|
|
eed2148b2a | ||
|
|
dc5345b6d3 | ||
|
|
221af0b5bb | ||
|
|
a10ccc1eb0 | ||
|
|
b72d858480 | ||
|
|
60412cc90b | ||
|
|
512ac87dc9 | ||
|
|
d97020d1c5 | ||
|
|
f82cb617ba | ||
|
|
f79281a1a0 | ||
|
|
7be1dbb78b | ||
|
|
f875787799 | ||
|
|
f82c208556 | ||
|
|
f2150e3d85 | ||
|
|
ecc198e8a0 | ||
|
|
949bc58a80 | ||
|
|
7d79fff16f | ||
|
|
3aeb678292 | ||
|
|
160ac41bed | ||
|
|
9dfbcbe89c | ||
|
|
30da529348 | ||
|
|
dd8d114711 | ||
|
|
2191a4a848 | ||
|
|
8b9ea8d988 | ||
|
|
46dda8567d | ||
|
|
6953da4d9a | ||
|
|
e987d6647e | ||
|
|
359d85727e | ||
|
|
a994bf9dd8 | ||
|
|
14c4e095f6 | ||
|
|
59dce0e56f | ||
|
|
1f54a893a7 | ||
|
|
9489f1ee3d | ||
|
|
dc3d720e8d | ||
|
|
efe30b598b | ||
|
|
42515bfb2d | ||
|
|
acb3657d95 | ||
|
|
e7159c9d36 | ||
|
|
f3fdca368b | ||
|
|
4ea3e08a45 | ||
|
|
1eebc72b21 | ||
|
|
9a91be7e36 | ||
|
|
48476f9b88 | ||
|
|
68c991eb9b | ||
|
|
b51c77b01b | ||
|
|
57105db554 | ||
|
|
4bd3bdaddf | ||
|
|
9cf59b8d73 | ||
|
|
a793b0bb42 | ||
|
|
4d9e2e1471 | ||
|
|
1719887e55 | ||
|
|
2be00aca9d | ||
|
|
6fd05c5ad7 | ||
|
|
65e404374f | ||
|
|
0b78731bb3 | ||
|
|
65d318ed88 | ||
|
|
1bfcea55a9 | ||
|
|
6780eb004d | ||
|
|
0e12b2d021 | ||
|
|
1b356f87ec | ||
|
|
bf24d0bae1 | ||
|
|
97c831d4bb | ||
|
|
e3e48ffa6d | ||
|
|
b678416122 | ||
|
|
df722925fa | ||
|
|
4cbc0d9806 | ||
|
|
b0f3711b4e | ||
|
|
14ec6579b2 | ||
|
|
30bf039473 | ||
|
|
33bea317b0 | ||
|
|
c6814dc05e | ||
|
|
16808069ec | ||
|
|
e813974e29 | ||
|
|
7e0010b536 | ||
|
|
93171adcb3 | ||
|
|
9501cc76a4 | ||
|
|
5d7db046ac | ||
|
|
46c259bc3e | ||
|
|
3bacff91d3 | ||
|
|
bd79d483d2 | ||
|
|
16e31f4881 | ||
|
|
afa23c393d | ||
|
|
54d23cb781 | ||
|
|
c757e410e9 | ||
|
|
39dd25567d | ||
|
|
32cd998c2a | ||
|
|
691fc6335e | ||
|
|
b0025d1416 | ||
|
|
2467d8b0e7 | ||
|
|
28993c53e7 | ||
|
|
efdea870f0 | ||
|
|
b2995ec862 | ||
|
|
2bcc84dbb2 | ||
|
|
70cc98ce33 | ||
|
|
6e055f398d | ||
|
|
9f6234f032 | ||
|
|
6135544b72 | ||
|
|
5f32ec218b | ||
|
|
3b9a884db2 | ||
|
|
ada6b85868 | ||
|
|
2f8a4f447c | ||
|
|
9bd6499271 | ||
|
|
76fcc919ef | ||
|
|
a382297edf | ||
|
|
4987dfe4f6 | ||
|
|
d1a2e50b8d | ||
|
|
b9e44b6166 | ||
|
|
af601edc94 | ||
|
|
6640dcf9cd | ||
|
|
9b2734ed38 | ||
|
|
afcfce056f | ||
|
|
8f3af2f27b | ||
|
|
706aae47b3 | ||
|
|
d494295b21 | ||
|
|
c1600a253b | ||
|
|
a3bc29ad8f | ||
|
|
83225ed157 | ||
|
|
f13f6dc01f | ||
|
|
2d2489443a | ||
|
|
0e5b7fbfa2 | ||
|
|
d16a8068f7 | ||
|
|
c5ef11febc | ||
|
|
027f31447a | ||
|
|
41d1a4e5fb | ||
|
|
924e3191cb | ||
|
|
ebdc6b8fd9 | ||
|
|
5c05128cd7 | ||
|
|
5bf5685a12 | ||
|
|
f7391cb4c4 | ||
|
|
bad7bd8884 | ||
|
|
e0524a1656 | ||
|
|
f59af9baa3 | ||
|
|
b36890ca82 | ||
|
|
22703af08b | ||
|
|
c4108269b3 | ||
|
|
94c9d090cf | ||
|
|
28b9235a43 | ||
|
|
3aebae5e15 | ||
|
|
2631cb75d6 | ||
|
|
07b8b4156f | ||
|
|
7978967c1a | ||
|
|
3f24ff4de3 | ||
|
|
7253dd82a6 | ||
|
|
2b8eb3ae7e | ||
|
|
429eae71cd | ||
|
|
e5c552defb | ||
|
|
5c950a2e2c | ||
|
|
577ff78189 | ||
|
|
3f3cde05f7 | ||
|
|
b48f2c3276 | ||
|
|
8e818846f0 | ||
|
|
1554e37f8c | ||
|
|
affaabd011 | ||
|
|
f34a8f991c | ||
|
|
c2b14d610b | ||
|
|
02693d0cbb | ||
|
|
23155279ab | ||
|
|
2d23f7403d | ||
|
|
ae411c6fd5 | ||
|
|
ab8d6075a9 | ||
|
|
bc5ae29a67 | ||
|
|
1a8aabc30c | ||
|
|
8c51d7f713 | ||
|
|
8a1485e7ce | ||
|
|
614145431a | ||
|
|
db25f1999f | ||
|
|
4ed231b9bb | ||
|
|
25a5342c11 | ||
|
|
c7202e3ca9 | ||
|
|
89c2e94cea | ||
|
|
3dc46771b5 | ||
|
|
0eac4d4d7f | ||
|
|
a0ceb788db | ||
|
|
98fb36d03a | ||
|
|
a670006517 | ||
|
|
9cdbe67cd4 | ||
|
|
bbe8af452c | ||
|
|
f5edf28ce1 | ||
|
|
8fc30f590b | ||
|
|
e578f23ebe | ||
|
|
99c488fc9e | ||
|
|
f6a710660d | ||
|
|
a61744bb65 | ||
|
|
17c3078c24 | ||
|
|
5fa7731b56 | ||
|
|
c8e2be4d8c | ||
|
|
e3db613a07 | ||
|
|
0f7839027f | ||
|
|
31b322a108 | ||
|
|
b3c0494618 | ||
|
|
78ddb0533d | ||
|
|
da2158e7f2 | ||
|
|
d2a1efb6e7 | ||
|
|
98a880db2d | ||
|
|
cb82ef8703 | ||
|
|
d56246767b | ||
|
|
bb477984aa | ||
|
|
5a3e650e02 | ||
|
|
3c96dd2fac | ||
|
|
da051e3ff3 | ||
|
|
d15b6323c2 | ||
|
|
ec5f8fe4a4 | ||
|
|
71d84d76f8 | ||
|
|
d33ed52ec2 | ||
|
|
3a970544bb | ||
|
|
792ac3a2e8 | ||
|
|
60bbc27401 | ||
|
|
cf7cbcb6e6 | ||
|
|
c126a8eba9 | ||
|
|
66a60d0357 | ||
|
|
1acdadd027 | ||
|
|
200be9dadd | ||
|
|
a73e2872a4 | ||
|
|
ce6f7729c5 | ||
|
|
c285411371 | ||
|
|
46394c600e | ||
|
|
bea9cb3248 | ||
|
|
1087dcd714 | ||
|
|
0b63029b7e | ||
|
|
9679d24414 | ||
|
|
7947fd53e5 | ||
|
|
8dedf75565 | ||
|
|
b5b7c12b49 | ||
|
|
51f4e3cc3a | ||
|
|
24b0315d2e | ||
|
|
be446220eb | ||
|
|
a3ef2d332e | ||
|
|
ba6fe576e3 | ||
|
|
abcef38102 | ||
|
|
d5780b2f30 | ||
|
|
f7e498a0a2 | ||
|
|
51ac7ca2de | ||
|
|
c94535f6b5 | ||
|
|
07457ae368 | ||
|
|
4767fff08c | ||
|
|
f0c3498ecc | ||
|
|
1eca52d0fe | ||
|
|
7fbac9ad2f | ||
|
|
6fb80ed50b | ||
|
|
47f63ac81b | ||
|
|
42cc0b28ba | ||
|
|
993806f781 | ||
|
|
8a5af33aaa | ||
|
|
a974e36e9e | ||
|
|
17bcb2b39e | ||
|
|
cddf02d0c1 | ||
|
|
75ff7ece37 | ||
|
|
ec2b407a20 | ||
|
|
1dc9d78e54 | ||
|
|
5742a75c9d | ||
|
|
b5e9ad6d7e | ||
|
|
6393025219 | ||
|
|
9ab3e289bc | ||
|
|
6454474886 | ||
|
|
c5720a7a03 | ||
|
|
41b15adc6d | ||
|
|
05b962e718 | ||
|
|
1f01ca7b85 | ||
|
|
5d3b4fa5ec | ||
|
|
754d2b2dd3 | ||
|
|
ffad62e3dc | ||
|
|
1c11e16565 | ||
|
|
edc12990b4 | ||
|
|
12ea234d18 | ||
|
|
0461206a61 | ||
|
|
663f9e3962 | ||
|
|
34ee948c8e | ||
|
|
1bb9c2e4fe | ||
|
|
8ab18ce5cc | ||
|
|
71be16826e | ||
|
|
926c09d9df | ||
|
|
66c065ae7f | ||
|
|
083ed7775c | ||
|
|
5185452495 | ||
|
|
fa15f226ab | ||
|
|
fea4da2a33 | ||
|
|
055c933f4b | ||
|
|
9bcb867748 | ||
|
|
26e3c03f5f | ||
|
|
c195c3b2d1 | ||
|
|
f9e0aacfeb | ||
|
|
37fef66647 | ||
|
|
9f99b67563 | ||
|
|
fe902648ad | ||
|
|
13b933cd0b | ||
|
|
1d3b1d1d80 | ||
|
|
67a612af3a | ||
|
|
a891683806 | ||
|
|
440a72fc42 | ||
|
|
696d2e5197 | ||
|
|
2b17d56fc7 | ||
|
|
a410ef5d9f | ||
|
|
fe94769541 | ||
|
|
c63ae9c00c | ||
|
|
d5ece8d007 | ||
|
|
692a971dc0 | ||
|
|
05b8370cc0 | ||
|
|
b6111b35a2 | ||
|
|
4d72687628 | ||
|
|
8f125983ce | ||
|
|
4279825caa | ||
|
|
77ae3a4623 | ||
|
|
4c222dbc54 | ||
|
|
4e0f93ee8a | ||
|
|
e99f3e6627 | ||
|
|
f73877c34a | ||
|
|
abd3f12cae | ||
|
|
00117f5b7b | ||
|
|
d7d728f93e | ||
|
|
dc9217c4ec | ||
|
|
95acb13b93 | ||
|
|
234cc00d9f | ||
|
|
d6ba164799 | ||
|
|
910aa03dc8 | ||
|
|
a3e4a4c873 | ||
|
|
2d7c843447 | ||
|
|
e342b45473 | ||
|
|
f354bccd58 | ||
|
|
98073134db | ||
|
|
360666b00b | ||
|
|
ce4c807870 | ||
|
|
f2783bdac8 | ||
|
|
875ed16c3b | ||
|
|
383ba56d1f | ||
|
|
45eb54e624 | ||
|
|
5aff4e2ed6 | ||
|
|
e73e47dd94 | ||
|
|
1c8ac5efbc | ||
|
|
90fa5e1ecd | ||
|
|
348994917b | ||
|
|
60dbea1027 | ||
|
|
dae19bbccf | ||
|
|
c81f83887e | ||
|
|
04e555dde9 | ||
|
|
be94518e31 | ||
|
|
66bec1e08c | ||
|
|
f61ce10716 | ||
|
|
b1b92b2995 | ||
|
|
bd9f2c4757 | ||
|
|
a3ead2153e | ||
|
|
e12f008b92 | ||
|
|
d064ece0ff | ||
|
|
379fbf68b1 | ||
|
|
83783c1a88 | ||
|
|
c7e46205b3 | ||
|
|
0c61f0ded2 | ||
|
|
49e2ec0498 | ||
|
|
fb0a74c101 | ||
|
|
245a7ddfe9 | ||
|
|
ca8874c2e1 | ||
|
|
dbcd7c8e03 | ||
|
|
9cce63659e | ||
|
|
be0bbab0c8 | ||
|
|
7b6d3698c4 | ||
|
|
56daca8b4f | ||
|
|
ed382d102e | ||
|
|
745de2502e | ||
|
|
503a4b1374 | ||
|
|
0b71b2d659 | ||
|
|
2ad244df94 | ||
|
|
d0da4f03a6 | ||
|
|
ab0cd4152a | ||
|
|
93d04bfe60 | ||
|
|
b1ccb40bd3 | ||
|
|
d177001ea8 | ||
|
|
2f921897c7 | ||
|
|
1a0f7146ce | ||
|
|
dff2386594 | ||
|
|
55cc782cc6 | ||
|
|
6903099873 | ||
|
|
6e2d84be33 | ||
|
|
9e542d0bbe | ||
|
|
ade9af9ecd | ||
|
|
59f11a1b26 | ||
|
|
cc1d6e2b47 | ||
|
|
2655b1b3d1 | ||
|
|
053dd28f8c | ||
|
|
65e4cf83d8 | ||
|
|
419099318c | ||
|
|
972edd3a30 | ||
|
|
cc4125e766 | ||
|
|
da40cc9830 | ||
|
|
29d7e2dcfe | ||
|
|
3d906fd582 | ||
|
|
0cad43c18b | ||
|
|
c66b686a63 | ||
|
|
e2a1e3f327 | ||
|
|
2b5cf75a53 | ||
|
|
1c350ac87b | ||
|
|
ef6539909c | ||
|
|
47d7c6fe65 | ||
|
|
c94c5fbc95 | ||
|
|
63a833d114 | ||
|
|
72162128e2 | ||
|
|
a8f712c335 | ||
|
|
ea8888a685 | ||
|
|
2aecf69b67 | ||
|
|
1a4c24dd86 | ||
|
|
6cf2b45051 | ||
|
|
38dd2bdf6e | ||
|
|
8784f1da70 | ||
|
|
b9208ea94e | ||
|
|
dfb648b480 | ||
|
|
f7af4f06ea | ||
|
|
d98f9eb62c | ||
|
|
fef1335f51 | ||
|
|
59f14cbdd4 | ||
|
|
e8645c543f | ||
|
|
b92f6177e3 | ||
|
|
2888829a4f | ||
|
|
3d392ead19 | ||
|
|
a79b2bfa79 | ||
|
|
15e1e2f02e | ||
|
|
9e652803d0 | ||
|
|
7795fceb72 | ||
|
|
9f01f26ea6 | ||
|
|
abbc9a1d7a | ||
|
|
2c15befb8e | ||
|
|
ba8ea84c4e | ||
|
|
97912046da | ||
|
|
43e99c23e3 | ||
|
|
ffe14d75fc | ||
|
|
872ef66641 | ||
|
|
6363862ec2 | ||
|
|
775a112e83 | ||
|
|
7168904290 | ||
|
|
4213209b08 | ||
|
|
5dce91b7f6 | ||
|
|
6d7fb9f87c | ||
|
|
cc6c9dd8d1 | ||
|
|
21ebec52f3 | ||
|
|
11023ab225 | ||
|
|
fe97b15905 | ||
|
|
de8738aa03 | ||
|
|
568bbf9126 | ||
|
|
a2481652da | ||
|
|
33e3d3272d | ||
|
|
b909651c29 | ||
|
|
16794c5252 | ||
|
|
49ad270d88 | ||
|
|
df9b6cb7e8 | ||
|
|
c6c4551928 | ||
|
|
de84353eb0 | ||
|
|
6f05b80f34 | ||
|
|
797dc706e2 | ||
|
|
bdb615bcf9 | ||
|
|
1b98717b0e | ||
|
|
7bd1aedada | ||
|
|
34bf8f9d1f | ||
|
|
b07f70e9fe | ||
|
|
e635788955 | ||
|
|
bdb1cef3e5 | ||
|
|
e5a32e65c0 | ||
|
|
5fc77922b4 | ||
|
|
ce8add2895 | ||
|
|
88e1e5b770 | ||
|
|
76ecbd3497 | ||
|
|
e33c9b932f | ||
|
|
038f6caa04 | ||
|
|
36f5249d71 | ||
|
|
570702a5bd | ||
|
|
5d03c9c644 | ||
|
|
83906def4a | ||
|
|
33e0f25fb1 | ||
|
|
2080de4139 | ||
|
|
decaca889c | ||
|
|
794a39032f | ||
|
|
cc3cf17060 | ||
|
|
b07da4bfa8 | ||
|
|
4f52a4ee79 | ||
|
|
cf77b999a3 | ||
|
|
c9f062bdd9 | ||
|
|
12669f7ea0 | ||
|
|
03451d2a6e | ||
|
|
b5d7595f87 | ||
|
|
0d3d760a43 | ||
|
|
1cfc86e5ce | ||
|
|
938343323d | ||
|
|
37112715c0 | ||
|
|
bc80c10193 | ||
|
|
d989f48226 | ||
|
|
45bcbb3a3d | ||
|
|
f4a74e0254 | ||
|
|
ace83cf68d | ||
|
|
209b9b9a6f | ||
|
|
0d9dd6a33e | ||
|
|
e4137a031e | ||
|
|
41e9ea7e4f | ||
|
|
1a8b88973d | ||
|
|
3109e251b9 | ||
|
|
452ab280f1 | ||
|
|
34ecfbb846 | ||
|
|
11fb97d275 | ||
|
|
4bb30d075b | ||
|
|
f38af55c05 | ||
|
|
8a007376cc | ||
|
|
940a1dfc78 | ||
|
|
cbd64d1602 | ||
|
|
955c9992d0 | ||
|
|
87606818cd | ||
|
|
ffcd264bac | ||
|
|
04c70cf99d | ||
|
|
0e0b6fca07 | ||
|
|
b24a789352 | ||
|
|
3d4f447037 | ||
|
|
7aa52ca00a | ||
|
|
a94c9050c5 | ||
|
|
9b18e71e12 | ||
|
|
d5ff048bd0 | ||
|
|
76f44f2573 | ||
|
|
9988f1a76f | ||
|
|
1559376bc2 | ||
|
|
4d0f252acd | ||
|
|
d5a84e66ad | ||
|
|
1dfe3453f4 | ||
|
|
a7dc9f31b9 | ||
|
|
d275f18b3c | ||
|
|
d770429401 | ||
|
|
501f0be99f | ||
|
|
a70122f873 | ||
|
|
1e1bd15a06 | ||
|
|
164fb1f4f5 | ||
|
|
0206514bdb | ||
|
|
ecf2b42fa4 | ||
|
|
df683aeea9 | ||
|
|
cdd05958f7 | ||
|
|
4ec5fc05fb | ||
|
|
4019cafac1 | ||
|
|
4333e179d1 | ||
|
|
87d67be428 | ||
|
|
350863adff | ||
|
|
d125fc46a0 | ||
|
|
5f4c079071 | ||
|
|
5ec576a76e | ||
|
|
9edb00ebbb | ||
|
|
59843e5464 | ||
|
|
4fe0ccf807 | ||
|
|
2ee012d3fe | ||
|
|
161b4cb5f5 | ||
|
|
f9bee68d71 | ||
|
|
e0d98aca1b | ||
|
|
d47518c00c | ||
|
|
58f97dc3ab | ||
|
|
c7706cb80c | ||
|
|
49bcc877ef | ||
|
|
4b81dd552e | ||
|
|
ff63aaf3f3 | ||
|
|
39cbd1477b | ||
|
|
3741cc558a | ||
|
|
2a314ca3c1 | ||
|
|
10f9564825 | ||
|
|
1ae87c9b18 | ||
|
|
24301ba462 | ||
|
|
ff6481274f | ||
|
|
866df585a2 | ||
|
|
416bec04a1 | ||
|
|
f747d6725e | ||
|
|
8846918e55 | ||
|
|
e347aefcd9 | ||
|
|
0a0676af3a | ||
|
|
7a69b63b4f | ||
|
|
fab5626741 | ||
|
|
c6832d6478 | ||
|
|
f451597746 | ||
|
|
a92e5b5156 | ||
|
|
786b3b26ea | ||
|
|
2e9fd2fd79 | ||
|
|
3be2b9893b | ||
|
|
7f6bed4f5f | ||
|
|
37322f284a | ||
|
|
4106bb1792 | ||
|
|
229db1242a | ||
|
|
8201b42135 | ||
|
|
24818575dc | ||
|
|
213050e7f2 | ||
|
|
1efaf4e3ea | ||
|
|
5532147992 | ||
|
|
550b43094d | ||
|
|
5e831837c8 | ||
|
|
480e88a088 | ||
|
|
81cbcbe8af | ||
|
|
5649634809 | ||
|
|
69a253f738 | ||
|
|
fbc8cfddb8 | ||
|
|
3e1024804c | ||
|
|
cf0b51be00 | ||
|
|
5b295a2a8f | ||
|
|
ebb3d7a149 | ||
|
|
73ab348d11 | ||
|
|
8ce17086f8 | ||
|
|
45cffc93b1 | ||
|
|
71cf4d5a34 | ||
|
|
b79a4154af | ||
|
|
ff818f8472 | ||
|
|
b540d32ca3 | ||
|
|
895a11d45f | ||
|
|
bdafae6132 | ||
|
|
f45d9ce6da | ||
|
|
7456e2c8f5 | ||
|
|
fc51b50fe6 | ||
|
|
b31aa26975 | ||
|
|
49801e1b14 | ||
|
|
1453464cbd | ||
|
|
b9be8ff13d | ||
|
|
9663c3cadd | ||
|
|
da5490bc0a | ||
|
|
df9b1b9fbb | ||
|
|
6a0a3ded13 | ||
|
|
ac7f4a08c3 | ||
|
|
e5ff5e3364 | ||
|
|
f0ac19fcc1 | ||
|
|
ce23d34923 | ||
|
|
bdbba87f5a | ||
|
|
fa8ef0477b | ||
|
|
3e9ec4cfb6 | ||
|
|
ad1c9d8129 | ||
|
|
7aaac4c13c | ||
|
|
a6c71c3d54 | ||
|
|
05714f38dc | ||
|
|
4ef5e4d8ae | ||
|
|
5fd0e1d6fe | ||
|
|
97faf5bef3 | ||
|
|
bd77128733 | ||
|
|
935debc6bf | ||
|
|
699ed19b6a | ||
|
|
126e5a94c2 | ||
|
|
792bd36ea7 | ||
|
|
04dfee1b43 | ||
|
|
6a08cfeea9 | ||
|
|
29a0f6c9f6 | ||
|
|
e2bdf26d82 | ||
|
|
1643949110 | ||
|
|
577dabd727 | ||
|
|
ed4bd0693f | ||
|
|
0f2982b34d | ||
|
|
0954094800 | ||
|
|
22dcfafc62 | ||
|
|
dd34051426 | ||
|
|
6feaf2cb8a | ||
|
|
603f64ea92 | ||
|
|
568f9ab0d0 | ||
|
|
a2ff1c33e6 | ||
|
|
84452e9fc0 | ||
|
|
5a6ae453cf | ||
|
|
cde3e3361d | ||
|
|
25f01192a4 | ||
|
|
bacb08ec65 | ||
|
|
36c905ee1c | ||
|
|
6b9280534a | ||
|
|
f94c6e850f | ||
|
|
f4cecf6906 | ||
|
|
a81d3766f8 | ||
|
|
7182c2e66d | ||
|
|
503316bc70 | ||
|
|
d5af59f2c7 | ||
|
|
cb3ac1ad3a | ||
|
|
e161080e4c | ||
|
|
ceab7f917b | ||
|
|
41d8066c4c | ||
|
|
e373cbd776 | ||
|
|
05ea6b6b10 | ||
|
|
28eed3ae71 | ||
|
|
535c67ac9b | ||
|
|
68027a6e15 | ||
|
|
12dea6b499 | ||
|
|
d8bd078a02 | ||
|
|
439bc109b0 | ||
|
|
b1ec93ceb5 | ||
|
|
44b9aa0e48 | ||
|
|
0a59063027 | ||
|
|
5db4608abd | ||
|
|
6ece2aa6cb | ||
|
|
95ee45f666 | ||
|
|
556e90b8d8 | ||
|
|
42841e6247 | ||
|
|
324c82248a | ||
|
|
94f5a47918 | ||
|
|
3f70990956 | ||
|
|
d77635e572 | ||
|
|
0f26f1b751 | ||
|
|
456bc22138 | ||
|
|
b6fe91e396 | ||
|
|
0fb4d26949 | ||
|
|
3b3583a416 | ||
|
|
cac1d576c8 | ||
|
|
9cba0d0a48 | ||
|
|
3bf11e9dc0 | ||
|
|
edbb160ac6 | ||
|
|
ee111dc63c | ||
|
|
b06edb756a | ||
|
|
fd745494e0 | ||
|
|
702bf3f479 | ||
|
|
6adc02a91f | ||
|
|
55bae5a130 | ||
|
|
1aae817b17 | ||
|
|
8afc8c23fb | ||
|
|
36e473b139 | ||
|
|
e529723f86 | ||
|
|
6c5112c142 | ||
|
|
77ac68a603 | ||
|
|
e816f40872 | ||
|
|
65313f114b | ||
|
|
ef1f1342f5 | ||
|
|
9205fe6c08 | ||
|
|
75df3e81fe | ||
|
|
47fffbadb5 | ||
|
|
78354473fa | ||
|
|
8d7efb44b5 | ||
|
|
cab00b3d8c | ||
|
|
8efab01336 | ||
|
|
66feb8beb4 | ||
|
|
ca4cccffeb | ||
|
|
51b1760c50 | ||
|
|
42e1bda365 | ||
|
|
194b6b557a | ||
|
|
e7bc439997 | ||
|
|
ef76cce0ac | ||
|
|
63eec6d969 | ||
|
|
719ae74c06 | ||
|
|
37cf424eb8 | ||
|
|
3a87f7ba9d | ||
|
|
275428d825 | ||
|
|
7b9dac86ca | ||
|
|
46496ee2cc | ||
|
|
110cc402cc | ||
|
|
587f006259 | ||
|
|
ec45c0df81 | ||
|
|
dc575aeca4 | ||
|
|
a3f790f000 | ||
|
|
4e9e188d02 | ||
|
|
abb17efae4 | ||
|
|
16be990502 | ||
|
|
d1a496f9a3 | ||
|
|
cd730fcfef | ||
|
|
1b65bf665b | ||
|
|
6f4735790c | ||
|
|
040666f89d | ||
|
|
227cb800c3 | ||
|
|
0052569a14 | ||
|
|
f7561c4888 | ||
|
|
92200f19e7 | ||
|
|
4c01f18a62 | ||
|
|
a6ce3e49fe | ||
|
|
1b3a5d1bf6 | ||
|
|
db7bdc63c8 | ||
|
|
7aca550f02 | ||
|
|
b60980b3fd | ||
|
|
b578c2c584 | ||
|
|
fc45bd624e | ||
|
|
17be3d9d2c | ||
|
|
5b3a38a7bc | ||
|
|
4403835d50 | ||
|
|
b7a3d3eb46 | ||
|
|
29846b22fe | ||
|
|
32d235e8c7 | ||
|
|
cb982b3513 | ||
|
|
d7ed6c26dd | ||
|
|
8ff19f7e68 | ||
|
|
729e062c3a | ||
|
|
7d0340ac07 | ||
|
|
01960e74c1 | ||
|
|
8e40250985 | ||
|
|
8ae2edb61a | ||
|
|
0baa7bcbf1 | ||
|
|
fffee48918 | ||
|
|
515abb6e14 | ||
|
|
6c1c3ff87f | ||
|
|
5b65575c7a | ||
|
|
0f258fc5f8 | ||
|
|
206bc661dc | ||
|
|
d0e35b109e | ||
|
|
61769c4f20 | ||
|
|
95778ee5f4 | ||
|
|
155030fdca | ||
|
|
98237ef76c | ||
|
|
f0a7b38199 | ||
|
|
9fc5e6751b | ||
|
|
4c1630312b | ||
|
|
d397c5c996 | ||
|
|
f6c41b5a60 | ||
|
|
06eb5c01c3 | ||
|
|
7df49f91e8 | ||
|
|
96dcbb0ce7 | ||
|
|
5f828fb986 | ||
|
|
533d663938 | ||
|
|
ae788503a9 | ||
|
|
cf0acd9c73 | ||
|
|
0857f2f1cf | ||
|
|
c05d412bdb | ||
|
|
c8e0ce717d | ||
|
|
cc3485b201 | ||
|
|
81ea7080c2 | ||
|
|
76ff6f5ae0 | ||
|
|
0c0d0b7a6f | ||
|
|
ec8cf1f6b7 | ||
|
|
da44310d1b | ||
|
|
4bd9c84bb0 | ||
|
|
3b1269a770 | ||
|
|
7c986ccee8 | ||
|
|
903bad8f36 | ||
|
|
4b9577437c | ||
|
|
c316011fbc | ||
|
|
3fb1f18c22 | ||
|
|
53935058f5 | ||
|
|
a3860c9581 | ||
|
|
dc20899d26 | ||
|
|
62ac3ddb75 | ||
|
|
b792a61bf9 | ||
|
|
aae9f9e1cb | ||
|
|
d098bf5e6a | ||
|
|
b0e8a3ecd9 | ||
|
|
4efa684022 | ||
|
|
f2c8082990 | ||
|
|
1abba80045 | ||
|
|
68564a2b75 | ||
|
|
385b701b38 | ||
|
|
11c9a1d707 | ||
|
|
c5aef6b561 | ||
|
|
dfcf73cfd0 | ||
|
|
7fc9389700 | ||
|
|
d1af7349bc | ||
|
|
0fd955197d | ||
|
|
cbf33507d1 | ||
|
|
bc60a5d97e | ||
|
|
57596b2991 | ||
|
|
43b3602a52 | ||
|
|
c09ec961b8 | ||
|
|
d140b453b2 | ||
|
|
9eb42636ec | ||
|
|
ee2d663fce | ||
|
|
8e83615a22 | ||
|
|
0ff129c5ca | ||
|
|
e49858439f | ||
|
|
3df07f7f47 | ||
|
|
ff9e179593 | ||
|
|
7539fee04b | ||
|
|
71a339a58f | ||
|
|
9ef2ea016b | ||
|
|
de4936a16d | ||
|
|
574d2b8904 | ||
|
|
1f3f7634e7 | ||
|
|
3c0725baff | ||
|
|
b0e1411012 | ||
|
|
39daf4714d | ||
|
|
4706afa823 | ||
|
|
25977d389d | ||
|
|
f760110569 | ||
|
|
133e78fe97 | ||
|
|
d92e0c8620 | ||
|
|
62fdb69d6b | ||
|
|
def57c9fb2 | ||
|
|
21c9c898c3 | ||
|
|
1f03c922c2 | ||
|
|
3f6ae6bdac | ||
|
|
60615ee1eb | ||
|
|
92b0d1bfa9 | ||
|
|
237988dc1f | ||
|
|
a846ec29ca | ||
|
|
4533e96bff | ||
|
|
0a401c3ac9 | ||
|
|
468abaf077 | ||
|
|
4ccf2f641c | ||
|
|
34eb2785cf | ||
|
|
09dbfe323e | ||
|
|
1f06c5b425 | ||
|
|
b98e089f7a | ||
|
|
a0ad06ed0a | ||
|
|
ec63365429 | ||
|
|
2cb85e4346 | ||
|
|
0d7c479c51 | ||
|
|
5a6c21e662 | ||
|
|
d6cadac98f | ||
|
|
dac2fc2c37 | ||
|
|
0fb45cef0d | ||
|
|
5ebdbd4003 | ||
|
|
b30f1023cb | ||
|
|
e5f65a4d1e | ||
|
|
ab42a65aa4 | ||
|
|
e351456bfe | ||
|
|
452e68b08f | ||
|
|
d65beed7a1 | ||
|
|
f5a5a0e8cb | ||
|
|
98380a0906 | ||
|
|
22fe7508f3 | ||
|
|
c8e241fc76 | ||
|
|
2c943e00d0 | ||
|
|
7ddb83b72d | ||
|
|
50bac01699 | ||
|
|
e0a92dfadd | ||
|
|
b50c951091 | ||
|
|
2f589a95a9 | ||
|
|
53eac86a95 | ||
|
|
9412f8955e | ||
|
|
71c98d82b1 | ||
|
|
42c1a925b4 | ||
|
|
1e84534ffd | ||
|
|
b087733e37 | ||
|
|
675efe29ac | ||
|
|
9833af8225 | ||
|
|
790c624571 | ||
|
|
67a70a8453 | ||
|
|
64bb05e2dd | ||
|
|
c111db6e73 | ||
|
|
24c5915bd3 | ||
|
|
04c3717618 | ||
|
|
78275a0984 | ||
|
|
bd908ed10d | ||
|
|
5473deec95 | ||
|
|
653258afd2 | ||
|
|
00a2210eea | ||
|
|
ec5688a013 | ||
|
|
fc0c7b5708 | ||
|
|
464f7ac486 | ||
|
|
fb2457146c | ||
|
|
d90870838e | ||
|
|
8c45f23291 | ||
|
|
315a3efa52 | ||
|
|
28abb5ae6f | ||
|
|
bb78d89682 | ||
|
|
fd36e19168 | ||
|
|
58db516e44 | ||
|
|
2b875e94dc | ||
|
|
0ba2447f55 | ||
|
|
8e684d0d3a | ||
|
|
ce3c1d4685 | ||
|
|
2cd77d47eb | ||
|
|
ac9366e351 | ||
|
|
af356586f8 | ||
|
|
7650db81a4 | ||
|
|
8925c86afd | ||
|
|
8f1836009e | ||
|
|
5fcf3f9b95 | ||
|
|
1c466d9e40 | ||
|
|
b97c2d9cbc | ||
|
|
3d970e4967 | ||
|
|
765c8f53dd | ||
|
|
4e81caeadf | ||
|
|
08b10e4d58 | ||
|
|
f8594f72e8 | ||
|
|
24ebee07cd | ||
|
|
a06ebb0991 | ||
|
|
cd5d4498e7 | ||
|
|
0bd62780c6 | ||
|
|
896f9327d6 | ||
|
|
cc83a99efe | ||
|
|
b6da20fef7 | ||
|
|
2912678559 | ||
|
|
409e870ef0 | ||
|
|
3d84074a0a | ||
|
|
a717fdfed4 | ||
|
|
be1f68015b | ||
|
|
2c29dcf1f6 | ||
|
|
2b1173177f | ||
|
|
3c0f7dc79c | ||
|
|
39d813bf3a | ||
|
|
5e2bc0d05b | ||
|
|
ec751159ae | ||
|
|
dda8b95f83 | ||
|
|
286012fe2a | ||
|
|
ab27299789 | ||
|
|
dfcdd5aa88 | ||
|
|
a05ea52689 | ||
|
|
de12b5de5b | ||
|
|
57b03eaca4 | ||
|
|
36bd00b760 | ||
|
|
4578a9974a | ||
|
|
d0371f58c6 | ||
|
|
9f80457351 | ||
|
|
56daf6f676 | ||
|
|
7f7b8d423b | ||
|
|
031afc80cb | ||
|
|
b77e28b04d | ||
|
|
2f8c3fdcfe | ||
|
|
f501a87099 | ||
|
|
d0ec5f26dd | ||
|
|
97776e9329 | ||
|
|
a19356c49e | ||
|
|
4f16918cf0 | ||
|
|
1af9761144 | ||
|
|
d74e814c79 | ||
|
|
16d09bca6c | ||
|
|
462c29b769 | ||
|
|
3939276d58 | ||
|
|
ff85f18c4c | ||
|
|
dd14fe4123 | ||
|
|
d804659ee2 | ||
|
|
e3152cf901 | ||
|
|
de236f321f | ||
|
|
66f4611866 | ||
|
|
6a6ef052af | ||
|
|
4e4606dabd | ||
|
|
0ce11103ab | ||
|
|
899d0e0557 | ||
|
|
4aefeff41f | ||
|
|
08d59e50e8 | ||
|
|
e6c06aba8c | ||
|
|
e5c2a04922 | ||
|
|
c134ccf8d9 | ||
|
|
4f959d1ff6 | ||
|
|
1c06d93951 | ||
|
|
b2dff29baa | ||
|
|
72633e9472 | ||
|
|
898a88f7d8 | ||
|
|
c2f09f6666 | ||
|
|
d45bcbc347 | ||
|
|
1f834567f8 | ||
|
|
0b62c04867 | ||
|
|
c71bb2a570 | ||
|
|
0a575b5bf8 | ||
|
|
6f2bf903c5 | ||
|
|
210ad4b8db | ||
|
|
b42cf0e204 | ||
|
|
2045d3aab8 | ||
|
|
e20cb9431d | ||
|
|
7161eaea8b | ||
|
|
b786da52f5 | ||
|
|
ccbfec838d | ||
|
|
f5c1872225 | ||
|
|
3001013bad | ||
|
|
1280803e5e | ||
|
|
f84fc07fe0 | ||
|
|
d83e53f589 | ||
|
|
52ba2c53f1 | ||
|
|
f266e1de4c | ||
|
|
5a0aafd3ce | ||
|
|
c7ce07a43c | ||
|
|
a73644c285 | ||
|
|
e28f1ffc99 | ||
|
|
afc691b2f4 | ||
|
|
00f6eb83c3 | ||
|
|
bbf51ebbec | ||
|
|
7816c8f16e | ||
|
|
fdbcba2412 | ||
|
|
50dc6cb0aa | ||
|
|
e413198d12 | ||
|
|
72592772f9 | ||
|
|
f918206bcd | ||
|
|
6b34d67da8 | ||
|
|
04fb093d4d | ||
|
|
7de0e0cc4a | ||
|
|
5cf7688e38 | ||
|
|
97cd06b52b | ||
|
|
6d522ead1d | ||
|
|
8e0fae62f3 | ||
|
|
af72098d60 | ||
|
|
62da582f4e | ||
|
|
f3efa6eddc | ||
|
|
c85fb7bd0a | ||
|
|
fc7b183461 | ||
|
|
a378810f88 | ||
|
|
2f5b322fad | ||
|
|
efb9b50f85 | ||
|
|
84fdef8eb6 | ||
|
|
76050a261b | ||
|
|
2667361450 | ||
|
|
a2b3313cc0 | ||
|
|
d343446235 | ||
|
|
cbce70f8a4 | ||
|
|
4573d6b6fb | ||
|
|
973305d13c | ||
|
|
4637016372 | ||
|
|
698ba4f7f1 | ||
|
|
502ebabf1f | ||
|
|
13f88626ca | ||
|
|
a39f58f7b5 | ||
|
|
aeb36468ce | ||
|
|
fdd196526d | ||
|
|
ecca892b16 | ||
|
|
03343d0301 | ||
|
|
a61c8b0cb6 | ||
|
|
a92129da00 | ||
|
|
0b783d6390 | ||
|
|
9aa1a2e30e | ||
|
|
15b3c69514 | ||
|
|
f2722e09ec | ||
|
|
b442859be0 | ||
|
|
1496a2efb1 | ||
|
|
a0edb111f0 | ||
|
|
6bcf54fe29 | ||
|
|
3d7e24816a | ||
|
|
f5e02ec63f | ||
|
|
ed1f4ebace | ||
|
|
eb0e496cfd | ||
|
|
8b9fc30a6d | ||
|
|
12c2a6e99c | ||
|
|
714433b62d | ||
|
|
e42933d786 | ||
|
|
e9531e4edd | ||
|
|
0cd9cd294d | ||
|
|
b03fb12fca | ||
|
|
b7b2e8dc4e | ||
|
|
96568abc51 | ||
|
|
a180688858 | ||
|
|
2590067558 | ||
|
|
b5499238f7 | ||
|
|
cc5b96f539 | ||
|
|
32343dc937 | ||
|
|
1e71dd3031 | ||
|
|
ebf6f6a52a | ||
|
|
6bf6d661af | ||
|
|
fe16879f35 | ||
|
|
ead384d1bb | ||
|
|
1ebdc0bacd | ||
|
|
8eca8cdd53 | ||
|
|
24c61b1b37 | ||
|
|
ea18cc7166 | ||
|
|
387c499829 | ||
|
|
339470dd6e | ||
|
|
02d1089dbc | ||
|
|
1bc0932cec | ||
|
|
76cc2739c8 | ||
|
|
b23908aec2 | ||
|
|
6b1b8c0f7b | ||
|
|
06320a7eba | ||
|
|
fc02145d0c | ||
|
|
5360738775 | ||
|
|
0957df752a | ||
|
|
fe56d06b5d | ||
|
|
f4955b16cd | ||
|
|
07692ba73d | ||
|
|
64ac3e8f32 | ||
|
|
8013d3109a | ||
|
|
be6f01dc99 | ||
|
|
30f7779ec6 | ||
|
|
6b5823ca70 | ||
|
|
d4a09ed569 | ||
|
|
84fe409c4b | ||
|
|
51d90891ad | ||
|
|
b1fa06099c | ||
|
|
fa9d8ad6ad | ||
|
|
6703d7b451 | ||
|
|
73afd44d9c | ||
|
|
93cb93bb9b | ||
|
|
82902cff71 | ||
|
|
3657f7e54c | ||
|
|
a57a2f738d | ||
|
|
b93592d703 | ||
|
|
fd288e624b | ||
|
|
095fa3f6ef | ||
|
|
b1f6c578ad | ||
|
|
1bc991bfcb | ||
|
|
02feb478e8 | ||
|
|
c2df79f0c9 | ||
|
|
ef13965747 | ||
|
|
13421601de | ||
|
|
5a9b46c4b5 | ||
|
|
6268642097 | ||
|
|
12eadbc092 | ||
|
|
c9475d1dc2 | ||
|
|
56805defb6 | ||
|
|
477a784201 | ||
|
|
f54bac15c9 | ||
|
|
ae28ebe701 | ||
|
|
f16adf00da | ||
|
|
d17699f6f7 | ||
|
|
8afcf043ee | ||
|
|
dda9d034aa | ||
|
|
652bad51b4 | ||
|
|
4d2ccc0789 | ||
|
|
0e1c21e0f4 | ||
|
|
8d3f58b2cc | ||
|
|
be78905d85 | ||
|
|
b3f232c840 | ||
|
|
075a16c1c3 | ||
|
|
18a13e6266 | ||
|
|
7149bdbc3a | ||
|
|
9a4c4aa9bf | ||
|
|
2b32cab9d1 | ||
|
|
66611db261 | ||
|
|
fdc2095b42 | ||
|
|
2f9cab0da2 | ||
|
|
bd0d474751 | ||
|
|
cca0ab2669 | ||
|
|
6a69a7f416 |
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -20,17 +20,14 @@ Steps to reproduce the behavior:
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
** Keepass Database **
|
||||
**KeePass Database**
|
||||
- Created with: [e.g Windows KeePass 2.42]
|
||||
- Version: [e.g. 2]
|
||||
- Location: [e.g. Remote file retrieved with GDrive app]
|
||||
- Size: [e.g. 150Mo]
|
||||
- Contains attachment: [e.g. Yes]
|
||||
|
||||
**KeePass DX (please complete the following information):**
|
||||
**KeePassDX (please complete the following information):**
|
||||
- Version: [e.g. 2.5.0.0beta23]
|
||||
- Build: [e.g. Free]
|
||||
- Language: [e.g. French]
|
||||
@@ -38,7 +35,7 @@ If applicable, add screenshots to help explain your problem.
|
||||
**Android (please complete the following information):**
|
||||
- Device: [e.g. GalaxyS8]
|
||||
- Version: [e.g. 8.1]
|
||||
- Browser: [e.g. Chrome]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
- Browser for Autofill: [e.g. Chrome version X]
|
||||
|
||||
344
CHANGELOG
344
CHANGELOG
@@ -1,4 +1,292 @@
|
||||
KeepassDX (2.5.0.0beta24)
|
||||
KeePassDX(2.10.3)
|
||||
* Improve Magikeyboard options description #1022 #1023 (Thx @djibux)
|
||||
* Fix database opened without notification (database is now closed when screen is killed in background #1025)
|
||||
* Fix biometric prompt #1018
|
||||
|
||||
KeePassDX(2.10.2)
|
||||
* Fix search fields references #987
|
||||
* Fix Auto-Types with same key #997
|
||||
|
||||
KeePassDX(2.10.1)
|
||||
* Fix parcelable with custom data #986
|
||||
|
||||
KeePassDX(2.10.0)
|
||||
* Manage new database format 4.1 #956
|
||||
* Fix show button consistency #980
|
||||
* Fix persistent notification #979
|
||||
|
||||
KeePassDX(2.9.20)
|
||||
* Fix search with non-latin chars #971
|
||||
* Fix action mode with search #972 (rollback ignore accents #945)
|
||||
* Fix timeout with 0s #974
|
||||
|
||||
KeePassDX(2.9.19)
|
||||
* Fix search slowdown #964
|
||||
* Fix closing notification after lock request #965
|
||||
* Better temp advanced unlocking code implementation #965
|
||||
* Fix OTP token generation #967
|
||||
|
||||
KeePassDX(2.9.18)
|
||||
* Move groups #658
|
||||
* Improve autofill recognition #960
|
||||
* Remove diacritical marks in search string #945
|
||||
* Fix search in references #962
|
||||
* Fix themes in Libre version
|
||||
|
||||
KeePassDX(2.9.17)
|
||||
* Import / Export app properties #839
|
||||
* Force twofish padding compatibility #955
|
||||
* Better timeout preference #579
|
||||
|
||||
KeePassDX(2.9.16)
|
||||
* Fix small bugs #948
|
||||
|
||||
KeePassDX(2.9.15)
|
||||
* Fix themes #935 #926
|
||||
* Decrease default clipboard time #934
|
||||
* Better opening performance #929 #933
|
||||
* Fix memory usage setting #941
|
||||
|
||||
KeePassDX(2.9.14)
|
||||
* Add custom icons #96
|
||||
* Dark Themes #532 #714
|
||||
* Fix binary deduplication #715
|
||||
* Fix IconId #901
|
||||
* Resize image stream dynamically to prevent slowdown #919
|
||||
* Small changes #795 #900 #903 #909 #914
|
||||
|
||||
KeePassDX(2.9.13)
|
||||
* Binary image viewer #473 #749
|
||||
* Fix TOTP plugin settings #878
|
||||
* Allow Emoji #796
|
||||
* Scroll and better UI in entry edition screen #876
|
||||
* Better UI #876
|
||||
* Fix themes and add Purple Dark #889
|
||||
* Allow OTP with many padding #585
|
||||
* Add notes in groups #734
|
||||
|
||||
KeePassDX(2.9.12)
|
||||
* Fix OTP token type #863
|
||||
* Fix auto open biometric prompt #862
|
||||
* Fix back appearance setting #865
|
||||
* Fix orientation change in settings #872
|
||||
* Change memory unit to MiB #851
|
||||
* Small changes #642
|
||||
|
||||
KeePassDX(2.9.11)
|
||||
* Add Keyfile XML version 2 (fix hex) #844
|
||||
* Fix hex Keyfile #861
|
||||
|
||||
KeePassDX(2.9.10)
|
||||
* Try to fix autofill #852
|
||||
* Fix database change dialog displayed too often #853
|
||||
|
||||
KeePassDX(2.9.9)
|
||||
* Detect file changes and reload database #794
|
||||
* Inline suggestions autofill with compatible keyboard (Android R) #827
|
||||
* Add Keyfile XML version 2 #844
|
||||
* Fix binaries of 64 bytes #835
|
||||
* Special search in title fields #830
|
||||
* Priority to OTP button in notifications #845
|
||||
* Fix OTP generation for long secret key #848
|
||||
* Fix small bugs #849
|
||||
|
||||
KeePassDX(2.9.8)
|
||||
* Fix specific attachments with kdbx3.1 databases #828
|
||||
* Fix small bugs
|
||||
|
||||
KeePassDX(2.9.7)
|
||||
* Remove write permission since Android 10 #823
|
||||
* Fix small bugs
|
||||
|
||||
KeePassDX(2.9.6)
|
||||
* Fix KeyFile bug #820
|
||||
|
||||
KeePassDX(2.9.5)
|
||||
* Unlock database by device credentials (PIN/Password/Pattern) with Android M+ #102 #152 #811
|
||||
* Prevent auto switch back to previous keyboard if otp field exists #814
|
||||
* Fix timeout reset #817
|
||||
|
||||
KeePassDX(2.9.4)
|
||||
* Fix small bugs #812
|
||||
* Argon2ID implementation #791
|
||||
|
||||
KeePassDX(2.9.3)
|
||||
* Unlock database by device credentials (PIN/Password/Pattern) #779 #102
|
||||
* Advanced unlock with timeout #102 #437 #566
|
||||
* Remove default database parameter when the file is no longer accessible #803
|
||||
* Move OTP button to the first view level in Magikeyboard #587
|
||||
* Tooltips for Magikeyboard #586
|
||||
* Fix small bugs #805
|
||||
|
||||
KeePassDX(2.9.2)
|
||||
* Managing OTP links from QR applications #556
|
||||
* Prevent manual creation of existing field name #718
|
||||
* Harmonization of field names #789
|
||||
* Different channels for each type of notification #688
|
||||
* Fix OTP #780 #781
|
||||
* Fix same save shared info #783
|
||||
* Fix switch back to previous keyboard #782
|
||||
* Fix read only #792
|
||||
* Better UI #719 #534 #617 #793
|
||||
|
||||
KeePassDX(2.9.1)
|
||||
* Copy password from generator #697
|
||||
* Fix Magikeyboard not fully visible #772
|
||||
* Fix change font size #770
|
||||
* Fix crash #774
|
||||
* Small fixes #771
|
||||
|
||||
KeePassDX(2.9)
|
||||
* Upgrade to Android API 30 #723
|
||||
* Save new credentials with Autofill #524
|
||||
* Setting to close database after Autofill selection #755
|
||||
* Setting to switch keyboard when database is locked #625
|
||||
* Fix biometric issues #724 #740 #731
|
||||
* Fix autofill #725 #551
|
||||
* Fix subdomain search #728
|
||||
* Fix backup search #759
|
||||
* Small fixes and translations #732 #736 #737 #738 #742 #767
|
||||
|
||||
KeePassDX(2.8.7)
|
||||
* Downgrade to Android API 29 (crash on startup with API 30 on some devices)
|
||||
Sorry for the inconvenience
|
||||
|
||||
KeePassDX(2.8.6)
|
||||
* Fix Autofill recognition #712
|
||||
* Keep value after renaming custom field #709
|
||||
* Prevent random binary bug #713
|
||||
* Fix dialog background #717
|
||||
* Better domain recognition for autofill #702
|
||||
* Write custom data #651
|
||||
* Fix autolink #720
|
||||
|
||||
KeePassDX(2.8.5)
|
||||
* Fix Base 64 #708
|
||||
|
||||
KeePassDX(2.8.4)
|
||||
* Fix incomplete attachment deletion #684
|
||||
* Fix opening database v1 without backup folder #692
|
||||
* Fix ANR during first entry education #685
|
||||
* Entry edition as fragment and manual views to fix focus #686
|
||||
* Fix opening database with corrupted attachment #691
|
||||
* Manage empty keyfile #679
|
||||
|
||||
KeePassDX(2.8.3)
|
||||
* Upload attachments
|
||||
* Visibility button for each hidden field
|
||||
* Fix read header file
|
||||
* Fix deletion in KDB database
|
||||
* Fix minor issues
|
||||
|
||||
KeePassDX(2.8.2)
|
||||
* Fix themes / new UI
|
||||
* Fix multiples notifications
|
||||
* Fix entry in Magikeyboard memory
|
||||
* Fix biometric view visibility
|
||||
* Fix fields order
|
||||
* Upgrade code with ViewModel and LiveData
|
||||
|
||||
KeePassDX(2.8.1)
|
||||
* Capture exceptions in coroutines
|
||||
|
||||
KeePassDX(2.8)
|
||||
* Fix TOTP period (> 60s)
|
||||
* Fix searching in recycle bin
|
||||
* Settings to back to the previous keyboard during database credentials and after form filling
|
||||
* Improve action tasks
|
||||
* Improve recognition to reset app timeout
|
||||
* Fix minor issues
|
||||
|
||||
KeePassDX(2.7)
|
||||
* Add blocklists for autofill
|
||||
* Add autofill compatibility mode (usefull for Browser not compatible)
|
||||
* Upgrade autofill recognition algorithm
|
||||
* Setting to search through web subdomains
|
||||
* Refactoring selection mode
|
||||
|
||||
KeePassDX(2.6)
|
||||
* Share a web domain to automatically search for an entry
|
||||
* Default group icon for a new entry
|
||||
* Better autofill recognition
|
||||
* Fix entry not visually deleted in search
|
||||
* Fix hanged loading dialog
|
||||
* Fix auto open biometric prompt if comes from background
|
||||
* Minor fixes
|
||||
|
||||
KeePassDX(2.5)
|
||||
* First stable version of KeePassDX
|
||||
* Fork completely rewritten from the KeePassDroid project
|
||||
* Fix small issues from the last Release Candidate
|
||||
|
||||
KeePassDX(2.5RC2)
|
||||
* Replacement of Spongy Castle by Bouncy Castle
|
||||
* Update Autofill compatibility
|
||||
* Fix Magikeyboard "Go" action
|
||||
* Fix KeeWeb database opening
|
||||
* Fix default username
|
||||
* Fix themes
|
||||
* Fix small issues
|
||||
|
||||
KeePassDX(2.5RC1)
|
||||
* Add write permission to keep compatibility with old file managers
|
||||
* Fix autofill for apps
|
||||
* Auto search for autofill
|
||||
* New keyfile input
|
||||
* Icon to hide keyfile input
|
||||
* New lock button
|
||||
* Setting to hide lock button in user interface
|
||||
* Clickable links in notes
|
||||
* Fix autofill for key-value pairs
|
||||
|
||||
KeePassDX(2.5beta30)
|
||||
* Fix Lock after screen off (wait 1.5 seconds)
|
||||
* Upgrade autofill algorithm
|
||||
* Fix ANR during file verifications
|
||||
|
||||
KeePassDX(2.5beta29)
|
||||
* Upgrade autofill algorithm
|
||||
* Delete registered KeyFile after save new credentials
|
||||
* Fix title and username entry view refresh after an update
|
||||
* Fix database lock request (open notification always active)
|
||||
* Allow empty title in entries
|
||||
* Add expiration datetime
|
||||
|
||||
KeePassDX(2.5beta28)
|
||||
* Fix read only database
|
||||
* Upgrade to Android SDK 29
|
||||
|
||||
KeePassDX (2.5beta27)
|
||||
* New setting to hide broken links
|
||||
* Show URL when title is empty
|
||||
* Setting to open search field at database opening
|
||||
* Fix settings for database locations
|
||||
* Fix error message when database file not writable
|
||||
* Fix appearance refresh settings
|
||||
* Sort optimization
|
||||
|
||||
KeePassDX (2.5.0.0beta26)
|
||||
* Download attachments
|
||||
* Change file size string format
|
||||
* Prevent screenshot for all screen
|
||||
* Auto performed "Go" key in Magikeyboard
|
||||
* Restore and delete entry history
|
||||
* Setting to hide expired entries
|
||||
* New Black theme
|
||||
* Fix crash when clearing clipboard
|
||||
* Fix attachments compressions
|
||||
* Fix dates
|
||||
* Fix UUID message for Database v1
|
||||
|
||||
KeePassDX (2.5.0.0beta25)
|
||||
* Setting for Recycle Bin
|
||||
* Fix Recycle bin issues
|
||||
* Fix TOTP
|
||||
* Fix infinite save
|
||||
* Fix update group
|
||||
* Fix OOM
|
||||
|
||||
KeePassDX (2.5.0.0beta24)
|
||||
* Add OTP (HOTP / TOTP)
|
||||
* Add settings (Color, Security, Master Key)
|
||||
* Show history of each entry
|
||||
@@ -8,7 +296,7 @@ KeepassDX (2.5.0.0beta24)
|
||||
* Open/Save database as service / Add persistent notification
|
||||
* Fix settings / edit group / small bugs
|
||||
|
||||
KeepassDX (2.5.0.0beta23)
|
||||
KeePassDX (2.5.0.0beta23)
|
||||
* New, more secure database creation workflow
|
||||
* Recognize more database files
|
||||
* Add alias for history files (WARNING: history is erased)
|
||||
@@ -17,14 +305,14 @@ KeepassDX (2.5.0.0beta23)
|
||||
* Fix OOM with KeyFile
|
||||
* Fix small issues
|
||||
|
||||
KeepassDX (2.5.0.0beta22)
|
||||
KeePassDX (2.5.0.0beta22)
|
||||
* Rebuild code for actions
|
||||
* Add UUID as entry view
|
||||
* Fix bug with natural order
|
||||
* Fix number of entries in databaseV1
|
||||
* New entry views
|
||||
|
||||
KeepassDX (2.5.0.0beta21)
|
||||
KeePassDX (2.5.0.0beta21)
|
||||
* Fix nested groups no longer visible in V1 databases
|
||||
* Improved data import algorithm for V1 databases
|
||||
* Add natural database sort
|
||||
@@ -32,10 +320,10 @@ KeepassDX (2.5.0.0beta21)
|
||||
* Fix button disabled with only KeyFile
|
||||
* Show the number of entries in a group
|
||||
|
||||
KeepassDX (2.5.0.0beta20)
|
||||
KeePassDX (2.5.0.0beta20)
|
||||
* Fix a major bug that displays an entry history
|
||||
|
||||
KeepassDX (2.5.0.0beta19)
|
||||
KeePassDX (2.5.0.0beta19)
|
||||
* Add lock button always visible
|
||||
* New connection workflow
|
||||
* Code refactored in Kotlin
|
||||
@@ -46,7 +334,7 @@ KeepassDX (2.5.0.0beta19)
|
||||
* Fix memory when load database
|
||||
* Fix small bugs
|
||||
|
||||
KeepassDX (2.5.0.0beta18)
|
||||
KeePassDX (2.5.0.0beta18)
|
||||
* New recent databases views
|
||||
* New information dialog
|
||||
* Custom fields for the Magikeyboard
|
||||
@@ -55,10 +343,10 @@ KeepassDX (2.5.0.0beta18)
|
||||
* Fix memory when opening the database
|
||||
* Memory management for attachments
|
||||
|
||||
KeepassDX (2.5.0.0beta17)
|
||||
KeePassDX (2.5.0.0beta17)
|
||||
* Fix font and search
|
||||
|
||||
KeepassDX (2.5.0.0beta16)
|
||||
KeePassDX (2.5.0.0beta16)
|
||||
* New search in a single fragment
|
||||
* Search suggestions
|
||||
* Added the display of usernames
|
||||
@@ -66,20 +354,20 @@ KeepassDX (2.5.0.0beta16)
|
||||
* Fix read-only mode
|
||||
* Fix parcelable / toolbar / back
|
||||
|
||||
KeepassDX (2.5.0.0beta15)
|
||||
KeePassDX (2.5.0.0beta15)
|
||||
* Read only mode
|
||||
* Best group recovery for the navigation fragment
|
||||
* Fix copies in notifications
|
||||
* Fix orientation
|
||||
* Added translations
|
||||
|
||||
KeepassDX (2.5.0.0beta14)
|
||||
KeePassDX (2.5.0.0beta14)
|
||||
* Optimize all the memory with parcelables / fix search
|
||||
|
||||
KeepassDX (2.5.0.0beta13)
|
||||
KeePassDX (2.5.0.0beta13)
|
||||
* Fix memory issue with parcelable (crash in beta12 version)
|
||||
|
||||
KeepassDX (2.5.0.0beta12)
|
||||
KeePassDX (2.5.0.0beta12)
|
||||
* Added the Magikeyboard to fill the forms (settings still in development)
|
||||
* Added move and copy for groups and entries
|
||||
* New navigation in a single screen / new animations between activities
|
||||
@@ -92,10 +380,10 @@ KeepassDX (2.5.0.0beta12)
|
||||
* Fix the fingerprint recognition (WARNING : The keystore is reinit, you must delete the old keys)
|
||||
* Fix small bugs
|
||||
|
||||
KeepassDX (2.5.0.0beta11)
|
||||
KeePassDX (2.5.0.0beta11)
|
||||
* Fix crash in beta10 version
|
||||
|
||||
KeepassDX (2.5.0.0beta10)
|
||||
KeePassDX (2.5.0.0beta10)
|
||||
* Dynamically change Algorithm and Key Derivation Function in settings
|
||||
* Upgrade translations
|
||||
* New red volcano theme, fix classic dark theme
|
||||
@@ -103,7 +391,7 @@ KeepassDX (2.5.0.0beta10)
|
||||
* Update fingerprint state with checkbox
|
||||
* Fix bugs
|
||||
|
||||
KeepassDX (2.5.0.0beta9)
|
||||
KeePassDX (2.5.0.0beta9)
|
||||
* Education Screens to learn how to use the app
|
||||
* New designs
|
||||
* New custom font for character visibility
|
||||
@@ -112,9 +400,9 @@ KeepassDX (2.5.0.0beta9)
|
||||
* Change setting organisation
|
||||
* Pro version
|
||||
|
||||
KeepassDX (2.5.0.0beta8)
|
||||
KeePassDX (2.5.0.0beta8)
|
||||
* Hide custom entries protected
|
||||
* Best management of field references (https://keepass.info/help/base/fieldrefs.html)
|
||||
* Best management of field references (https://KeePass.info/help/base/fieldrefs.html)
|
||||
* Change database / default settings
|
||||
* Add Autofill for search
|
||||
* Add sorting by last access and by creation time
|
||||
@@ -122,7 +410,7 @@ KeepassDX (2.5.0.0beta8)
|
||||
* Refactor old code
|
||||
* Fix bugs
|
||||
|
||||
KeepassDX (2.5.0.0beta7)
|
||||
KeePassDX (2.5.0.0beta7)
|
||||
* Rebuild Notifications
|
||||
* Change links to https
|
||||
* Add extended Ascii (ñæËÌÂÝÜ...)
|
||||
@@ -131,10 +419,10 @@ KeepassDX (2.5.0.0beta7)
|
||||
* Add setting to prevent the password copy
|
||||
* Fix bugs
|
||||
|
||||
KeepassDX (2.5.0.0beta6)
|
||||
KeePassDX (2.5.0.0beta6)
|
||||
* Fix crash
|
||||
|
||||
KeepassDX (2.5.0.0beta5)
|
||||
KeePassDX (2.5.0.0beta5)
|
||||
* Autofill (Android O)
|
||||
* Deletion for group
|
||||
* New sorts with (Asc/Dsc, Groups before or after)
|
||||
@@ -155,7 +443,7 @@ KeepassDX (2.5.0.0beta5)
|
||||
* Fix many small bugs
|
||||
* Add recycle bin setting (not yet accessible)
|
||||
|
||||
KeepassDX (2.5.0.0beta4)
|
||||
KeePassDX (2.5.0.0beta4)
|
||||
* Show only file name
|
||||
* Setting for full path
|
||||
* Add information for each database file
|
||||
@@ -164,7 +452,7 @@ KeepassDX (2.5.0.0beta4)
|
||||
* Delete view assignment for fingerprint opening
|
||||
* Merge KeePassDroid 2.2.1
|
||||
|
||||
KeepassDX (2.5.0.0beta3)
|
||||
KeePassDX (2.5.0.0beta3)
|
||||
* New database workflow with new screens and folder selection
|
||||
* Settings for default password generation
|
||||
* Fingerprint dialog for explanations
|
||||
@@ -175,17 +463,17 @@ KeepassDX (2.5.0.0beta3)
|
||||
* Merge KeePassDroid 2.2.0.9
|
||||
* Add corruption fix mode
|
||||
|
||||
KeepassDX (2.5.0.0beta2)
|
||||
KeePassDX (2.5.0.0beta2)
|
||||
* Remove libs for F-Droid
|
||||
|
||||
KeepassDX (2.5.0.0beta1)
|
||||
* Fork KeepassDroid
|
||||
KeePassDX (2.5.0.0beta1)
|
||||
* Fork KeePassDroid
|
||||
* Add Material Design
|
||||
* Add Light and Night theme
|
||||
* Min API is 14
|
||||
* Solve bug for fingerprint
|
||||
* Update French translation
|
||||
* Change donation (see KeepassDroid to contribute on both projects)
|
||||
* Change donation (see KeePassDroid to contribute on both projects)
|
||||
|
||||
KeePassDroid (2.2.1)
|
||||
* Fix kdbx4 date corruption
|
||||
@@ -446,7 +734,7 @@ KeePassDroid (1.9.10)
|
||||
|
||||
KeePassDroid (1.9.9)
|
||||
* Go back to explicitly storing blank fields in the database
|
||||
(works around bug in keepassx)
|
||||
(works around bug in KeePassx)
|
||||
* Add support for native code on MIPS architectures
|
||||
* Adding Vibrate permission. On some devices notifications fail
|
||||
without the vibrate permission.
|
||||
|
||||
177
LICENSES/LICENSE_ANDROID_BACKUP_SERVICE
Normal file
177
LICENSES/LICENSE_ANDROID_BACKUP_SERVICE
Normal file
@@ -0,0 +1,177 @@
|
||||
Terms of Service
|
||||
|
||||
This is the terms of service for the Android Backup Service.
|
||||
|
||||
1. Your relationship with Google
|
||||
|
||||
1.1 Your use of the Android Backup Service (referred to as the "Service" in this document) is subject to the terms of a legal agreement between you and Google. "Google" means Google LLC, whose principal place of business is at 1600 Amphitheatre Parkway, Mountain View, CA 94043, United States. This document explains how the agreement is made up, and sets out some of the terms of that agreement.
|
||||
|
||||
1.2 Unless otherwise agreed in writing with Google, your agreement with Google will always include, at a minimum, the terms and conditions set out in this document. These are referred to below as the "Terms".
|
||||
|
||||
1.3 The Terms form a legally binding agreement between you and Google in relation to your use of the Service. It is important that you take the time to read them carefully.
|
||||
|
||||
2. Accepting the Terms
|
||||
|
||||
2.1 In order to use the Service, you must first agree to the Terms. You may not use the Service if you do not accept the Terms.
|
||||
|
||||
2.2 You can accept the Terms by clicking to accept or agree to the Terms, where this option is made available to you by Google.
|
||||
|
||||
2.3 You may not use the Service and may not accept the Terms if you are not of legal age to form a binding contract with Google.
|
||||
|
||||
2.4 You represent that you have full power, capacity and authority to accept these Terms. If you are accepting on behalf of your employer or another entity, you represent that you have full legal authority to bind your employer or such entity to these Terms. If you don't have the legal authority to bind, please ensure that an authorized person from your entity consents to and accepts these Terms.
|
||||
|
||||
3. Provision of the Service by Google
|
||||
|
||||
3.1 Google has subsidiaries and affiliated legal entities around the world ("Subsidiaries and Affiliates"). Sometimes, these companies will be providing the Service to you on behalf of Google itself. You acknowledge and agree that Subsidiaries and Affiliates will be entitled to provide the Service to you.
|
||||
|
||||
3.2 Google is constantly innovating in order to provide the best possible experience for its users. You acknowledge and agree that the form and nature of the Service which Google provides may change from time to time without prior notice to you.
|
||||
|
||||
3.3 As part of this continuing innovation, you acknowledge and agree that Google may stop (permanently or temporarily) providing the Service (or any features within the Service) to you or to users generally at Google's sole discretion, without prior notice to you. You may stop using the Service at any time. You do not need to specifically inform Google when you stop using the Service.
|
||||
|
||||
3.4 You acknowledge and agree that if Google disables your Backup Service Key, you and the Android application(s) you developed ("Application(s)") may be prevented from accessing the Service and any content that is stored with the Service.
|
||||
|
||||
3.5 You acknowledge and agree that Google may set a fixed upper limit on the number of backup transmissions you may send or receive through the Service or on the amount of storage space used for the provision of the Service at any time, at Google's discretion. You agree to abide by any such fixed upper limits.
|
||||
|
||||
4. Use of the Service by you
|
||||
|
||||
4.1 In order to access the Service, you must have a unique application identifier ("Package Name") for your Application as described in the documentation for the Service.
|
||||
|
||||
4.2 After supplying Google with the Package Name and accepting the Terms, you will be issued an alphanumeric key ("Backup Service Key") assigned to you by Google that is uniquely associated with your Application. Your Application must include this Backup Service Key as described in the documentation for the Service.
|
||||
|
||||
4.3 There is currently no limit to the number of Backup Service Keys you may obtain in this manner provided that you use a different Package Name for each Backup Service Key you obtain. You agree that each Backup Service Key is only valid for Applications with the corresponding Package Name. You agree that Google may, in its sole discretion, impose a limit on the number of Backup Service Keys that may be obtained in the future. You agree that your continued use of any of the Backup Service Keys assigned by Google, or distribution of any Applications using such Backup Service Keys, constitutes your continued agreement to these Terms.
|
||||
|
||||
4.4 You agree to use the Service only for purposes that are permitted by (a) the Terms and (b) any applicable law, regulation, third-party terms of service, or generally accepted practices or guidelines in the relevant jurisdictions (including any laws regarding the export of data or software to and from the United States or other relevant countries).
|
||||
|
||||
4.5 You agree not to access (or attempt to access) any of the Service by any means other than through the interfaces, methods, and APIs that are provided by Google, unless you have been specifically allowed to do so in a separate agreement with Google.
|
||||
|
||||
4.6 You agree that you will not engage in any activity that interferes with or disrupts the Service (or the servers and networks which are connected to the Service), or the servers or networks of any third-party.
|
||||
|
||||
4.7 You agree that your use of the Service will be in compliance with any documentation guidelines provided by Google and that failure to comply with the documentation guidelines may result in the disabling of the Backup Service Key(s) for your Application(s).
|
||||
|
||||
4.8 Unless you have been specifically permitted to do so in a separate agreement with Google, you agree that you will not reproduce, duplicate, copy, sell, trade or resell (a) use of the Service, or (b) access to the Service.
|
||||
|
||||
4.9 You agree that you are solely responsible for (and that Google has no responsibility to you or to any third party for) your and your Application's use of the Service, any breach of your obligations under the Terms, and for the consequences (including any loss or damage which Google may suffer) of any such breach.
|
||||
|
||||
4.10 You agree that in your use of the Service, you and your Applications will protect the privacy and legal rights of users. You must provide legally adequate privacy notice and protection for users whose data your Applications back up to the Service. Further, your Application may only use that information for the limited purpose of backing up the data to the Service unless the user has given you permission for further use. If the user has not given you permission to back up information to the Service, you may not transmit such information to the Service.
|
||||
|
||||
4.11 You agree that you and your Applications will not transmit or store sensitive user information, such as user names, passwords, or credit card numbers, through the Service.
|
||||
|
||||
5. Security
|
||||
|
||||
5.1 You agree and understand that you are responsible for maintaining the security associated with any information you provide to access the Service as well as of the Backup Service Key(s) assigned to you by Google. You agree that only you are authorized to use the Backup Service Key(s) assigned to you.
|
||||
|
||||
5.2 Accordingly, you agree that you will be solely responsible to Google for all activities that occur in connection with your access to the Service, as well as the Backup Service Key.
|
||||
|
||||
5.3 If you become aware of any unauthorized use of your Backup Service Key(s) you agree to notify Google immediately.
|
||||
|
||||
6. Privacy and your personal information
|
||||
|
||||
6.1 For information about Google's data protection practices, please read Google's privacy policy at http://www.google.com/privacy.html. This policy explains how Google treats your personal information when you use the Service.
|
||||
|
||||
6.2 You agree to the use of your data in accordance with Google's privacy policies.
|
||||
|
||||
7. Content in the Service
|
||||
|
||||
7.1 You agree that you are solely responsible for (and that Google has no responsibility to you or to any third party for) any Content that you or your Applications transmit or store through the Service and for the consequences of your actions (including any loss or damage which Google may suffer) by doing so. You agree that you are solely responsible for (A) any Content that is transmitted through the Service by your Applications, and (B) any Content that Devices retrieve from the Service by virtue of your Applications. For purposes of the Terms, "Content" means information such as data, messages, settings information, written text, computer software, music, audio files or other sounds, photographs, videos or other images. "Device(s)" means device(s) powered by the Android operating system.
|
||||
|
||||
7.2 You agree that you will not transmit any Content through the Service that is copyrighted, protected by trade secret or otherwise subject to third party proprietary rights, including patent, privacy and publicity rights, unless you are the owner of such rights or have permission from their rightful owner to transmit the Content through the Service.
|
||||
|
||||
8. Proprietary rights
|
||||
|
||||
8.1 You acknowledge and agree that Google (or Google's licensors) own all legal right, title and interest in and to the Service, including any intellectual property rights which subsist in the Service (whether those rights happen to be registered or not, and wherever in the world those rights may exist).
|
||||
|
||||
8.2 Unless you have agreed otherwise in writing with Google, nothing in the Terms gives you a right to use any of Google's trade names, trademarks, service marks, logos, domain names, and other distinctive brand features.
|
||||
|
||||
8.3 If you have been given an explicit right to use any of these brand features in a separate written agreement with Google, then you agree that your use of such features shall be in compliance with that agreement, any applicable provisions of the Terms, and Google's brand feature use guidelines as updated from time to time. These guidelines can be viewed online at http://www.google.com/permissions/ guidelines.html (or such other URL as Google may provide for this purpose from time to time).
|
||||
|
||||
8.4 You agree that you shall not remove, obscure, or alter any proprietary rights notices (including copyright, trade mark notices) which may be affixed to or contained within the Service.
|
||||
|
||||
9. License from Google
|
||||
|
||||
9.1 Subject to terms and conditions of these Terms, Google gives you a personal, worldwide, royalty-free, non-assignable and non-exclusive license to use the Service as provided to you by Google. This license is for the sole purpose of enabling you to use and enjoy the benefit of the Service as provided by Google, in the manner permitted by the Terms.
|
||||
|
||||
9.2 You may not (and you may not permit anyone else to) copy, modify, create a derivative work of, reverse engineer, decompile or otherwise attempt to extract the source code from the Service or any part thereof, unless this is expressly permitted or required by law, or unless you have been specifically told that you may do so by Google, in writing.
|
||||
|
||||
9.3 Unless Google has given you specific written permission to do so, you may not assign (or grant a sub-license of) your rights to use the Service, grant a security interest in or over your rights to use the Service, or otherwise transfer any part of your rights to use the Service.
|
||||
|
||||
10. Your code
|
||||
|
||||
10.1 Google claims no ownership or control over any source code written by you to be used with the Service. You retain copyright and any other rights you already hold in this code, and you are responsible for protecting those rights, as appropriate.
|
||||
|
||||
11. Ending your relationship with Google
|
||||
|
||||
11.1 The Terms will continue to apply until terminated by either you or Google as set out below.
|
||||
|
||||
11.2 You may terminate your legal agreement with Google by discontinuing your use of the Service at any time.
|
||||
|
||||
11.3 Google may, at any time, terminate its legal agreement with you if:
|
||||
|
||||
(A) you have breached any provision of the Terms (or have acted in manner which clearly shows that you do not intend to, or are unable to comply with the provisions of the Terms); or
|
||||
|
||||
(B) Google is required to do so by law (for example, where the provision of the Service to you is, or becomes, unlawful); or
|
||||
|
||||
(C) Google is transitioning to no longer providing the Service; or
|
||||
|
||||
(D) your Application fails to meet the documentation guidelines provided by Google.
|
||||
|
||||
11.4 Nothing in this Section shall affect Google's rights regarding provision of the Service under Section 3 of the Terms.
|
||||
|
||||
11.5 When these Terms come to an end, all of the legal rights, obligations and liabilities that you and Google have benefited from, been subject to (or which have accrued over time whilst the Terms have been in force) or which are expressed to continue indefinitely, shall be unaffected by this cessation, and the provisions of Sections 12, 13 and Paragraph 16 shall continue to apply to such rights, obligations and liabilities indefinitely.
|
||||
|
||||
12. EXCLUSION OF WARRANTIES
|
||||
|
||||
12.1 NOTHING IN THESE TERMS, INCLUDING SECTIONS 12 AND 13, SHALL EXCLUDE OR LIMIT GOOGLE'S WARRANTY OR LIABILITY FOR LOSSES WHICH MAY NOT BE LAWFULLY EXCLUDED OR LIMITED BY APPLICABLE LAW. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR CONDITIONS OR THE LIMITATION OR EXCLUSION OF LIABILITY FOR LOSS OR DAMAGE CAUSED BY NEGLIGENCE, BREACH OF CONTRACT OR BREACH OF IMPLIED TERMS, OR INCIDENTAL OR CONSEQUENTIAL DAMAGES. ACCORDINGLY, ONLY THE LIMITATIONS WHICH ARE LAWFUL IN YOUR JURISDICTION WILL APPLY TO YOU AND OUR LIABILITY WILL BE LIMITED TO THE MAXIMUM EXTENT PERMITTED BY LAW.
|
||||
|
||||
12.2 YOU EXPRESSLY UNDERSTAND AND AGREE THAT YOUR USE OF THE SERVICE IS AT YOUR SOLE RISK AND THAT THE SERVICE AND CONTENT ARE PROVIDED "AS IS" AND "AS AVAILABLE".
|
||||
|
||||
12.3 IN PARTICULAR, GOOGLE, ITS SUBSIDIARIES AND AFFILIATES, AND ITS LICENSORS DO NOT REPRESENT OR WARRANT TO YOU THAT:
|
||||
|
||||
(A) YOUR USE OF THE SERVICE WILL MEET YOUR REQUIREMENTS,
|
||||
|
||||
(B) YOUR USE OF THE SERVICE WILL BE UNINTERRUPTED, TIMELY, SECURE OR FREE FROM ERROR, AND
|
||||
|
||||
(C) THAT DEFECTS IN THE OPERATION OR FUNCTIONALITY OF ANY SOFTWARE PROVIDED TO YOU AS PART OF THE SERVICE WILL BE CORRECTED.
|
||||
|
||||
12.4 NO ADVICE OR INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED BY YOU FROM GOOGLE OR THROUGH OR FROM THE SERVICE SHALL CREATE ANY WARRANTY NOT EXPRESSLY STATED IN THE TERMS.
|
||||
|
||||
12.5 GOOGLE FURTHER EXPRESSLY DISCLAIMS ALL WARRANTIES AND CONDITIONS OF ANY KIND, WHETHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
|
||||
|
||||
13. LIMITATION OF LIABILITY
|
||||
|
||||
13.1 SUBJECT TO OVERALL PROVISION IN PARAGRAPH 12.1 ABOVE, YOU EXPRESSLY UNDERSTAND AND AGREE THAT GOOGLE, ITS SUBSIDIARIES AND AFFILIATES, AND ITS LICENSORS SHALL NOT BE LIABLE TO YOU FOR:
|
||||
|
||||
(A) ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL CONSEQUENTIAL OR EXEMPLARY DAMAGES WHICH MAY BE INCURRED BY YOU, HOWEVER CAUSED AND UNDER ANY THEORY OF LIABILITY. THIS SHALL INCLUDE, BUT NOT BE LIMITED TO, ANY LOSS OF PROFIT (WHETHER INCURRED DIRECTLY OR INDIRECTLY), ANY LOSS OF GOODWILL OR BUSINESS REPUTATION, ANY LOSS OF DATA SUFFERED, COST OF PROCUREMENT OF SUBSTITUTE GOODS OR SERVICE, OR OTHER INTANGIBLE LOSS;
|
||||
|
||||
(B) ANY LOSS OR DAMAGE WHICH MAY BE INCURRED BY YOU, INCLUDING BUT NOT LIMITED TO LOSS OR DAMAGE AS A RESULT OF:
|
||||
|
||||
(I) ANY CHANGES WHICH GOOGLE MAY MAKE TO THE SERVICE, OR FOR ANY PERMANENT OR TEMPORARY CESSATION IN THE PROVISION OF THE SERVICE (OR ANY FEATURES WITHIN THE SERVICE);
|
||||
|
||||
(II) THE DELETION OF, CORRUPTION OF, OR FAILURE TO STORE, ANY CONTENT AND OTHER COMMUNICATIONS DATA MAINTAINED OR TRANSMITTED BY OR THROUGH YOUR USE OF THE SERVICE;
|
||||
|
||||
(III) YOUR FAILURE TO PROVIDE GOOGLE WITH ACCURATE ACCOUNT INFORMATION; OR
|
||||
|
||||
(IV) YOUR FAILURE TO KEEP YOUR PASSWORD OR ACCOUNT DETAILS SECURE AND CONFIDENTIAL.
|
||||
|
||||
13.2 THE LIMITATIONS ON GOOGLE'S LIABILITY TO YOU IN PARAGRAPH 13.1 ABOVE SHALL APPLY WHETHER OR NOT GOOGLE HAS BEEN ADVISED OF OR SHOULD HAVE BEEN AWARE OF THE POSSIBILITY OF ANY SUCH LOSSES ARISING.
|
||||
|
||||
14. Indemnification
|
||||
|
||||
14.1 You agree to hold harmless and indemnify Google, and its subsidiaries, affiliates, officers, agents, employees, or licensors from and against any third party claim arising from or in any way related to (a) your breach of the Terms, (b) your use of the Service, or (c) your violation of applicable laws, rules or regulations in connection with the Service, including any liability or expense arising from all claims, losses, damages (actual and consequential), suits, judgments, litigation costs and attorneys' fees, of every kind and nature. In such a case, Google will provide you with written notice of such claim, suit or action.
|
||||
|
||||
15. Changes to the Terms
|
||||
|
||||
15.1 Due to things like changes to the law or changes to functionality offered through the Service, Google may need to change these Terms from time to time. You should look at the Terms regularly. We'll post notice of the modified Terms within, or through, the Service. Once the modified Terms are posted, the changes will become effective immediately, and you are deemed to have accepted the modified Terms if you continue to use the Service. If you do not agree to the modified Terms for the Service, please stop using the Service.
|
||||
|
||||
16. General legal terms
|
||||
|
||||
16.1 The Terms constitute the whole legal agreement between you and Google and govern your use of the Service (but excluding any service which Google may provide to you under a separate written agreement), and completely replace any prior agreements between you and Google in relation to the Service.
|
||||
|
||||
16.2 You agree that Google may provide you with notices, including those regarding changes to the Terms, by email, regular mail, or postings on the Service.
|
||||
|
||||
16.3 You agree that if Google does not exercise or enforce any legal right or remedy which is contained in the Terms (or which Google has the benefit of under any applicable law), this will not be taken to be a formal waiver of Google's rights and that those rights or remedies will still be available to Google.
|
||||
|
||||
16.4 If any court of law, having the jurisdiction to decide on this matter, rules that any provision of these Terms is invalid, then that provision will be removed from the Terms without affecting the rest of the Terms. The remaining provisions of the Terms will continue to be valid and enforceable.
|
||||
|
||||
16.5 You acknowledge and agree that each member of the group of companies of which Google is the parent shall be third party beneficiaries to the Terms and that such other companies shall be entitled to directly enforce, and rely upon, any provision of the Terms which confers a benefit on (or rights in favor of) them. Other than this, no other person or company shall be third party beneficiaries to the Terms.
|
||||
|
||||
16.6 The Terms, and your relationship with Google under the Terms, shall be governed by the laws of the State of California without regard to its conflict of laws provisions. You and Google agree to submit to the exclusive jurisdiction of the courts located within the county of Santa Clara, California to resolve any legal matter arising from the Terms. Notwithstanding this, you agree that Google shall still be allowed to apply for injunctive remedies (or an equivalent type of urgent legal relief) in any jurisdiction.
|
||||
202
LICENSES/LICENSE_APACHE
Normal file
202
LICENSES/LICENSE_APACHE
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
7
LICENSES/LICENSE_BOUNCY_CASTLE
Normal file
7
LICENSES/LICENSE_BOUNCY_CASTLE
Normal file
@@ -0,0 +1,7 @@
|
||||
Copyright (c) 2000 - 2011 The Legion Of The Bouncy Castle (http://www.bouncycastle.org)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
111
LICENSES/LICENSE_ICON_PACK_CLASSIC
Normal file
111
LICENSES/LICENSE_ICON_PACK_CLASSIC
Normal file
@@ -0,0 +1,111 @@
|
||||
Files ic00.png to ic61.png under res/drawable, res/drawable-hdpi and res/drawable-ldpi
|
||||
|
||||
TITLE: NUVOLA ICON THEME for KDE 3.x
|
||||
AUTHOR: David Vignoni | ICON KING
|
||||
SITE: http://www.icon-king.com
|
||||
MAILING LIST: http://mail.icon-king.com/mailman/listinfo/nuvola_icon-king.com
|
||||
|
||||
Copyright (c) 2003-2004 David Vignoni.
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation,
|
||||
version 2.1 of the License.
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library (see the the license.txt file); if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#######**** NOTE THIS ADD-ON ****#######
|
||||
The GNU Lesser General Public License or LGPL is written for software libraries
|
||||
in the first place. The LGPL has to be considered valid for this artwork
|
||||
library too.
|
||||
Nuvola icon theme for KDE 3.x is a special kind of software library, it is an
|
||||
artwork library, it's elements can be used in a Graphical User Interface, or
|
||||
GUI.
|
||||
Source code, for this library means:
|
||||
- raster png image* .
|
||||
The LGPL in some sections obliges you to make the files carry
|
||||
notices. With images this is in some cases impossible or hardly usefull.
|
||||
With this library a notice is placed at a prominent place in the directory
|
||||
containing the elements. You may follow this practice.
|
||||
The exception in section 6 of the GNU Lesser General Public License covers
|
||||
the use of elements of this art library in a GUI.
|
||||
dave [at] icon-king.com
|
||||
|
||||
Date: 15 october 2004
|
||||
Version: 1.0
|
||||
|
||||
DESCRIPTION:
|
||||
|
||||
Icon theme for KDE 3.x.
|
||||
Icons where designed using Adobe Illustrator, and then exported to PNG format.
|
||||
Icons shadows and minor corrections were done using Adobe Photoshop.
|
||||
Kiconedit was used to correct some 16x16 and 22x22 icons.
|
||||
|
||||
LICENSE
|
||||
|
||||
Released under GNU Lesser General Public License (LGPL)
|
||||
Look at the license.txt file.
|
||||
|
||||
CONTACT
|
||||
|
||||
David Vignoni
|
||||
e-mail : david [at] icon-king.com
|
||||
ICQ : 117761009
|
||||
http: http://www.icon-king.com
|
||||
|
||||
---
|
||||
|
||||
Files ic62.png under res/drawable, res/drawable-hdpi and res/drawable-ldpi
|
||||
|
||||
Based on http://de.wikipedia.org/w/index.php?title=Datei:Tux.svg&filetimestamp=20090927073505
|
||||
|
||||
The copyright holder of this file allows anyone to use it for any purpose,
|
||||
provided that the copyright holders Larry Ewing, Simon Budig and
|
||||
Anja Gerwinski are mentioned.
|
||||
|
||||
---
|
||||
|
||||
Files ic63.png under res/drawable, res/drawable-hdpi and res/drawable-ldpi
|
||||
|
||||
Based on http://en.wikipedia.org/wiki/File:ASF-logo.svg
|
||||
|
||||
Apache logo
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License. You may
|
||||
obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations under
|
||||
the License.
|
||||
|
||||
---
|
||||
|
||||
Files ic64.png under res/drawable, res/drawable-hdpi and res/drawable-ldpi
|
||||
|
||||
Created by Jeremy JAMET and licensed under the terms of the GPLv3.
|
||||
|
||||
---
|
||||
|
||||
Files ic65.png, ic67.png and ic68.png under res/drawable, res/drawable-hdpi and res/drawable-ldpi
|
||||
|
||||
Created by Tobias Selig and licensed under the terms of the GPLv2 or GPLv3.
|
||||
|
||||
---
|
||||
|
||||
File ic66.png under under res/drawable, res/drawable-hdpi and res/drawable-ldpi
|
||||
|
||||
Based on http://commons.wikimedia.org/wiki/File:Dollar_symbol_gold.svg
|
||||
|
||||
Author: Rugby471
|
||||
|
||||
Permission is granted to copy, distribute and/or modify this document under
|
||||
the terms of the GNU Free Documentation License, Version 1.2 or any later
|
||||
version published by the Free Software Foundation; with no Invariant Sections,
|
||||
no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is
|
||||
included in the section entitled "GNU Free Documentation License".
|
||||
|
||||
---
|
||||
373
LICENSES/LICENSE_MOZILLA_COMPONENTS
Normal file
373
LICENSES/LICENSE_MOZILLA_COMPONENTS
Normal file
@@ -0,0 +1,373 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
91
README.md
Normal file
91
README.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# Android KeePassDX
|
||||
|
||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> KeePassDX is a **multi-format KeePass manager for Android devices**. The app allows creating keys and passwords in a secure way by integrating with the Android design standards.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
|
||||
|
||||
### Features
|
||||
|
||||
- Create database files / entries and groups.
|
||||
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
|
||||
- **Compatible** with the majority of alternative programs (KeePass, KeePassX, KeePassXC, …).
|
||||
- Allows opening and **copying URI / URL fields quickly**.
|
||||
- **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*.
|
||||
- **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA).
|
||||
- Material design with **themes**.
|
||||
- **Auto-Fill** and Integration.
|
||||
- Field filling **keyboard**.
|
||||
- **History** of each entry.
|
||||
- Precise management of **settings**.
|
||||
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
|
||||
|
||||
KeePassDX is **open source** and **ad-free**.
|
||||
|
||||
## What is KeePassDX?
|
||||
|
||||
An alternative to remembering an endless list of passwords manually. This is made more difficult by **using different passwords for each account**. If you use one password everywhere and security fails only one of those places, it grants access to your e-mail account, website, etc, and you may not know about it or notice, before bad things happen.
|
||||
|
||||
KeePassDX is a **password manager for Android**, which helps you **manage your passwords in a secure way**. You can put all your passwords in one database, locked with a **master key** and/or a **keyfile**. You **only have to remember one single master password and/or select the keyfile** to unlock the whole database. The databases are encrypted using the best and **most secure encryption algorithms** currently known.
|
||||
|
||||
## Small print?
|
||||
|
||||
KeePassDX is under **open source GPL3 license**, meaning you can use, study, change and share it at will. Copyleft ensures it stays that way.
|
||||
From the full source, anyone can build, fork, and check whether for example the encryption algorithms are implemented correctly.
|
||||
There is **no advertising**.
|
||||
|
||||
Do not worry, **the main features remain completely free**.
|
||||
|
||||
Optional visual styles are accessible after a contribution (and a congratulatory message (Ո‿Ո) ) or the purchase of an extended version to encourage contribution to the work of open source projects!
|
||||
*If you contribute to the project and do not have access to the styles, do not hesitate to contact the author at [contact@kunzisoft.com](contact@kunzisoft.com).*
|
||||
|
||||
## Contributions
|
||||
|
||||
* Add features by making a **[pull request](https://help.github.com/articles/about-pull-requests/)**.
|
||||
* Help to **[translate](https://hosted.weblate.org/projects/keepass-dx/strings/)** KeePassDX to your language (on [Weblate](https://hosted.weblate.org/projects/keepass-dx/) or by sending a [pull request](https://help.github.com/articles/about-pull-requests/)).
|
||||
* **[Donate](https://www.kunzisoft.com/donation)** 人◕ ‿‿ ◕人Y for a better service and a quick development of your features.
|
||||
* Buy the **[Pro version](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro)** of KeePassDX.
|
||||
|
||||
## Download
|
||||
|
||||
*[F-Droid](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/) is the recommended way of installing, a libre software project that verifies that all the libraries and app code is libre software.*
|
||||
|
||||
[<img src="https://f-droid.org/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/)
|
||||
|
||||
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
|
||||
alt="Get it on Google Play"
|
||||
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/wiki/FAQ)
|
||||
|
||||
## Other devices
|
||||
|
||||
- [KeePass](https://keepass.info/) (https://keepass.info/) is the original and official project for the desktop, with technical documentation for standardized database files. It is updated regularly with active maintenance (written in C#).
|
||||
|
||||
- [KeePassXC](https://keepassxc.org/) (https://keepassxc.org/) is an alternative integration of KeePass written in C++.
|
||||
|
||||
- [KeeWeb](https://keeweb.info/) (https://keeweb.info/) is a web version that is also compatible with KeePass files.
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2020 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||
|
||||
This file is part of KeePassDX.
|
||||
|
||||
[KeePassDX](https://www.keepassdx.com) is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
KeePassDX is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*This project is a fork of [KeePassDroid](https://github.com/bpellin/keepassdroid) by bpellin.*
|
||||
85
ReadMe.md
85
ReadMe.md
@@ -1,85 +0,0 @@
|
||||
# Android Keepass DX
|
||||
|
||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> Keepass DX is a **multi-format KeePass manager for Android devices**. The application allows to create keys and passwords in a secure way by integrating with the Android design standards.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
|
||||
|
||||
### Features
|
||||
|
||||
* Create database files / entries and groups
|
||||
* Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm
|
||||
* **Compatible** with the majority of alternative programs (KeePass, KeePassX, KeePass XC...)
|
||||
* Allows **fast copy** of fields and opening of URI / URL
|
||||
* **Biometric recognition** for fast unlocking *(Fingerprint / Face unlock / ...)*
|
||||
* **One-Time Password** management *(HOTP / TOTP)*
|
||||
* Material design with **themes**
|
||||
* **AutoFill** and Integration
|
||||
* Field filling **keyboard**
|
||||
* Precise management of **settings**
|
||||
* Code written in **native language** *(Kotlin / Java / JNI / C)*
|
||||
|
||||
Keepass DX is **open source** and **ad-free**.
|
||||
|
||||
## What is KeePass DX?
|
||||
|
||||
Today you need to remember many passwords. You need a password for your e-mail account, your website's FTP password, online passwords (like website member account), etc. etc. etc. The list is endless. Also, you **should use different passwords for each account**. Because if you use only one password everywhere and someone gets this password you have a problem... A serious problem. The thief would have access to your e-mail account, website, etc. Unimaginable.
|
||||
|
||||
KeePass DX is a **free open source password manager for Android**, which helps you to **manage your passwords in a secure way**. You can put all your passwords in one database, which is locked with one **master key** or a **key file**. So you **only have to remember one single master password or select the key file** to unlock the whole database. The databases are encrypted using the best and **most secure encryption algorithms** currently known.
|
||||
|
||||
## Is it really free?
|
||||
|
||||
Yes, KeePass DX is under **free license (OSI certified)** and **without advertising**. You can have a look at its full source and check whether the encryption algorithms are implemented correctly.
|
||||
|
||||
*Note : If you access the application from a store, visual features may not be available to incentivize the contribution to the work of open source projects. These optional visuals are accessible after a donation (and a small congratulation message :) or the purchase of an extended version, but do not worry, the main features remain completely free. If you contribute to the project and you do not have access to the themes, do not hesitate to contact me at [contact@kunzisoft.com](contact@kunzisoft.com), I will give you the procedure.*
|
||||
|
||||
## Contributions
|
||||
|
||||
You can contribute in different ways to help us on our work.
|
||||
|
||||
* Add features by a **[pull request](https://help.github.com/articles/about-pull-requests/)**.
|
||||
* Help to **[translate](https://hosted.weblate.org/projects/keepass-dx/strings/)** into your language (By using [Weblate](https://hosted.weblate.org/projects/keepass-dx/) or with a manual [pull request](https://help.github.com/articles/about-pull-requests/))
|
||||
* **[Donate](https://www.kunzisoft.com/donation)** 人◕ ‿‿ ◕人Y for a better service and a quick development of your features.
|
||||
* Buy the **[Pro version](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro)** of KeePass DX
|
||||
|
||||
## Download
|
||||
|
||||
*We recommend the installation from [F-Droid](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/) which verifies that all libraries and application code are open source.*
|
||||
|
||||
[<img src="https://f-droid.org/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/)
|
||||
|
||||
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
|
||||
alt="Get it on Google Play"
|
||||
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
|
||||
|
||||
## F.A.Q.
|
||||
|
||||
Other questions? You can read the [F.A.Q.](https://github.com/Kunzisoft/KeePassDX/wiki/F.A.Q.)
|
||||
|
||||
## Other devices
|
||||
|
||||
- [KeePass XC](https://keepassxc.org/) (https://keepassxc.org/) works with **GNU/Linux**, **Mac** and **Windows**, is updated regularly and under the terms of the GNU General Public License. This is the recommended version for computers.
|
||||
|
||||
- [KeePass](https://keepass.info/) (https://keepass.info/) is the historical project, with good technical documentation for standardized database files but only running on **Windows**.
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2019 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||
|
||||
This file is part of KeePass DX.
|
||||
|
||||
[KeePass DX](https://www.keepassdx.com) is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*This project is a fork of [KeepassDroid](https://github.com/bpellin/keepassdroid) by bpellin.*
|
||||
@@ -1 +0,0 @@
|
||||
theme: jekyll-theme-cayman
|
||||
112
app/build.gradle
112
app/build.gradle
@@ -1,34 +1,34 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
buildToolsVersion '28.0.3'
|
||||
ndkVersion "20.1.5948944"
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion "30.0.3"
|
||||
ndkVersion "21.4.7075529"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 28
|
||||
versionCode = 24
|
||||
versionName = "2.5.0.0beta24"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 30
|
||||
versionCode = 81
|
||||
versionName = "2.10.3"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
testInstrumentationRunner = "android.test.InstrumentationTestRunner"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
|
||||
}
|
||||
manifestPlaceholders = [ googleAndroidBackupAPIKey:"unused" ]
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path "src/main/jni/CMakeLists.txt"
|
||||
kapt {
|
||||
arguments {
|
||||
arg("room.incremental", "true")
|
||||
arg("room.schemaLocation", "$projectDir/schemas".toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled = false
|
||||
@@ -36,35 +36,46 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
dexOptions {
|
||||
}
|
||||
|
||||
flavorDimensions "tier"
|
||||
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_Dark\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
||||
buildConfigField "String[]", "STYLES_DISABLED",
|
||||
"{\"KeepassDXStyle_Red\"," +
|
||||
"\"KeepassDXStyle_Red_Night\"," +
|
||||
"\"KeepassDXStyle_Purple\"," +
|
||||
"\"KeepassDXStyle_Purple_Dark\"}"
|
||||
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_Dark\",\"KeepassDXStyle_Blue\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
||||
buildConfigField "String[]", "STYLES_DISABLED",
|
||||
"{\"KeepassDXStyle_Blue\"," +
|
||||
"\"KeepassDXStyle_Blue_Night\"," +
|
||||
"\"KeepassDXStyle_Red\"," +
|
||||
"\"KeepassDXStyle_Red_Night\"," +
|
||||
"\"KeepassDXStyle_Purple\"," +
|
||||
"\"KeepassDXStyle_Purple_Dark\"}"
|
||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,44 +85,57 @@ android {
|
||||
free.res.srcDir 'src/free/res'
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.includeAndroidResources = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
def spongycastleVersion = "1.58.0.0"
|
||||
def room_version = "2.2.1"
|
||||
def room_version = "2.2.6"
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.preference:preference:1.1.0'
|
||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
||||
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 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.biometric:biometric:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.biometric:biometric:1.1.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'
|
||||
// Database
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
|
||||
implementation "com.madgag.spongycastle:core:$spongycastleVersion"
|
||||
implementation "com.madgag.spongycastle:prov:$spongycastleVersion"
|
||||
// Autofill
|
||||
implementation "androidx.autofill:autofill:1.1.0"
|
||||
// Time
|
||||
implementation 'joda-time:joda-time:2.9.9'
|
||||
implementation 'joda-time:joda-time:2.10.6'
|
||||
// Color
|
||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.3'
|
||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4'
|
||||
// Education
|
||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.12.0'
|
||||
// Apache Commons Collections
|
||||
implementation 'commons-collections:commons-collections:3.2.1'
|
||||
implementation 'org.apache.commons:commons-io:1.3.2'
|
||||
// Apache Commons Codec
|
||||
implementation 'commons-codec:commons-codec:1.11'
|
||||
// Base64
|
||||
implementation 'biz.source_code:base64coder:2010-12-19'
|
||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
|
||||
// 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')
|
||||
|
||||
// Tests
|
||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "56438e5f7372ef3e36e33b782aed245d",
|
||||
"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, `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": "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, '56438e5f7372ef3e36e33b782aed245d')"
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
app/src/androidTest/assets/test_image.png
Normal file
BIN
app/src/androidTest/assets/test_image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 MiB |
133
app/src/androidTest/assets/test_text.txt
Normal file
133
app/src/androidTest/assets/test_text.txt
Normal file
@@ -0,0 +1,133 @@
|
||||
Basic Latin
|
||||
! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~
|
||||
Latin-1 Supplement
|
||||
¡ ¢ £ ¤ ¥ ¦ § ¨ © ª « ¬ ® ¯ ° ± ² ³ ´ µ ¶ · ¸ ¹ º » ¼ ½ ¾ ¿ À Á Â Ã Ä Å Æ Ç È É Ê Ë Ì Í Î Ï Ð Ñ Ò Ó Ô Õ Ö × Ø Ù Ú Û Ü Ý Þ ß à á â ã ä å æ ç è é ê ë ì í î ï ð ñ ò ó ô õ ö ÷ ø ù ú û ü ý þ ÿ
|
||||
Latin Extended-A
|
||||
Ā ā Ă ă Ą ą Ć ć Ĉ ĉ Ċ ċ Č č Ď ď Đ đ Ē ē Ĕ ĕ Ė ė Ę ę Ě ě Ĝ ĝ Ğ ğ Ġ ġ Ģ ģ Ĥ ĥ Ħ ħ Ĩ ĩ Ī ī Ĭ ĭ Į į İ ı IJ ij Ĵ ĵ Ķ ķ ĸ Ĺ ĺ Ļ ļ Ľ ľ Ŀ ŀ Ł ł Ń ń Ņ ņ Ň ň ʼn Ŋ ŋ Ō ō Ŏ ŏ Ő ő Œ œ Ŕ ŕ Ŗ ŗ Ř ř Ś ś Ŝ ŝ Ş ş Š š Ţ ţ Ť ť Ŧ ŧ Ũ ũ Ū ū Ŭ ŭ Ů ů Ű ű Ų ų Ŵ ŵ Ŷ ŷ Ÿ Ź ź Ż ż Ž ž ſ
|
||||
Latin Extended-B
|
||||
ƀ Ɓ Ƃ ƃ Ƅ ƅ Ɔ Ƈ ƈ Ɖ Ɗ Ƌ ƌ ƍ Ǝ Ə Ɛ Ƒ ƒ Ɠ Ɣ ƕ Ɩ Ɨ Ƙ ƙ ƚ ƛ Ɯ Ɲ ƞ Ɵ Ơ ơ Ƣ ƣ Ƥ ƥ Ʀ Ƨ ƨ Ʃ ƪ ƫ Ƭ ƭ Ʈ Ư ư Ʊ Ʋ Ƴ ƴ Ƶ ƶ Ʒ Ƹ ƹ ƺ ƻ Ƽ ƽ ƾ ƿ ǀ ǁ ǂ ǃ DŽ Dž dž LJ Lj lj NJ Nj nj Ǎ ǎ Ǐ ǐ Ǒ ǒ Ǔ ǔ Ǖ ǖ Ǘ ǘ Ǚ ǚ Ǜ ǜ ǝ Ǟ ǟ Ǡ ǡ Ǣ ǣ Ǥ ǥ Ǧ ǧ Ǩ ǩ Ǫ ǫ Ǭ ǭ Ǯ ǯ ǰ DZ Dz dz Ǵ ǵ Ǻ ǻ Ǽ ǽ Ǿ ǿ Ȁ ȁ Ȃ ȃ ...
|
||||
IPA Extensions
|
||||
ɐ ɑ ɒ ɓ ɔ ɕ ɖ ɗ ɘ ə ɚ ɛ ɜ ɝ ɞ ɟ ɠ ɡ ɢ ɣ ɤ ɥ ɦ ɧ ɨ ɩ ɪ ɫ ɬ ɭ ɮ ɯ ɰ ɱ ɲ ɳ ɴ ɵ ɶ ɷ ɸ ɹ ɺ ɻ ɼ ɽ ɾ ɿ ʀ ʁ ʂ ʃ ʄ ʅ ʆ ʇ ʈ ʉ ʊ ʋ ʌ ʍ ʎ ʏ ʐ ʑ ʒ ʓ ʔ ʕ ʖ ʗ ʘ ʙ ʚ ʛ ʜ ʝ ʞ ʟ ʠ ʡ ʢ ʣ ʤ ʥ ʦ ʧ ʨ
|
||||
Spacing Modifier Letters
|
||||
ʰ ʱ ʲ ʳ ʴ ʵ ʶ ʷ ʸ ʹ ʺ ʻ ʼ ʽ ʾ ʿ ˀ ˁ ˂ ˃ ˄ ˅ ˆ ˇ ˈ ˉ ˊ ˋ ˌ ˍ ˎ ˏ ː ˑ ˒ ˓ ˔ ˕ ˖ ˗ ˘ ˙ ˚ ˛ ˜ ˝ ˞ ˠ ˡ ˢ ˣ ˤ ˥ ˦ ˧ ˨ ˩
|
||||
Combining Diacritical Marks
|
||||
̀ ́ ̂ ̃ ̄ ̅ ̆ ̇ ̈ ̉ ̊ ̋ ̌ ̍ ̎ ̏ ̐ ̑ ̒ ̓ ̔ ̕ ̖ ̗ ̘ ̙ ̚ ̛ ̜ ̝ ̞ ̟ ̠ ̡ ̢ ̣ ̤ ̥ ̦ ̧ ̨ ̩ ̪ ̫ ̬ ̭ ̮ ̯ ̰ ̱ ̲ ̳ ̴ ̵ ̶ ̷ ̸ ̹ ̺ ̻ ̼ ̽ ̾ ̿ ̀ ́ ͂ ̓ ̈́ ͅ ͠ ͡
|
||||
Greek
|
||||
ʹ ͵ ͺ ; ΄ ΅ Ά · Έ Ή Ί Ό Ύ Ώ ΐ Α Β Γ Δ Ε Ζ Η Θ Ι Κ Λ Μ Ν Ξ Ο Π Ρ Σ Τ Υ Φ Χ Ψ Ω Ϊ Ϋ ά έ ή ί ΰ α β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ ς σ τ υ φ χ ψ ω ϊ ϋ ό ύ ώ ϐ ϑ ϒ ϓ ϔ ϕ ϖ Ϛ Ϝ Ϟ Ϡ Ϣ ϣ Ϥ ϥ Ϧ ϧ Ϩ ϩ Ϫ ϫ Ϭ ϭ Ϯ ϯ ϰ ϱ ϲ ϳ
|
||||
Cyrillic
|
||||
Ё Ђ Ѓ Є Ѕ І Ї Ј Љ Њ Ћ Ќ Ў Џ А Б В Г Д Е Ж З И Й К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ Ъ Ы Ь Э Ю Я а б в г д е ж з и й к л м н о п р с т у ф х ц ч ш щ ъ ы ь э ю я ё ђ ѓ є ѕ і ї ј љ њ ћ ќ ў џ Ѡ ѡ Ѣ ѣ Ѥ ѥ Ѧ ѧ Ѩ ѩ Ѫ ѫ Ѭ ѭ Ѯ ѯ Ѱ ѱ Ѳ ѳ Ѵ ѵ Ѷ ѷ Ѹ ѹ Ѻ ѻ Ѽ ѽ Ѿ ѿ Ҁ ҁ ҂ ҃ ...
|
||||
Armenian
|
||||
Ա Բ Գ Դ Ե Զ Է Ը Թ Ժ Ի Լ Խ Ծ Կ Հ Ձ Ղ Ճ Մ Յ Ն Շ Ո Չ Պ Ջ Ռ Ս Վ Տ Ր Ց Ւ Փ Ք Օ Ֆ ՙ ՚ ՛ ՜ ՝ ՞ ՟ ա բ գ դ ե զ է ը թ ժ ի լ խ ծ կ հ ձ ղ ճ մ յ ն շ ո չ պ ջ ռ ս վ տ ր ց ւ փ ք օ ֆ և ։
|
||||
Hebrew
|
||||
֑ ֒ ֓ ֔ ֕ ֖ ֗ ֘ ֙ ֚ ֛ ֜ ֝ ֞ ֟ ֠ ֡ ֣ ֤ ֥ ֦ ֧ ֨ ֩ ֪ ֫ ֬ ֭ ֮ ֯ ְ ֱ ֲ ֳ ִ ֵ ֶ ַ ָ ֹ ֻ ּ ֽ ־ ֿ ׀ ׁ ׂ ׃ ׄ א ב ג ד ה ו ז ח ט י ך כ ל ם מ ן נ ס ע ף פ ץ צ ק ר ש ת װ ױ ײ ׳ ״
|
||||
Arabic
|
||||
، ؛ ؟ ء آ أ ؤ إ ئ ا ب ة ت ث ج ح خ د ذ ر ز س ش ص ض ط ظ ع غ ـ ف ق ك ل م ن ه و ى ي ً ٌ ٍ َ ُ ِ ّ ْ ٠ ١ ٢ ٣ ٤ ٥ ٦ ٧ ٨ ٩ ٪ ٫ ٬ ٭ ٰ ٱ ٲ ٳ ٴ ٵ ٶ ٷ ٸ ٹ ٺ ٻ ټ ٽ پ ٿ ڀ ځ ڂ ڃ ڄ څ چ ڇ ڈ ډ ڊ ڋ ڌ ڍ ڎ ڏ ڐ ڑ ڒ ړ ڔ ڕ ږ ڗ ژ ڙ ښ ڛ ڜ ڝ ڞ ڟ ڠ ڡ ڢ ڣ ڤ ڥ ڦ ڧ ڨ ک ڪ ګ ڬ ڭ ڮ گ ڰ ڱ ...
|
||||
Devanagari
|
||||
ँ ं ः अ आ इ ई उ ऊ ऋ ऌ ऍ ऎ ए ऐ ऑ ऒ ओ औ क ख ग घ ङ च छ ज झ ञ ट ठ ड ढ ण त थ द ध न ऩ प फ ब भ म य र ऱ ल ळ ऴ व श ष स ह ़ ऽ ा ि ी ु ू ृ ॄ ॅ ॆ े ै ॉ ॊ ो ौ ् ॐ ॑ ॒ ॓ ॔ क़ ख़ ग़ ज़ ड़ ढ़ फ़ य़ ॠ ॡ ॢ ॣ । ॥ ० १ २ ३ ४ ५ ६ ७ ८ ९ ॰
|
||||
Bengali
|
||||
ঁ ং ঃ অ আ ই ঈ উ ঊ ঋ ঌ এ ঐ ও ঔ ক খ গ ঘ ঙ চ ছ জ ঝ ঞ ট ঠ ড ঢ ণ ত থ দ ধ ন প ফ ব ভ ম য র ল শ ষ স হ ় া ি ী ু ূ ৃ ৄ ে ৈ ো ৌ ্ ৗ ড় ঢ় য় ৠ ৡ ৢ ৣ ০ ১ ২ ৩ ৪ ৫ ৬ ৭ ৮ ৯ ৰ ৱ ৲ ৳ ৴ ৵ ৶ ৷ ৸ ৹ ৺
|
||||
Gurmukhi
|
||||
ਂ ਅ ਆ ਇ ਈ ਉ ਊ ਏ ਐ ਓ ਔ ਕ ਖ ਗ ਘ ਙ ਚ ਛ ਜ ਝ ਞ ਟ ਠ ਡ ਢ ਣ ਤ ਥ ਦ ਧ ਨ ਪ ਫ ਬ ਭ ਮ ਯ ਰ ਲ ਲ਼ ਵ ਸ਼ ਸ ਹ ਼ ਾ ਿ ੀ ੁ ੂ ੇ ੈ ੋ ੌ ੍ ਖ਼ ਗ਼ ਜ਼ ੜ ਫ਼ ੦ ੧ ੨ ੩ ੪ ੫ ੬ ੭ ੮ ੯ ੰ ੱ ੲ ੳ ੴ
|
||||
Gujarati
|
||||
ઁ ં ઃ અ આ ઇ ઈ ઉ ઊ ઋ ઍ એ ઐ ઑ ઓ ઔ ક ખ ગ ઘ ઙ ચ છ જ ઝ ઞ ટ ઠ ડ ઢ ણ ત થ દ ધ ન પ ફ બ ભ મ ય ર લ ળ વ શ ષ સ હ ઼ ઽ ા િ ી ુ ૂ ૃ ૄ ૅ ે ૈ ૉ ો ૌ ્ ૐ ૠ ૦ ૧ ૨ ૩ ૪ ૫ ૬ ૭ ૮ ૯
|
||||
Oriya
|
||||
ଁ ଂ ଃ ଅ ଆ ଇ ଈ ଉ ଊ ଋ ଌ ଏ ଐ ଓ ଔ କ ଖ ଗ ଘ ଙ ଚ ଛ ଜ ଝ ଞ ଟ ଠ ଡ ଢ ଣ ତ ଥ ଦ ଧ ନ ପ ଫ ବ ଭ ମ ଯ ର ଲ ଳ ଶ ଷ ସ ହ ଼ ଽ ା ି ୀ ୁ ୂ ୃ େ ୈ ୋ ୌ ୍ ୖ ୗ ଡ଼ ଢ଼ ୟ ୠ ୡ ୦ ୧ ୨ ୩ ୪ ୫ ୬ ୭ ୮ ୯ ୰
|
||||
Tamil
|
||||
ஂ ஃ அ ஆ இ ஈ உ ஊ எ ஏ ஐ ஒ ஓ ஔ க ங ச ஜ ஞ ட ண த ந ன ப ம ய ர ற ல ள ழ வ ஷ ஸ ஹ ா ி ீ ு ூ ெ ே ை ொ ோ ௌ ் ௗ ௧ ௨ ௩ ௪ ௫ ௬ ௭ ௮ ௯ ௰ ௱ ௲
|
||||
Telugu
|
||||
ఁ ం ః అ ఆ ఇ ఈ ఉ ఊ ఋ ఌ ఎ ఏ ఐ ఒ ఓ ఔ క ఖ గ ఘ ఙ చ ఛ జ ఝ ఞ ట ఠ డ ఢ ణ త థ ద ధ న ప ఫ బ భ మ య ర ఱ ల ళ వ శ ష స హ ా ి ీ ు ూ ృ ౄ ె ే ై ొ ో ౌ ్ ౕ ౖ ౠ ౡ ౦ ౧ ౨ ౩ ౪ ౫ ౬ ౭ ౮ ౯
|
||||
Kannada
|
||||
ಂ ಃ ಅ ಆ ಇ ಈ ಉ ಊ ಋ ಌ ಎ ಏ ಐ ಒ ಓ ಔ ಕ ಖ ಗ ಘ ಙ ಚ ಛ ಜ ಝ ಞ ಟ ಠ ಡ ಢ ಣ ತ ಥ ದ ಧ ನ ಪ ಫ ಬ ಭ ಮ ಯ ರ ಱ ಲ ಳ ವ ಶ ಷ ಸ ಹ ಾ ಿ ೀ ು ೂ ೃ ೄ ೆ ೇ ೈ ೊ ೋ ೌ ್ ೕ ೖ ೞ ೠ ೡ ೦ ೧ ೨ ೩ ೪ ೫ ೬ ೭ ೮ ೯
|
||||
Malayalam
|
||||
ം ഃ അ ആ ഇ ഈ ഉ ഊ ഋ ഌ എ ഏ ഐ ഒ ഓ ഔ ക ഖ ഗ ഘ ങ ച ഛ ജ ഝ ഞ ട ഠ ഡ ഢ ണ ത ഥ ദ ധ ന പ ഫ ബ ഭ മ യ ര റ ല ള ഴ വ ശ ഷ സ ഹ ാ ി ീ ു ൂ ൃ െ േ ൈ ൊ ോ ൌ ് ൗ ൠ ൡ ൦ ൧ ൨ ൩ ൪ ൫ ൬ ൭ ൮ ൯
|
||||
Thai
|
||||
ก ข ฃ ค ฅ ฆ ง จ ฉ ช ซ ฌ ญ ฎ ฏ ฐ ฑ ฒ ณ ด ต ถ ท ธ น บ ป ผ ฝ พ ฟ ภ ม ย ร ฤ ล ฦ ว ศ ษ ส ห ฬ อ ฮ ฯ ะ ั า ำ ิ ี ึ ื ุ ู ฺ ฿ เ แ โ ใ ไ ๅ ๆ ็ ่ ้ ๊ ๋ ์ ํ ๎ ๏ ๐ ๑ ๒ ๓ ๔ ๕ ๖ ๗ ๘ ๙ ๚ ๛
|
||||
Lao
|
||||
ກ ຂ ຄ ງ ຈ ຊ ຍ ດ ຕ ຖ ທ ນ ບ ປ ຜ ຝ ພ ຟ ມ ຢ ຣ ລ ວ ສ ຫ ອ ຮ ຯ ະ ັ າ ຳ ິ ີ ຶ ື ຸ ູ ົ ຼ ຽ ເ ແ ໂ ໃ ໄ ໆ ່ ້ ໊ ໋ ໌ ໍ ໐ ໑ ໒ ໓ ໔ ໕ ໖ ໗ ໘ ໙ ໜ ໝ
|
||||
Tibetan
|
||||
ༀ ༁ ༂ ༃ ༄ ༅ ༆ ༇ ༈ ༉ ༊ ་ ༌ ། ༎ ༏ ༐ ༑ ༒ ༓ ༔ ༕ ༖ ༗ ༘ ༙ ༚ ༛ ༜ ༝ ༞ ༟ ༠ ༡ ༢ ༣ ༤ ༥ ༦ ༧ ༨ ༩ ༪ ༫ ༬ ༭ ༮ ༯ ༰ ༱ ༲ ༳ ༴ ༵ ༶ ༷ ༸ ༹ ༺ ༻ ༼ ༽ ༾ ༿ ཀ ཁ ག གྷ ང ཅ ཆ ཇ ཉ ཊ ཋ ཌ ཌྷ ཎ ཏ ཐ ད དྷ ན པ ཕ བ བྷ མ ཙ ཚ ཛ ཛྷ ཝ ཞ ཟ འ ཡ ར ལ ཤ ཥ ས ཧ ཨ ཀྵ ཱ ི ཱི ུ ཱུ ྲྀ ཷ ླྀ ཹ ེ ཻ ོ ཽ ཾ ཿ ྀ ཱྀ ྂ ྃ ྄ ྅ ྆ ྇ ...
|
||||
Georgian
|
||||
Ⴀ Ⴁ Ⴂ Ⴃ Ⴄ Ⴅ Ⴆ Ⴇ Ⴈ Ⴉ Ⴊ Ⴋ Ⴌ Ⴍ Ⴎ Ⴏ Ⴐ Ⴑ Ⴒ Ⴓ Ⴔ Ⴕ Ⴖ Ⴗ Ⴘ Ⴙ Ⴚ Ⴛ Ⴜ Ⴝ Ⴞ Ⴟ Ⴠ Ⴡ Ⴢ Ⴣ Ⴤ Ⴥ ა ბ გ დ ე ვ ზ თ ი კ ლ მ ნ ო პ ჟ რ ს ტ უ ფ ქ ღ ყ შ ჩ ც ძ წ ჭ ხ ჯ ჰ ჱ ჲ ჳ ჴ ჵ ჶ ჻
|
||||
Hangul Jamo
|
||||
ᄀ ᄁ ᄂ ᄃ ᄄ ᄅ ᄆ ᄇ ᄈ ᄉ ᄊ ᄋ ᄌ ᄍ ᄎ ᄏ ᄐ ᄑ ᄒ ᄓ ᄔ ᄕ ᄖ ᄗ ᄘ ᄙ ᄚ ᄛ ᄜ ᄝ ᄞ ᄟ ᄠ ᄡ ᄢ ᄣ ᄤ ᄥ ᄦ ᄧ ᄨ ᄩ ᄪ ᄫ ᄬ ᄭ ᄮ ᄯ ᄰ ᄱ ᄲ ᄳ ᄴ ᄵ ᄶ ᄷ ᄸ ᄹ ᄺ ᄻ ᄼ ᄽ ᄾ ᄿ ᅀ ᅁ ᅂ ᅃ ᅄ ᅅ ᅆ ᅇ ᅈ ᅉ ᅊ ᅋ ᅌ ᅍ ᅎ ᅏ ᅐ ᅑ ᅒ ᅓ ᅔ ᅕ ᅖ ᅗ ᅘ ᅙ ᅟ ᅠ ᅡ ᅢ ᅣ ᅤ ᅥ ᅦ ᅧ ᅨ ᅩ ᅪ ᅫ ᅬ ᅭ ᅮ ᅯ ᅰ ᅱ ᅲ ᅳ ᅴ ᅵ ᅶ ᅷ ᅸ ᅹ ᅺ ᅻ ᅼ ᅽ ᅾ ᅿ ᆀ ᆁ ᆂ ᆃ ᆄ ...
|
||||
Latin Extended Additional
|
||||
Ḁ ḁ Ḃ ḃ Ḅ ḅ Ḇ ḇ Ḉ ḉ Ḋ ḋ Ḍ ḍ Ḏ ḏ Ḑ ḑ Ḓ ḓ Ḕ ḕ Ḗ ḗ Ḙ ḙ Ḛ ḛ Ḝ ḝ Ḟ ḟ Ḡ ḡ Ḣ ḣ Ḥ ḥ Ḧ ḧ Ḩ ḩ Ḫ ḫ Ḭ ḭ Ḯ ḯ Ḱ ḱ Ḳ ḳ Ḵ ḵ Ḷ ḷ Ḹ ḹ Ḻ ḻ Ḽ ḽ Ḿ ḿ Ṁ ṁ Ṃ ṃ Ṅ ṅ Ṇ ṇ Ṉ ṉ Ṋ ṋ Ṍ ṍ Ṏ ṏ Ṑ ṑ Ṓ ṓ Ṕ ṕ Ṗ ṗ Ṙ ṙ Ṛ ṛ Ṝ ṝ Ṟ ṟ Ṡ ṡ Ṣ ṣ Ṥ ṥ Ṧ ṧ Ṩ ṩ Ṫ ṫ Ṭ ṭ Ṯ ṯ Ṱ ṱ Ṳ ṳ Ṵ ṵ Ṷ ṷ Ṹ ṹ Ṻ ṻ Ṽ ṽ Ṿ ṿ ...
|
||||
Greek Extended
|
||||
ἀ ἁ ἂ ἃ ἄ ἅ ἆ ἇ Ἀ Ἁ Ἂ Ἃ Ἄ Ἅ Ἆ Ἇ ἐ ἑ ἒ ἓ ἔ ἕ Ἐ Ἑ Ἒ Ἓ Ἔ Ἕ ἠ ἡ ἢ ἣ ἤ ἥ ἦ ἧ Ἠ Ἡ Ἢ Ἣ Ἤ Ἥ Ἦ Ἧ ἰ ἱ ἲ ἳ ἴ ἵ ἶ ἷ Ἰ Ἱ Ἲ Ἳ Ἴ Ἵ Ἶ Ἷ ὀ ὁ ὂ ὃ ὄ ὅ Ὀ Ὁ Ὂ Ὃ Ὄ Ὅ ὐ ὑ ὒ ὓ ὔ ὕ ὖ ὗ Ὑ Ὓ Ὕ Ὗ ὠ ὡ ὢ ὣ ὤ ὥ ὦ ὧ Ὠ Ὡ Ὢ Ὣ Ὤ Ὥ Ὦ Ὧ ὰ ά ὲ έ ὴ ή ὶ ί ὸ ό ὺ ύ ὼ ώ ᾀ ᾁ ᾂ ᾃ ᾄ ᾅ ᾆ ᾇ ᾈ ᾉ ᾊ ᾋ ᾌ ᾍ ...
|
||||
General Punctuation
|
||||
‐ ‑ ‒ – — ― ‖ ‗ ‘ ’ ‚ ‛ “ ” „ ‟ † ‡ • ‣ ․ ‥ … ‧
‰ ‱ ′ ″ ‴ ‵ ‶ ‷ ‸ ‹ › ※ ‼ ‽ ‾ ‿ ⁀ ⁁ ⁂ ⁃ ⁄ ⁅ ⁆
|
||||
Superscripts and Subscripts
|
||||
⁰ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁺ ⁻ ⁼ ⁽ ⁾ ⁿ ₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₊ ₋ ₌ ₍ ₎
|
||||
Currency Symbols
|
||||
₠ ₡ ₢ ₣ ₤ ₥ ₦ ₧ ₨ ₩ ₪ ₫
|
||||
Combining Marks for Symbols
|
||||
⃐ ⃑ ⃒ ⃓ ⃔ ⃕ ⃖ ⃗ ⃘ ⃙ ⃚ ⃛ ⃜ ⃝ ⃞ ⃟ ⃠ ⃡
|
||||
Letterlike Symbols
|
||||
℀ ℁ ℂ ℃ ℄ ℅ ℆ ℇ ℈ ℉ ℊ ℋ ℌ ℍ ℎ ℏ ℐ ℑ ℒ ℓ ℔ ℕ № ℗ ℘ ℙ ℚ ℛ ℜ ℝ ℞ ℟ ℠ ℡ ™ ℣ ℤ ℥ Ω ℧ ℨ ℩ K Å ℬ ℭ ℮ ℯ ℰ ℱ Ⅎ ℳ ℴ ℵ ℶ ℷ ℸ
|
||||
Number Forms
|
||||
⅓ ⅔ ⅕ ⅖ ⅗ ⅘ ⅙ ⅚ ⅛ ⅜ ⅝ ⅞ ⅟ Ⅰ Ⅱ Ⅲ Ⅳ Ⅴ Ⅵ Ⅶ Ⅷ Ⅸ Ⅹ Ⅺ Ⅻ Ⅼ Ⅽ Ⅾ Ⅿ ⅰ ⅱ ⅲ ⅳ ⅴ ⅵ ⅶ ⅷ ⅸ ⅹ ⅺ ⅻ ⅼ ⅽ ⅾ ⅿ ↀ ↁ ↂ
|
||||
Arrows
|
||||
← ↑ → ↓ ↔ ↕ ↖ ↗ ↘ ↙ ↚ ↛ ↜ ↝ ↞ ↟ ↠ ↡ ↢ ↣ ↤ ↥ ↦ ↧ ↨ ↩ ↪ ↫ ↬ ↭ ↮ ↯ ↰ ↱ ↲ ↳ ↴ ↵ ↶ ↷ ↸ ↹ ↺ ↻ ↼ ↽ ↾ ↿ ⇀ ⇁ ⇂ ⇃ ⇄ ⇅ ⇆ ⇇ ⇈ ⇉ ⇊ ⇋ ⇌ ⇍ ⇎ ⇏ ⇐ ⇑ ⇒ ⇓ ⇔ ⇕ ⇖ ⇗ ⇘ ⇙ ⇚ ⇛ ⇜ ⇝ ⇞ ⇟ ⇠ ⇡ ⇢ ⇣ ⇤ ⇥ ⇦ ⇧ ⇨ ⇩ ⇪
|
||||
Mathematical Operators
|
||||
∀ ∁ ∂ ∃ ∄ ∅ ∆ ∇ ∈ ∉ ∊ ∋ ∌ ∍ ∎ ∏ ∐ ∑ − ∓ ∔ ∕ ∖ ∗ ∘ ∙ √ ∛ ∜ ∝ ∞ ∟ ∠ ∡ ∢ ∣ ∤ ∥ ∦ ∧ ∨ ∩ ∪ ∫ ∬ ∭ ∮ ∯ ∰ ∱ ∲ ∳ ∴ ∵ ∶ ∷ ∸ ∹ ∺ ∻ ∼ ∽ ∾ ∿ ≀ ≁ ≂ ≃ ≄ ≅ ≆ ≇ ≈ ≉ ≊ ≋ ≌ ≍ ≎ ≏ ≐ ≑ ≒ ≓ ≔ ≕ ≖ ≗ ≘ ≙ ≚ ≛ ≜ ≝ ≞ ≟ ≠ ≡ ≢ ≣ ≤ ≥ ≦ ≧ ≨ ≩ ≪ ≫ ≬ ≭ ≮ ≯ ≰ ≱ ≲ ≳ ≴ ≵ ≶ ≷ ≸ ≹ ≺ ≻ ≼ ≽ ≾ ≿ ...
|
||||
Miscellaneous Technical
|
||||
⌀ ⌂ ⌃ ⌄ ⌅ ⌆ ⌇ ⌈ ⌉ ⌊ ⌋ ⌌ ⌍ ⌎ ⌏ ⌐ ⌑ ⌒ ⌓ ⌔ ⌕ ⌖ ⌗ ⌘ ⌙ ⌚ ⌛ ⌜ ⌝ ⌞ ⌟ ⌠ ⌡ ⌢ ⌣ ⌤ ⌥ ⌦ ⌧ ⌨ 〈 〉 ⌫ ⌬ ⌭ ⌮ ⌯ ⌰ ⌱ ⌲ ⌳ ⌴ ⌵ ⌶ ⌷ ⌸ ⌹ ⌺ ⌻ ⌼ ⌽ ⌾ ⌿ ⍀ ⍁ ⍂ ⍃ ⍄ ⍅ ⍆ ⍇ ⍈ ⍉ ⍊ ⍋ ⍌ ⍍ ⍎ ⍏ ⍐ ⍑ ⍒ ⍓ ⍔ ⍕ ⍖ ⍗ ⍘ ⍙ ⍚ ⍛ ⍜ ⍝ ⍞ ⍟ ⍠ ⍡ ⍢ ⍣ ⍤ ⍥ ⍦ ⍧ ⍨ ⍩ ⍪ ⍫ ⍬ ⍭ ⍮ ⍯ ⍰ ⍱ ⍲ ⍳ ⍴ ⍵ ⍶ ⍷ ⍸ ⍹ ⍺
|
||||
Control Pictures
|
||||
␀ ␁ ␂ ␃ ␄ ␅ ␆ ␇ ␈ ␉ ␊ ␋ ␌ ␍ ␎ ␏ ␐ ␑ ␒ ␓ ␔ ␕ ␖ ␗ ␘ ␙ ␚ ␛ ␜ ␝ ␞ ␟ ␠ ␡ ␢ ␣ 
|
||||
Optical Character Recognition
|
||||
⑀ ⑁ ⑂ ⑃ ⑄ ⑅ ⑆ ⑇ ⑈ ⑉ ⑊
|
||||
Enclosed Alphanumerics
|
||||
① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ ⑫ ⑬ ⑭ ⑮ ⑯ ⑰ ⑱ ⑲ ⑳ ⑴ ⑵ ⑶ ⑷ ⑸ ⑹ ⑺ ⑻ ⑼ ⑽ ⑾ ⑿ ⒀ ⒁ ⒂ ⒃ ⒄ ⒅ ⒆ ⒇ ⒈ ⒉ ⒊ ⒋ ⒌ ⒍ ⒎ ⒏ ⒐ ⒑ ⒒ ⒓ ⒔ ⒕ ⒖ ⒗ ⒘ ⒙ ⒚ ⒛ ⒜ ⒝ ⒞ ⒟ ⒠ ⒡ ⒢ ⒣ ⒤ ⒥ ⒦ ⒧ ⒨ ⒩ ⒪ ⒫ ⒬ ⒭ ⒮ ⒯ ⒰ ⒱ ⒲ ⒳ ⒴ ⒵ Ⓐ Ⓑ Ⓒ Ⓓ Ⓔ Ⓕ Ⓖ Ⓗ Ⓘ Ⓙ Ⓚ Ⓛ Ⓜ Ⓝ Ⓞ Ⓟ Ⓠ Ⓡ Ⓢ Ⓣ Ⓤ Ⓥ Ⓦ Ⓧ Ⓨ Ⓩ ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ ⓖ ⓗ ⓘ ⓙ ⓚ ⓛ ⓜ ⓝ ⓞ ⓟ ...
|
||||
Box Drawing
|
||||
─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏ ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟ ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯ ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿ ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏ ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟ ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯ ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿
|
||||
Block Elements
|
||||
▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▉ ▊ ▋ ▌ ▍ ▎ ▏ ▐ ░ ▒ ▓ ▔ ▕
|
||||
Geometric Shapes
|
||||
■ □ ▢ ▣ ▤ ▥ ▦ ▧ ▨ ▩ ▪ ▫ ▬ ▭ ▮ ▯ ▰ ▱ ▲ △ ▴ ▵ ▶ ▷ ▸ ▹ ► ▻ ▼ ▽ ▾ ▿ ◀ ◁ ◂ ◃ ◄ ◅ ◆ ◇ ◈ ◉ ◊ ○ ◌ ◍ ◎ ● ◐ ◑ ◒ ◓ ◔ ◕ ◖ ◗ ◘ ◙ ◚ ◛ ◜ ◝ ◞ ◟ ◠ ◡ ◢ ◣ ◤ ◥ ◦ ◧ ◨ ◩ ◪ ◫ ◬ ◭ ◮ ◯
|
||||
Miscellaneous Symbols
|
||||
☀ ☁ ☂ ☃ ☄ ★ ☆ ☇ ☈ ☉ ☊ ☋ ☌ ☍ ☎ ☏ ☐ ☑ ☒ ☓ ☚ ☛ ☜ ☝ ☞ ☟ ☠ ☡ ☢ ☣ ☤ ☥ ☦ ☧ ☨ ☩ ☪ ☫ ☬ ☭ ☮ ☯ ☰ ☱ ☲ ☳ ☴ ☵ ☶ ☷ ☸ ☹ ☺ ☻ ☼ ☽ ☾ ☿ ♀ ♁ ♂ ♃ ♄ ♅ ♆ ♇ ♈ ♉ ♊ ♋ ♌ ♍ ♎ ♏ ♐ ♑ ♒ ♓ ♔ ♕ ♖ ♗ ♘ ♙ ♚ ♛ ♜ ♝ ♞ ♟ ♠ ♡ ♢ ♣ ♤ ♥ ♦ ♧ ♨ ♩ ♪ ♫ ♬ ♭ ♮ ♯
|
||||
Dingbats
|
||||
✁ ✂ ✃ ✄ ✆ ✇ ✈ ✉ ✌ ✍ ✎ ✏ ✐ ✑ ✒ ✓ ✔ ✕ ✖ ✗ ✘ ✙ ✚ ✛ ✜ ✝ ✞ ✟ ✠ ✡ ✢ ✣ ✤ ✥ ✦ ✧ ✩ ✪ ✫ ✬ ✭ ✮ ✯ ✰ ✱ ✲ ✳ ✴ ✵ ✶ ✷ ✸ ✹ ✺ ✻ ✼ ✽ ✾ ✿ ❀ ❁ ❂ ❃ ❄ ❅ ❆ ❇ ❈ ❉ ❊ ❋ ❍ ❏ ❐ ❑ ❒ ❖ ❘ ❙ ❚ ❛ ❜ ❝ ❞ ❡ ❢ ❣ ❤ ❥ ❦ ❧ ❶ ❷ ❸ ❹ ❺ ❻ ❼ ❽ ❾ ❿ ➀ ➁ ➂ ➃ ➄ ➅ ➆ ➇ ➈ ➉ ➊ ➋ ➌ ➍ ➎ ➏ ➐ ➑ ➒ ➓ ➔ ➘ ➙ ➚ ➛ ➜ ➝ ...
|
||||
CJK Symbols and Punctuation
|
||||
、 。 〃 〄 々 〆 〇 〈 〉 《 》 「 」 『 』 【 】 〒 〓 〔 〕 〖 〗 〘 〙 〚 〛 〜 〝 〞 〟 〠 〡 〢 〣 〤 〥 〦 〧 〨 〩 〪 〫 〬 〭 〮 〯 〰 〱 〲 〳 〴 〵 〶 〷 〿
|
||||
Hiragana
|
||||
ぁ あ ぃ い ぅ う ぇ え ぉ お か が き ぎ く ぐ け げ こ ご さ ざ し じ す ず せ ぜ そ ぞ た だ ち ぢ っ つ づ て で と ど な に ぬ ね の は ば ぱ ひ び ぴ ふ ぶ ぷ へ べ ぺ ほ ぼ ぽ ま み む め も ゃ や ゅ ゆ ょ よ ら り る れ ろ ゎ わ ゐ ゑ を ん ゔ ゙ ゚ ゛ ゜ ゝ ゞ
|
||||
Katakana
|
||||
ァ ア ィ イ ゥ ウ ェ エ ォ オ カ ガ キ ギ ク グ ケ ゲ コ ゴ サ ザ シ ジ ス ズ セ ゼ ソ ゾ タ ダ チ ヂ ッ ツ ヅ テ デ ト ド ナ ニ ヌ ネ ノ ハ バ パ ヒ ビ ピ フ ブ プ ヘ ベ ペ ホ ボ ポ マ ミ ム メ モ ャ ヤ ュ ユ ョ ヨ ラ リ ル レ ロ ヮ ワ ヰ ヱ ヲ ン ヴ ヵ ヶ ヷ ヸ ヹ ヺ ・ ー ヽ ヾ
|
||||
Bopomofo
|
||||
ㄅ ㄆ ㄇ ㄈ ㄉ ㄊ ㄋ ㄌ ㄍ ㄎ ㄏ ㄐ ㄑ ㄒ ㄓ ㄔ ㄕ ㄖ ㄗ ㄘ ㄙ ㄚ ㄛ ㄜ ㄝ ㄞ ㄟ ㄠ ㄡ ㄢ ㄣ ㄤ ㄥ ㄦ ㄧ ㄨ ㄩ ㄪ ㄫ ㄬ
|
||||
Hangul Compatibility Jamo
|
||||
ㄱ ㄲ ㄳ ㄴ ㄵ ㄶ ㄷ ㄸ ㄹ ㄺ ㄻ ㄼ ㄽ ㄾ ㄿ ㅀ ㅁ ㅂ ㅃ ㅄ ㅅ ㅆ ㅇ ㅈ ㅉ ㅊ ㅋ ㅌ ㅍ ㅎ ㅏ ㅐ ㅑ ㅒ ㅓ ㅔ ㅕ ㅖ ㅗ ㅘ ㅙ ㅚ ㅛ ㅜ ㅝ ㅞ ㅟ ㅠ ㅡ ㅢ ㅣ ㅤ ㅥ ㅦ ㅧ ㅨ ㅩ ㅪ ㅫ ㅬ ㅭ ㅮ ㅯ ㅰ ㅱ ㅲ ㅳ ㅴ ㅵ ㅶ ㅷ ㅸ ㅹ ㅺ ㅻ ㅼ ㅽ ㅾ ㅿ ㆀ ㆁ ㆂ ㆃ ㆄ ㆅ ㆆ ㆇ ㆈ ㆉ ㆊ ㆋ ㆌ ㆍ ㆎ
|
||||
Kanbun
|
||||
㆐ ㆑ ㆒ ㆓ ㆔ ㆕ ㆖ ㆗ ㆘ ㆙ ㆚ ㆛ ㆜ ㆝ ㆞ ㆟
|
||||
Enclosed CJK Letters and Months
|
||||
㈀ ㈁ ㈂ ㈃ ㈄ ㈅ ㈆ ㈇ ㈈ ㈉ ㈊ ㈋ ㈌ ㈍ ㈎ ㈏ ㈐ ㈑ ㈒ ㈓ ㈔ ㈕ ㈖ ㈗ ㈘ ㈙ ㈚ ㈛ ㈜ ㈠ ㈡ ㈢ ㈣ ㈤ ㈥ ㈦ ㈧ ㈨ ㈩ ㈪ ㈫ ㈬ ㈭ ㈮ ㈯ ㈰ ㈱ ㈲ ㈳ ㈴ ㈵ ㈶ ㈷ ㈸ ㈹ ㈺ ㈻ ㈼ ㈽ ㈾ ㈿ ㉀ ㉁ ㉂ ㉃ ㉠ ㉡ ㉢ ㉣ ㉤ ㉥ ㉦ ㉧ ㉨ ㉩ ㉪ ㉫ ㉬ ㉭ ㉮ ㉯ ㉰ ㉱ ㉲ ㉳ ㉴ ㉵ ㉶ ㉷ ㉸ ㉹ ㉺ ㉻ ㉿ ㊀ ㊁ ㊂ ㊃ ㊄ ㊅ ㊆ ㊇ ㊈ ㊉ ㊊ ㊋ ㊌ ㊍ ㊎ ㊏ ㊐ ㊑ ㊒ ㊓ ㊔ ㊕ ㊖ ㊗ ㊘ ㊙ ㊚ ㊛ ㊜ ㊝ ㊞ ㊟ ㊠ ㊡ ...
|
||||
CJK Compatibility
|
||||
㌀ ㌁ ㌂ ㌃ ㌄ ㌅ ㌆ ㌇ ㌈ ㌉ ㌊ ㌋ ㌌ ㌍ ㌎ ㌏ ㌐ ㌑ ㌒ ㌓ ㌔ ㌕ ㌖ ㌗ ㌘ ㌙ ㌚ ㌛ ㌜ ㌝ ㌞ ㌟ ㌠ ㌡ ㌢ ㌣ ㌤ ㌥ ㌦ ㌧ ㌨ ㌩ ㌪ ㌫ ㌬ ㌭ ㌮ ㌯ ㌰ ㌱ ㌲ ㌳ ㌴ ㌵ ㌶ ㌷ ㌸ ㌹ ㌺ ㌻ ㌼ ㌽ ㌾ ㌿ ㍀ ㍁ ㍂ ㍃ ㍄ ㍅ ㍆ ㍇ ㍈ ㍉ ㍊ ㍋ ㍌ ㍍ ㍎ ㍏ ㍐ ㍑ ㍒ ㍓ ㍔ ㍕ ㍖ ㍗ ㍘ ㍙ ㍚ ㍛ ㍜ ㍝ ㍞ ㍟ ㍠ ㍡ ㍢ ㍣ ㍤ ㍥ ㍦ ㍧ ㍨ ㍩ ㍪ ㍫ ㍬ ㍭ ㍮ ㍯ ㍰ ㍱ ㍲ ㍳ ㍴ ㍵ ㍶ ㍻ ㍼ ㍽ ㍾ ㍿ ㎀ ㎁ ㎂ ㎃ ...
|
||||
CJK Unified Ideographs
|
||||
一 丁 丂 七 丄 丅 丆 万 丈 三 上 下 丌 不 与 丏 丐 丑 丒 专 且 丕 世 丗 丘 丙 业 丛 东 丝 丞 丟 丠 両 丢 丣 两 严 並 丧 丨 丩 个 丫 丬 中 丮 丯 丰 丱 串 丳 临 丵 丶 丷 丸 丹 为 主 丼 丽 举 丿 乀 乁 乂 乃 乄 久 乆 乇 么 义 乊 之 乌 乍 乎 乏 乐 乑 乒 乓 乔 乕 乖 乗 乘 乙 乚 乛 乜 九 乞 也 习 乡 乢 乣 乤 乥 书 乧 乨 乩 乪 乫 乬 乭 乮 乯 买 乱 乲 乳 乴 乵 乶 乷 乸 乹 乺 乻 乼 乽 乾 乿 ...
|
||||
Hangul Syllables
|
||||
가 각 갂 갃 간 갅 갆 갇 갈 갉 갊 갋 갌 갍 갎 갏 감 갑 값 갓 갔 강 갖 갗 갘 같 갚 갛 개 객 갞 갟 갠 갡 갢 갣 갤 갥 갦 갧 갨 갩 갪 갫 갬 갭 갮 갯 갰 갱 갲 갳 갴 갵 갶 갷 갸 갹 갺 갻 갼 갽 갾 갿 걀 걁 걂 걃 걄 걅 걆 걇 걈 걉 걊 걋 걌 걍 걎 걏 걐 걑 걒 걓 걔 걕 걖 걗 걘 걙 걚 걛 걜 걝 걞 걟 걠 걡 걢 걣 걤 걥 걦 걧 걨 걩 걪 걫 걬 걭 걮 걯 거 걱 걲 걳 건 걵 걶 걷 걸 걹 걺 걻 걼 걽 걾 걿 ...
|
||||
Private Use
|
||||
...
|
||||
CJK Compatibility Ideographs
|
||||
豈 更 車 賈 滑 串 句 龜 龜 契 金 喇 奈 懶 癩 羅 蘿 螺 裸 邏 樂 洛 烙 珞 落 酪 駱 亂 卵 欄 爛 蘭 鸞 嵐 濫 藍 襤 拉 臘 蠟 廊 朗 浪 狼 郎 來 冷 勞 擄 櫓 爐 盧 老 蘆 虜 路 露 魯 鷺 碌 祿 綠 菉 錄 鹿 論 壟 弄 籠 聾 牢 磊 賂 雷 壘 屢 樓 淚 漏 累 縷 陋 勒 肋 凜 凌 稜 綾 菱 陵 讀 拏 樂 諾 丹 寧 怒 率 異 北 磻 便 復 不 泌 數 索 參 塞 省 葉 說 殺 辰 沈 拾 若 掠 略 亮 兩 凉 梁 糧 良 諒 量 勵 ...
|
||||
Alphabetic Presentation Forms
|
||||
ff fi fl ffi ffl ſt st ﬓ ﬔ ﬕ ﬖ ﬗ ﬞ ײַ ﬠ ﬡ ﬢ ﬣ ﬤ ﬥ ﬦ ﬧ ﬨ ﬩ שׁ שׂ שּׁ שּׂ אַ אָ אּ בּ גּ דּ הּ וּ זּ טּ יּ ךּ כּ לּ מּ נּ סּ ףּ פּ צּ קּ רּ שּ תּ וֹ בֿ כֿ פֿ ﭏ
|
||||
Arabic Presentation Forms-A
|
||||
ﭐ ﭑ ﭒ ﭓ ﭔ ﭕ ﭖ ﭗ ﭘ ﭙ ﭚ ﭛ ﭜ ﭝ ﭞ ﭟ ﭠ ﭡ ﭢ ﭣ ﭤ ﭥ ﭦ ﭧ ﭨ ﭩ ﭪ ﭫ ﭬ ﭭ ﭮ ﭯ ﭰ ﭱ ﭲ ﭳ ﭴ ﭵ ﭶ ﭷ ﭸ ﭹ ﭺ ﭻ ﭼ ﭽ ﭾ ﭿ ﮀ ﮁ ﮂ ﮃ ﮄ ﮅ ﮆ ﮇ ﮈ ﮉ ﮊ ﮋ ﮌ ﮍ ﮎ ﮏ ﮐ ﮑ ﮒ ﮓ ﮔ ﮕ ﮖ ﮗ ﮘ ﮙ ﮚ ﮛ ﮜ ﮝ ﮞ ﮟ ﮠ ﮡ ﮢ ﮣ ﮤ ﮥ ﮦ ﮧ ﮨ ﮩ ﮪ ﮫ ﮬ ﮭ ﮮ ﮯ ﮰ ﮱ ﯓ ﯔ ﯕ ﯖ ﯗ ﯘ ﯙ ﯚ ﯛ ﯜ ﯝ ﯞ ﯟ ﯠ ﯡ ﯢ ﯣ ﯤ ﯥ ﯦ ﯧ ﯨ ﯩ ﯪ ﯫ ﯬ ﯭ ﯮ ﯯ ﯰ ...
|
||||
Combining Half Marks
|
||||
︠ ︡ ︢ ︣
|
||||
CJK Compatibility Forms
|
||||
︰ ︱ ︲ ︳ ︴ ︵ ︶ ︷ ︸ ︹ ︺ ︻ ︼ ︽ ︾ ︿ ﹀ ﹁ ﹂ ﹃ ﹄ ﹉ ﹊ ﹋ ﹌ ﹍ ﹎ ﹏
|
||||
Small Form Variants
|
||||
﹐ ﹑ ﹒ ﹔ ﹕ ﹖ ﹗ ﹘ ﹙ ﹚ ﹛ ﹜ ﹝ ﹞ ﹟ ﹠ ﹡ ﹢ ﹣ ﹤ ﹥ ﹦ ﹨ ﹩ ﹪ ﹫
|
||||
Arabic Presentation Forms-B
|
||||
ﹰ ﹱ ﹲ ﹴ ﹶ ﹷ ﹸ ﹹ ﹺ ﹻ ﹼ ﹽ ﹾ ﹿ ﺀ ﺁ ﺂ ﺃ ﺄ ﺅ ﺆ ﺇ ﺈ ﺉ ﺊ ﺋ ﺌ ﺍ ﺎ ﺏ ﺐ ﺑ ﺒ ﺓ ﺔ ﺕ ﺖ ﺗ ﺘ ﺙ ﺚ ﺛ ﺜ ﺝ ﺞ ﺟ ﺠ ﺡ ﺢ ﺣ ﺤ ﺥ ﺦ ﺧ ﺨ ﺩ ﺪ ﺫ ﺬ ﺭ ﺮ ﺯ ﺰ ﺱ ﺲ ﺳ ﺴ ﺵ ﺶ ﺷ ﺸ ﺹ ﺺ ﺻ ﺼ ﺽ ﺾ ﺿ ﻀ ﻁ ﻂ ﻃ ﻄ ﻅ ﻆ ﻇ ﻈ ﻉ ﻊ ﻋ ﻌ ﻍ ﻎ ﻏ ﻐ ﻑ ﻒ ﻓ ﻔ ﻕ ﻖ ﻗ ﻘ ﻙ ﻚ ﻛ ﻜ ﻝ ﻞ ﻟ ﻠ ﻡ ﻢ ﻣ ﻤ ﻥ ﻦ ﻧ ﻨ ﻩ ﻪ ﻫ ﻬ ﻭ ﻮ ﻯ ﻰ ﻱ ...
|
||||
Halfwidth and Fullwidth Forms
|
||||
! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~ 。 「 」 、 ・ ヲ ァ ィ ゥ ェ ォ ャ ュ ョ ッ ー ア イ ウ エ オ カ キ ク ケ コ サ シ ス セ ソ タ チ ツ ...
|
||||
Specials
|
||||
|
||||
Specials
|
||||
<20>
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests
|
||||
|
||||
import junit.framework.TestCase
|
||||
|
||||
import com.kunzisoft.keepass.database.element.PwDate
|
||||
import org.junit.Assert
|
||||
|
||||
class PwDateTest : TestCase() {
|
||||
|
||||
fun testDate() {
|
||||
val jDate = PwDate(System.currentTimeMillis())
|
||||
val intermediate = PwDate(jDate)
|
||||
val cDate = PwDate(intermediate.byteArrayDate!!, 0)
|
||||
|
||||
Assert.assertTrue("jDate and intermediate not equal", jDate == intermediate)
|
||||
Assert.assertTrue("jDate and cDate not equal", cDate == jDate)
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.crypto
|
||||
|
||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||
|
||||
import junit.framework.TestCase
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.Random
|
||||
|
||||
import javax.crypto.BadPaddingException
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.IllegalBlockSizeException
|
||||
import javax.crypto.NoSuchPaddingException
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
|
||||
class AESTest : TestCase() {
|
||||
|
||||
private val mRand = Random()
|
||||
|
||||
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, IllegalBlockSizeException::class, BadPaddingException::class, InvalidAlgorithmParameterException::class)
|
||||
fun testEncrypt() {
|
||||
// Test above below and at the blocksize
|
||||
testFinal(15)
|
||||
testFinal(16)
|
||||
testFinal(17)
|
||||
|
||||
// Test random larger sizes
|
||||
val size = mRand.nextInt(494) + 18
|
||||
testFinal(size)
|
||||
}
|
||||
|
||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, IllegalBlockSizeException::class, BadPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
||||
private fun testFinal(dataSize: Int) {
|
||||
|
||||
// Generate some input
|
||||
val input = ByteArray(dataSize)
|
||||
mRand.nextBytes(input)
|
||||
|
||||
// Generate key
|
||||
val keyArray = ByteArray(32)
|
||||
mRand.nextBytes(keyArray)
|
||||
val key = SecretKeySpec(keyArray, "AES")
|
||||
|
||||
// Generate IV
|
||||
val ivArray = ByteArray(16)
|
||||
mRand.nextBytes(ivArray)
|
||||
val iv = IvParameterSpec(ivArray)
|
||||
|
||||
val android = CipherFactory.getInstance("AES/CBC/PKCS5Padding", true)
|
||||
android.init(Cipher.ENCRYPT_MODE, key, iv)
|
||||
val outAndroid = android.doFinal(input, 0, dataSize)
|
||||
|
||||
val nat = CipherFactory.getInstance("AES/CBC/PKCS5Padding")
|
||||
nat.init(Cipher.ENCRYPT_MODE, key, iv)
|
||||
val outNative = nat.doFinal(input, 0, dataSize)
|
||||
|
||||
assertArrayEquals("Arrays differ on size: $dataSize", outAndroid, outNative)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,63 +1,50 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.crypto
|
||||
|
||||
import com.kunzisoft.keepass.utils.readBytesLength
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
|
||||
import org.junit.Test
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.Random
|
||||
|
||||
import javax.crypto.BadPaddingException
|
||||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.CipherInputStream
|
||||
import javax.crypto.CipherOutputStream
|
||||
import javax.crypto.IllegalBlockSizeException
|
||||
import javax.crypto.NoSuchPaddingException
|
||||
|
||||
import junit.framework.TestCase
|
||||
|
||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||
import com.kunzisoft.keepass.crypto.engine.AesEngine
|
||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
||||
import com.kunzisoft.keepass.stream.BetterCipherInputStream
|
||||
import com.kunzisoft.keepass.stream.LEDataInputStream
|
||||
|
||||
class CipherTest : TestCase() {
|
||||
class EncryptionTest {
|
||||
private val rand = Random()
|
||||
|
||||
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidAlgorithmParameterException::class, IllegalBlockSizeException::class, BadPaddingException::class)
|
||||
@Test
|
||||
fun testCipherFactory() {
|
||||
val key = ByteArray(32)
|
||||
rand.nextBytes(key)
|
||||
|
||||
val iv = ByteArray(16)
|
||||
rand.nextBytes(iv)
|
||||
|
||||
val plaintext = ByteArray(1024)
|
||||
|
||||
rand.nextBytes(key)
|
||||
rand.nextBytes(iv)
|
||||
rand.nextBytes(plaintext)
|
||||
|
||||
val aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID)
|
||||
val aes = EncryptionAlgorithm.AESRijndael.cipherEngine
|
||||
val encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv)
|
||||
val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv)
|
||||
|
||||
@@ -67,20 +54,20 @@ class CipherTest : TestCase() {
|
||||
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext)
|
||||
}
|
||||
|
||||
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidAlgorithmParameterException::class, IllegalBlockSizeException::class, BadPaddingException::class, IOException::class)
|
||||
@Test
|
||||
fun testCipherStreams() {
|
||||
val MESSAGE_LENGTH = 1024
|
||||
val length = 1024
|
||||
|
||||
val key = ByteArray(32)
|
||||
val iv = ByteArray(16)
|
||||
|
||||
val plaintext = ByteArray(MESSAGE_LENGTH)
|
||||
|
||||
rand.nextBytes(key)
|
||||
|
||||
val iv = ByteArray(16)
|
||||
rand.nextBytes(iv)
|
||||
|
||||
val plaintext = ByteArray(length)
|
||||
rand.nextBytes(plaintext)
|
||||
|
||||
val aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID)
|
||||
val aes = EncryptionAlgorithm.AESRijndael.cipherEngine
|
||||
val encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv)
|
||||
val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv)
|
||||
|
||||
@@ -92,10 +79,9 @@ class CipherTest : TestCase() {
|
||||
val secrettext = bos.toByteArray()
|
||||
|
||||
val bis = ByteArrayInputStream(secrettext)
|
||||
val cis = BetterCipherInputStream(bis, decrypt)
|
||||
val lis = LEDataInputStream(cis)
|
||||
val cis = CipherInputStream(bis, decrypt)
|
||||
|
||||
val decrypttext = lis.readBytes(MESSAGE_LENGTH)
|
||||
val decrypttext = cis.readBytesLength(length)
|
||||
|
||||
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext)
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.crypto
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
|
||||
import java.io.IOException
|
||||
import java.util.Random
|
||||
|
||||
import junit.framework.TestCase
|
||||
|
||||
import com.kunzisoft.keepass.crypto.finalkey.AndroidFinalKey
|
||||
import com.kunzisoft.keepass.crypto.finalkey.NativeFinalKey
|
||||
|
||||
class FinalKeyTest : TestCase() {
|
||||
private var mRand: Random? = null
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
|
||||
mRand = Random()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun testNativeAndroid() {
|
||||
// Test both an old and an even number to test my flip variable
|
||||
testNativeFinalKey(5)
|
||||
testNativeFinalKey(6)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun testNativeFinalKey(rounds: Int) {
|
||||
val seed = ByteArray(32)
|
||||
val key = ByteArray(32)
|
||||
val nativeKey: ByteArray
|
||||
val androidKey: ByteArray
|
||||
|
||||
mRand!!.nextBytes(seed)
|
||||
mRand!!.nextBytes(key)
|
||||
|
||||
val aKey = AndroidFinalKey()
|
||||
androidKey = aKey.transformMasterKey(seed, key, rounds.toLong())
|
||||
|
||||
val nKey = NativeFinalKey()
|
||||
nativeKey = nKey.transformMasterKey(seed, key, rounds.toLong())
|
||||
|
||||
assertArrayEquals("Does not match", androidKey, nativeKey)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package com.kunzisoft.keepass.tests.stream
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.kunzisoft.keepass.utils.readAllBytes
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryCache
|
||||
import com.kunzisoft.keepass.database.element.binary.BinaryFile
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import org.junit.Test
|
||||
import java.io.DataInputStream
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import kotlin.random.Random
|
||||
|
||||
class BinaryDataTest {
|
||||
|
||||
private val context: Context by lazy {
|
||||
InstrumentationRegistry.getInstrumentation().context
|
||||
}
|
||||
|
||||
private val cacheDirectory = UriUtil.getBinaryDir(InstrumentationRegistry.getInstrumentation().targetContext)
|
||||
private val fileA = File(cacheDirectory, TEST_FILE_CACHE_A)
|
||||
private val fileB = File(cacheDirectory, TEST_FILE_CACHE_B)
|
||||
private val fileC = File(cacheDirectory, TEST_FILE_CACHE_C)
|
||||
|
||||
private val binaryCache = BinaryCache()
|
||||
|
||||
private fun saveBinary(asset: String, binaryData: BinaryFile) {
|
||||
context.assets.open(asset).use { assetInputStream ->
|
||||
binaryData.getOutputDataStream(binaryCache).use { binaryOutputStream ->
|
||||
assetInputStream.readAllBytes(DEFAULT_BUFFER_SIZE) { buffer ->
|
||||
binaryOutputStream.write(buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSaveTextInCache() {
|
||||
val binaryA = BinaryFile(fileA)
|
||||
val binaryB = BinaryFile(fileB)
|
||||
saveBinary(TEST_TEXT_ASSET, binaryA)
|
||||
saveBinary(TEST_TEXT_ASSET, binaryB)
|
||||
assertEquals("Save text binary length failed.", binaryA.getSize(), binaryB.getSize())
|
||||
assertEquals("Save text binary MD5 failed.", binaryA.binaryHash(), binaryB.binaryHash())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSaveImageInCache() {
|
||||
val binaryA = BinaryFile(fileA)
|
||||
val binaryB = BinaryFile(fileB)
|
||||
saveBinary(TEST_IMAGE_ASSET, binaryA)
|
||||
saveBinary(TEST_IMAGE_ASSET, binaryB)
|
||||
assertEquals("Save image binary length failed.", binaryA.getSize(), binaryB.getSize())
|
||||
assertEquals("Save image binary failed.", binaryA.binaryHash(), binaryB.binaryHash())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCompressText() {
|
||||
val binaryA = BinaryFile(fileA)
|
||||
val binaryB = BinaryFile(fileB)
|
||||
val binaryC = BinaryFile(fileC)
|
||||
saveBinary(TEST_TEXT_ASSET, binaryA)
|
||||
saveBinary(TEST_TEXT_ASSET, binaryB)
|
||||
saveBinary(TEST_TEXT_ASSET, binaryC)
|
||||
binaryA.compress(binaryCache)
|
||||
binaryB.compress(binaryCache)
|
||||
assertEquals("Compress text length failed.", binaryA.getSize(), binaryB.getSize())
|
||||
assertEquals("Compress text MD5 failed.", binaryA.binaryHash(), binaryB.binaryHash())
|
||||
binaryB.decompress(binaryCache)
|
||||
assertEquals("Decompress text length failed.", binaryB.getSize(), binaryC.getSize())
|
||||
assertEquals("Decompress text MD5 failed.", binaryB.binaryHash(), binaryC.binaryHash())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCompressImage() {
|
||||
val binaryA = BinaryFile(fileA)
|
||||
var binaryB = BinaryFile(fileB)
|
||||
val binaryC = BinaryFile(fileC)
|
||||
saveBinary(TEST_IMAGE_ASSET, binaryA)
|
||||
saveBinary(TEST_IMAGE_ASSET, binaryB)
|
||||
saveBinary(TEST_IMAGE_ASSET, binaryC)
|
||||
binaryA.compress(binaryCache)
|
||||
binaryB.compress(binaryCache)
|
||||
assertEquals("Compress image length failed.", binaryA.getSize(), binaryA.getSize())
|
||||
assertEquals("Compress image failed.", binaryA.binaryHash(), binaryA.binaryHash())
|
||||
binaryB = BinaryFile(fileB, true)
|
||||
binaryB.decompress(binaryCache)
|
||||
assertEquals("Decompress image length failed.", binaryB.getSize(), binaryC.getSize())
|
||||
assertEquals("Decompress image failed.", binaryB.binaryHash(), binaryC.binaryHash())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCompressBytes() {
|
||||
// Test random byte array
|
||||
val byteArray = ByteArray(50)
|
||||
Random.nextBytes(byteArray)
|
||||
testCompressBytes(byteArray)
|
||||
|
||||
// Test empty byte array
|
||||
testCompressBytes(ByteArray(0))
|
||||
}
|
||||
|
||||
private fun testCompressBytes(byteArray: ByteArray) {
|
||||
val binaryA = binaryCache.getBinaryData("0", true)
|
||||
binaryA.getOutputDataStream(binaryCache).use { outputStream ->
|
||||
outputStream.write(byteArray)
|
||||
}
|
||||
val binaryB = binaryCache.getBinaryData("1", true)
|
||||
binaryB.getOutputDataStream(binaryCache).use { outputStream ->
|
||||
outputStream.write(byteArray)
|
||||
}
|
||||
val binaryC = binaryCache.getBinaryData("2", true)
|
||||
binaryC.getOutputDataStream(binaryCache).use { outputStream ->
|
||||
outputStream.write(byteArray)
|
||||
}
|
||||
binaryA.compress(binaryCache)
|
||||
binaryB.compress(binaryCache)
|
||||
assertEquals("Compress bytes decompressed failed.", binaryA.isCompressed, true)
|
||||
assertEquals("Compress bytes length failed.", binaryA.getSize(), binaryA.getSize())
|
||||
assertEquals("Compress bytes failed.", binaryA.binaryHash(), binaryA.binaryHash())
|
||||
binaryB.decompress(binaryCache)
|
||||
assertEquals("Decompress bytes decompressed failed.", binaryB.isCompressed, false)
|
||||
assertEquals("Decompress bytes length failed.", binaryB.getSize(), binaryC.getSize())
|
||||
assertEquals("Decompress bytes failed.", binaryB.binaryHash(), binaryC.binaryHash())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReadText() {
|
||||
val binaryA = BinaryFile(fileA)
|
||||
saveBinary(TEST_TEXT_ASSET, binaryA)
|
||||
assert(streamAreEquals(context.assets.open(TEST_TEXT_ASSET),
|
||||
binaryA.getInputDataStream(binaryCache)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReadImage() {
|
||||
val binaryA = BinaryFile(fileA)
|
||||
saveBinary(TEST_IMAGE_ASSET, binaryA)
|
||||
assert(streamAreEquals(context.assets.open(TEST_IMAGE_ASSET),
|
||||
binaryA.getInputDataStream(binaryCache)))
|
||||
}
|
||||
|
||||
private fun streamAreEquals(inputStreamA: InputStream,
|
||||
inputStreamB: InputStream): Boolean {
|
||||
val bufferA = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
val bufferB = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
val dataInputStreamB = DataInputStream(inputStreamB)
|
||||
try {
|
||||
var len: Int
|
||||
while (inputStreamA.read(bufferA).also { len = it } > 0) {
|
||||
dataInputStreamB.readFully(bufferB, 0, len)
|
||||
for (i in 0 until len) {
|
||||
if (bufferA[i] != bufferB[i])
|
||||
return false
|
||||
}
|
||||
}
|
||||
return inputStreamB.read() < 0 // is the end of the second file also.
|
||||
} catch (e: Exception) {
|
||||
return false
|
||||
}
|
||||
finally {
|
||||
inputStreamA.close()
|
||||
inputStreamB.close()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TEST_FILE_CACHE_A = "testA"
|
||||
private const val TEST_FILE_CACHE_B = "testB"
|
||||
private const val TEST_FILE_CACHE_C = "testC"
|
||||
private const val TEST_IMAGE_ASSET = "test_image.png"
|
||||
private const val TEST_TEXT_ASSET = "test_text.txt"
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.stream
|
||||
@@ -33,7 +33,7 @@ import junit.framework.TestCase
|
||||
import com.kunzisoft.keepass.stream.HashedBlockInputStream
|
||||
import com.kunzisoft.keepass.stream.HashedBlockOutputStream
|
||||
|
||||
class HashedBlock : TestCase() {
|
||||
class HashedBlockTest : TestCase() {
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun testBlockAligned() {
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.utils
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
import com.kunzisoft.keepass.utils.StringUtil
|
||||
|
||||
import junit.framework.TestCase
|
||||
|
||||
class StringUtilTest : TestCase() {
|
||||
private val text = "AbCdEfGhIj"
|
||||
private val search = "BcDe"
|
||||
private val badSearch = "Ed"
|
||||
|
||||
private val repText = "AbCtestingaBc"
|
||||
private val repSearch = "ABc"
|
||||
private val repSearchBad = "CCCCCC"
|
||||
private val repNew = "12345"
|
||||
private val repResult = "12345testing12345"
|
||||
|
||||
fun testIndexOfIgnoreCase1() {
|
||||
assertEquals(1, StringUtil.indexOfIgnoreCase(text, search, Locale.ENGLISH))
|
||||
}
|
||||
|
||||
fun testIndexOfIgnoreCase2() {
|
||||
assertEquals(-1f, StringUtil.indexOfIgnoreCase(text, search, Locale.ENGLISH).toFloat(), 2f)
|
||||
}
|
||||
|
||||
fun testIndexOfIgnoreCase3() {
|
||||
assertEquals(-1, StringUtil.indexOfIgnoreCase(text, badSearch, Locale.ENGLISH))
|
||||
}
|
||||
|
||||
fun testReplaceAllIgnoresCase1() {
|
||||
assertEquals(repResult, StringUtil.replaceAllIgnoresCase(repText, repSearch, repNew, Locale.ENGLISH))
|
||||
}
|
||||
|
||||
fun testReplaceAllIgnoresCase2() {
|
||||
assertEquals(repText, StringUtil.replaceAllIgnoresCase(repText, repSearchBad, repNew, Locale.ENGLISH))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.kunzisoft.keepass.tests.utils
|
||||
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import junit.framework.TestCase
|
||||
import java.util.*
|
||||
|
||||
class UUIDTest: TestCase() {
|
||||
|
||||
fun testUUID() {
|
||||
val randomUUID = UUID.randomUUID()
|
||||
val hexStringUUID = UuidUtil.toHexString(randomUUID)
|
||||
val retrievedUUID = UuidUtil.fromHexString(hexStringUUID)
|
||||
assertEquals(randomUUID, retrievedUUID)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.kunzisoft.keepass.tests.utils
|
||||
|
||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||
import junit.framework.TestCase
|
||||
|
||||
class UnsignedIntTest: TestCase() {
|
||||
|
||||
fun testUInt() {
|
||||
val standardInt = UnsignedInt(15).toKotlinInt()
|
||||
assertEquals(15, standardInt)
|
||||
val unsignedInt = UnsignedInt(-1).toKotlinLong()
|
||||
assertEquals(4294967295L, unsignedInt)
|
||||
}
|
||||
|
||||
fun testMaxValue() {
|
||||
val maxValue = UnsignedInt.MAX_VALUE.toKotlinLong()
|
||||
assertEquals(4294967295L, maxValue)
|
||||
val longValue = UnsignedInt.fromKotlinLong(4294967295L).toKotlinLong()
|
||||
assertEquals(longValue, maxValue)
|
||||
}
|
||||
|
||||
fun testLong() {
|
||||
val longValue = UnsignedInt.fromKotlinLong(50L).toKotlinInt()
|
||||
assertEquals(50, longValue)
|
||||
val uIntLongValue = UnsignedInt.fromKotlinLong(4294967290).toKotlinLong()
|
||||
assertEquals(4294967290, uIntLongValue)
|
||||
}
|
||||
}
|
||||
@@ -1,49 +1,43 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.Calendar
|
||||
import java.util.Random
|
||||
package com.kunzisoft.keepass.tests.utils
|
||||
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
import junit.framework.TestCase
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.*
|
||||
|
||||
import com.kunzisoft.keepass.database.element.PwDate
|
||||
import com.kunzisoft.keepass.stream.LEDataInputStream
|
||||
import com.kunzisoft.keepass.stream.LEDataOutputStream
|
||||
import com.kunzisoft.keepass.utils.Types
|
||||
|
||||
class TypesTest : TestCase() {
|
||||
class ValuesTest : TestCase() {
|
||||
|
||||
fun testReadWriteLongZero() {
|
||||
testReadWriteLong(0.toByte())
|
||||
}
|
||||
|
||||
fun testReadWriteLongMax() {
|
||||
testReadWriteLong(java.lang.Byte.MAX_VALUE)
|
||||
testReadWriteLong(Byte.MAX_VALUE)
|
||||
}
|
||||
|
||||
fun testReadWriteLongMin() {
|
||||
testReadWriteLong(java.lang.Byte.MIN_VALUE)
|
||||
testReadWriteLong(Byte.MIN_VALUE)
|
||||
}
|
||||
|
||||
fun testReadWriteLongRnd() {
|
||||
@@ -56,15 +50,9 @@ class TypesTest : TestCase() {
|
||||
|
||||
private fun testReadWriteLong(value: Byte) {
|
||||
val orig = ByteArray(8)
|
||||
val dest = ByteArray(8)
|
||||
|
||||
setArray(orig, value, 0, 8)
|
||||
|
||||
val one = LEDataInputStream.readLong(orig, 0)
|
||||
LEDataOutputStream.writeLong(one, dest, 0)
|
||||
|
||||
assertArrayEquals(orig, dest)
|
||||
setArray(orig, value, 8)
|
||||
|
||||
assertArrayEquals(orig, uLongTo8Bytes(bytes64ToULong(orig)))
|
||||
}
|
||||
|
||||
fun testReadWriteIntZero() {
|
||||
@@ -72,33 +60,30 @@ class TypesTest : TestCase() {
|
||||
}
|
||||
|
||||
fun testReadWriteIntMin() {
|
||||
testReadWriteInt(java.lang.Byte.MIN_VALUE)
|
||||
testReadWriteInt(Byte.MIN_VALUE)
|
||||
}
|
||||
|
||||
fun testReadWriteIntMax() {
|
||||
testReadWriteInt(java.lang.Byte.MAX_VALUE)
|
||||
testReadWriteInt(Byte.MAX_VALUE)
|
||||
}
|
||||
|
||||
private fun testReadWriteInt(value: Byte) {
|
||||
val orig = ByteArray(4)
|
||||
val dest = ByteArray(4)
|
||||
|
||||
for (i in 0..3) {
|
||||
orig[i] = 0
|
||||
}
|
||||
|
||||
setArray(orig, value, 0, 4)
|
||||
setArray(orig, value, 4)
|
||||
|
||||
val one = LEDataInputStream.readInt(orig, 0)
|
||||
|
||||
LEDataOutputStream.writeInt(one, dest, 0)
|
||||
val one = bytes4ToUInt(orig)
|
||||
val dest = uIntTo4Bytes(one)
|
||||
|
||||
assertArrayEquals(orig, dest)
|
||||
|
||||
}
|
||||
|
||||
private fun setArray(buf: ByteArray, value: Byte, offset: Int, size: Int) {
|
||||
for (i in offset until offset + size) {
|
||||
private fun setArray(buf: ByteArray, value: Byte, size: Int) {
|
||||
for (i in 0 until size) {
|
||||
buf[i] = value
|
||||
}
|
||||
}
|
||||
@@ -109,32 +94,28 @@ class TypesTest : TestCase() {
|
||||
orig[0] = 0
|
||||
orig[1] = 1
|
||||
|
||||
val one = LEDataInputStream.readUShort(orig, 0)
|
||||
val dest = LEDataOutputStream.writeUShortBuf(one)
|
||||
val one = bytes2ToUShort(orig)
|
||||
val dest = uShortTo2Bytes(one)
|
||||
|
||||
assertArrayEquals(orig, dest)
|
||||
|
||||
}
|
||||
|
||||
fun testReadWriteShortMin() {
|
||||
testReadWriteShort(java.lang.Byte.MIN_VALUE)
|
||||
testReadWriteShort(Byte.MIN_VALUE)
|
||||
}
|
||||
|
||||
fun testReadWriteShortMax() {
|
||||
testReadWriteShort(java.lang.Byte.MAX_VALUE)
|
||||
testReadWriteShort(Byte.MAX_VALUE)
|
||||
}
|
||||
|
||||
private fun testReadWriteShort(value: Byte) {
|
||||
val orig = ByteArray(2)
|
||||
val dest = ByteArray(2)
|
||||
setArray(orig, value, 2)
|
||||
|
||||
setArray(orig, value, 0, 2)
|
||||
|
||||
val one = LEDataInputStream.readUShort(orig, 0)
|
||||
LEDataOutputStream.writeUShort(one, dest, 0)
|
||||
val one = bytes2ToUShort(orig)
|
||||
val dest = uShortTo2Bytes(one)
|
||||
|
||||
assertArrayEquals(orig, dest)
|
||||
|
||||
}
|
||||
|
||||
fun testReadWriteByteZero() {
|
||||
@@ -142,24 +123,16 @@ class TypesTest : TestCase() {
|
||||
}
|
||||
|
||||
fun testReadWriteByteMin() {
|
||||
testReadWriteByte(java.lang.Byte.MIN_VALUE)
|
||||
testReadWriteByte(Byte.MIN_VALUE)
|
||||
}
|
||||
|
||||
fun testReadWriteByteMax() {
|
||||
testReadWriteShort(java.lang.Byte.MAX_VALUE)
|
||||
testReadWriteShort(Byte.MAX_VALUE)
|
||||
}
|
||||
|
||||
private fun testReadWriteByte(value: Byte) {
|
||||
val orig = ByteArray(1)
|
||||
val dest = ByteArray(1)
|
||||
|
||||
setArray(orig, value, 0, 1)
|
||||
|
||||
val one = Types.readUByte(orig, 0)
|
||||
Types.writeUByte(one, dest, 0)
|
||||
|
||||
assertArrayEquals(orig, dest)
|
||||
|
||||
val dest: Byte = UnsignedInt(value.toInt() and 0xFF).toKotlinByte()
|
||||
assert(value == dest)
|
||||
}
|
||||
|
||||
fun testDate() {
|
||||
@@ -168,27 +141,35 @@ class TypesTest : TestCase() {
|
||||
val expected = Calendar.getInstance()
|
||||
expected.set(2008, 1, 2, 3, 4, 5)
|
||||
|
||||
val buf = PwDate.writeTime(expected.time, cal)
|
||||
val actual = Calendar.getInstance()
|
||||
actual.time = PwDate.readTime(buf, 0, cal)
|
||||
actual.time = DateInstant(bytes5ToDate(dateTo5Bytes(expected.time, cal), cal)).date
|
||||
|
||||
val jDate = DateInstant(System.currentTimeMillis())
|
||||
val intermediate = DateInstant(jDate)
|
||||
val cDate = DateInstant(bytes5ToDate(dateTo5Bytes(intermediate.date)))
|
||||
|
||||
assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR))
|
||||
assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH))
|
||||
assertEquals("Day mismatch: ", 1, actual.get(Calendar.DAY_OF_MONTH))
|
||||
assertEquals("Day mismatch: ", 2, actual.get(Calendar.DAY_OF_MONTH))
|
||||
assertEquals("Hour mismatch: ", 3, actual.get(Calendar.HOUR_OF_DAY))
|
||||
assertEquals("Minute mismatch: ", 4, actual.get(Calendar.MINUTE))
|
||||
assertEquals("Second mismatch: ", 5, actual.get(Calendar.SECOND))
|
||||
assertTrue("jDate and intermediate not equal", jDate == intermediate)
|
||||
assertTrue("jDate $jDate and cDate $cDate not equal", cDate == jDate)
|
||||
}
|
||||
|
||||
fun testUUID() {
|
||||
val rnd = Random()
|
||||
val bUUID = ByteArray(16)
|
||||
rnd.nextBytes(bUUID)
|
||||
Random().nextBytes(bUUID)
|
||||
|
||||
val uuid = Types.bytestoUUID(bUUID)
|
||||
val eUUID = Types.UUIDtoBytes(uuid)
|
||||
val uuid = bytes16ToUuid(bUUID)
|
||||
val eUUID = uuidTo16Bytes(uuid)
|
||||
|
||||
val lUUID = bytes16ToUuid(bUUID)
|
||||
val leUUID = uuidTo16Bytes(lUUID)
|
||||
|
||||
assertArrayEquals("UUID match failed", bUUID, eUUID)
|
||||
assertArrayEquals("UUID match failed", bUUID, leUUID)
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
@@ -198,12 +179,10 @@ class TypesTest : TestCase() {
|
||||
ulongBytes[i] = -1
|
||||
}
|
||||
|
||||
val bos = ByteArrayOutputStream()
|
||||
val leos = LEDataOutputStream(bos)
|
||||
leos.writeLong(Types.ULONG_MAX_VALUE)
|
||||
leos.close()
|
||||
|
||||
val uLongMax = bos.toByteArray()
|
||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||
byteArrayOutputStream.write(UnsignedLong.MAX_BYTES)
|
||||
byteArrayOutputStream.close()
|
||||
val uLongMax = byteArrayOutputStream.toByteArray()
|
||||
|
||||
assertArrayEquals(ulongBytes, uLongMax)
|
||||
}
|
||||
@@ -1,19 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108"
|
||||
android:width="108dp"
|
||||
android:height="108dp">
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108"
|
||||
android:width="108dp"
|
||||
android:height="108dp">
|
||||
<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:fillColor="@color/long_shadow"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeMiterLimit="4" />
|
||||
android:strokeMiterLimit="4" >
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endColor="#0000"
|
||||
android:endX="80"
|
||||
android:endY="80"
|
||||
android:startColor="#4e000000"
|
||||
android:startX="0"
|
||||
android:startY="0"
|
||||
android:type="linear"/>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</group>
|
||||
<group
|
||||
android:scaleX="0.3939503"
|
||||
|
||||
BIN
app/src/free/res/drawable/ic_launcher_foreground.png
Normal file
BIN
app/src/free/res/drawable/ic_launcher_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
@@ -1,19 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108"
|
||||
android:width="108dp"
|
||||
android:height="108dp">
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108"
|
||||
android:width="108dp"
|
||||
android:height="108dp">
|
||||
<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:fillColor="@color/long_shadow"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeMiterLimit="4" />
|
||||
android:strokeMiterLimit="4" >
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endColor="#0000"
|
||||
android:endX="80"
|
||||
android:endY="80"
|
||||
android:startColor="#4e000000"
|
||||
android:startX="0"
|
||||
android:startY="0"
|
||||
android:type="linear"/>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</group>
|
||||
<group
|
||||
android:scaleX="0.3939503"
|
||||
|
||||
BIN
app/src/libre/res/drawable/ic_launcher_foreground.png
Normal file
BIN
app/src/libre/res/drawable/ic_launcher_foreground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
@@ -1,5 +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
|
||||
@@ -7,10 +8,21 @@
|
||||
android:normalScreens="true"
|
||||
android:largeScreens="true"
|
||||
android:anyDensity="true" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<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" />
|
||||
|
||||
<application
|
||||
android:label="@string/app_name"
|
||||
@@ -18,14 +30,15 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:name="com.kunzisoft.keepass.app.App"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
|
||||
android:theme="@style/KeepassDXStyle.Night">
|
||||
<!-- TODO backup API Key -->
|
||||
android:largeHeap="true"
|
||||
android:resizeableActivity="true"
|
||||
android:theme="@style/KeepassDXStyle.Night"
|
||||
tools:targetApi="n">
|
||||
<meta-data
|
||||
android:name="com.google.android.backup.api_key"
|
||||
android:value="" />
|
||||
|
||||
android:value="${googleAndroidBackupAPIKey}" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.FileDatabaseSelectActivity"
|
||||
android:theme="@style/KeepassDXStyle.SplashScreen"
|
||||
@@ -41,7 +54,7 @@
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.PasswordActivity"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
android:windowSoftInputMode="adjustResize|stateUnchanged">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -116,25 +129,51 @@
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.EntryActivity"
|
||||
android:configChanges="keyboardHidden" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.IconPickerActivity"
|
||||
android:configChanges="keyboardHidden" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.ImageViewerActivity"
|
||||
android:configChanges="keyboardHidden" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<!-- About and Settings -->
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/menu_about" />
|
||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
||||
<activity android:name="com.kunzisoft.keepass.autofill.AutofillLauncherActivity"
|
||||
android:label="@string/about" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
|
||||
android:theme="@style/Theme.Transparent"
|
||||
android:configChanges="keyboardHidden" />
|
||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
|
||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsAutofillActivity" />
|
||||
<activity android:name="com.kunzisoft.keepass.magikeyboard.KeyboardLauncherActivity"
|
||||
android:label="@string/keyboard_name"
|
||||
android:exported="true">
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
|
||||
android:theme="@style/Theme.Transparent">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="otpauth" android:host="totp" />
|
||||
<data android:scheme="otpauth" android:host="hotp" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.kunzisoft.keepass.settings.MagikIMESettings"
|
||||
<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"/>
|
||||
@@ -142,15 +181,19 @@
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.notifications.DatabaseOpenNotificationService"
|
||||
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService"
|
||||
android:name="com.kunzisoft.keepass.services.AttachmentFileNotificationService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService"
|
||||
android:name="com.kunzisoft.keepass.services.ClipboardEntryNotificationService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.services.AdvancedUnlockNotificationService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<!-- Receiver for Autofill -->
|
||||
@@ -176,7 +219,7 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService"
|
||||
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
|
||||
Binary file not shown.
BIN
app/src/main/assets/publicsuffixes
Normal file
BIN
app/src/main/assets/publicsuffixes
Normal file
Binary file not shown.
1061
app/src/main/java/com/igreenwood/loupe/Loupe.kt
Normal file
1061
app/src/main/java/com/igreenwood/loupe/Loupe.kt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,35 +1,35 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
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 org.joda.time.DateTime
|
||||
|
||||
class AboutActivity : StylishActivity() {
|
||||
@@ -40,7 +40,7 @@ class AboutActivity : StylishActivity() {
|
||||
setContentView(R.layout.activity_about)
|
||||
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
toolbar.title = getString(R.string.menu_about)
|
||||
toolbar.title = getString(R.string.about)
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
@@ -64,9 +64,17 @@ class AboutActivity : StylishActivity() {
|
||||
val buildTextView = findViewById<TextView>(R.id.activity_about_build)
|
||||
buildTextView.text = build
|
||||
|
||||
findViewById<TextView>(R.id.activity_about_licence_text).apply {
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
text = HtmlCompat.fromHtml(getString(R.string.html_about_licence, DateTime().year),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
}
|
||||
|
||||
val disclaimerText = findViewById<TextView>(R.id.disclaimer)
|
||||
disclaimerText.text = getString(R.string.disclaimer_formal, DateTime().year)
|
||||
findViewById<TextView>(R.id.activity_about_contribution_text).apply {
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
text = HtmlCompat.fromHtml(getString(R.string.html_about_contribution),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
@@ -22,8 +22,10 @@ import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
@@ -33,44 +35,66 @@ import android.widget.ProgressBar
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
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.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.EntryVersioned
|
||||
import com.kunzisoft.keepass.database.element.PwNodeId
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
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.settings.SettingsAutofillActivity
|
||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
import com.kunzisoft.keepass.view.EntryContentsView
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
class EntryActivity : LockingHideActivity() {
|
||||
class EntryActivity : LockingActivity() {
|
||||
|
||||
private var coordinatorLayout: CoordinatorLayout? = null
|
||||
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
||||
private var titleIconView: ImageView? = null
|
||||
private var historyView: View? = null
|
||||
private var entryContentsView: EntryContentsView? = null
|
||||
private var entryProgress: ProgressBar? = null
|
||||
private var lockView: View? = null
|
||||
private var toolbar: Toolbar? = null
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
|
||||
private var mEntry: EntryVersioned? = null
|
||||
private var mEntry: Entry? = null
|
||||
|
||||
private var mIsHistory: Boolean = false
|
||||
private var mEntryLastVersion: Entry? = null
|
||||
private var mEntryHistoryPosition: Int = -1
|
||||
|
||||
private var mShowPassword: Boolean = false
|
||||
|
||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
|
||||
|
||||
private var clipboardHelper: ClipboardHelper? = null
|
||||
private var firstLaunchOfActivity: Boolean = false
|
||||
private var mFirstLaunchOfActivity: Boolean = false
|
||||
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
private var iconColor: Int = 0
|
||||
|
||||
@@ -98,30 +122,74 @@ class EntryActivity : LockingHideActivity() {
|
||||
invalidateOptionsMenu()
|
||||
|
||||
// Get views
|
||||
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
||||
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
||||
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)
|
||||
entryProgress = findViewById(R.id.entry_progress)
|
||||
lockView = findViewById(R.id.lock_button)
|
||||
|
||||
lockView?.setOnClickListener {
|
||||
lockAndExit()
|
||||
}
|
||||
|
||||
// Focus view to reinitialize timeout
|
||||
coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
|
||||
|
||||
// Init the clipboard helper
|
||||
clipboardHelper = ClipboardHelper(this)
|
||||
firstLaunchOfActivity = true
|
||||
mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true
|
||||
|
||||
// Init SAF manager
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
|
||||
// Init attachment service binder manager
|
||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||
|
||||
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()
|
||||
}
|
||||
ACTION_DATABASE_RELOAD_TASK -> {
|
||||
// Close the current activity
|
||||
this.showActionErrorIfNeeded(result)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
coordinatorLayout?.showActionErrorIfNeeded(result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// Show the lock button
|
||||
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
// Get Entry from UUID
|
||||
try {
|
||||
val keyEntry: PwNodeId<UUID> = intent.getParcelableExtra(KEY_ENTRY)
|
||||
mEntry = mDatabase?.getEntryById(keyEntry)
|
||||
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, -1)
|
||||
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, mEntryHistoryPosition)
|
||||
mEntryHistoryPosition = historyPosition
|
||||
if (historyPosition >= 0) {
|
||||
mIsHistory = true
|
||||
mEntry = mEntry?.getHistory()?.get(historyPosition)
|
||||
@@ -142,10 +210,9 @@ class EntryActivity : LockingHideActivity() {
|
||||
// Refresh Menu
|
||||
invalidateOptionsMenu()
|
||||
|
||||
val entryInfo = entry.getEntryInfo(Database.getInstance())
|
||||
|
||||
val entryInfo = entry.getEntryInfo(mDatabase)
|
||||
// Manage entry copy to start notification if allowed
|
||||
if (firstLaunchOfActivity) {
|
||||
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
|
||||
@@ -155,30 +222,46 @@ class EntryActivity : LockingHideActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
firstLaunchOfActivity = false
|
||||
mAttachmentFileBinderManager?.apply {
|
||||
registerProgressTask()
|
||||
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
||||
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
||||
if (entryAttachmentState.streamDirection != StreamDirection.UPLOAD) {
|
||||
entryContentsView?.putAttachment(entryAttachmentState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mFirstLaunchOfActivity = false
|
||||
}
|
||||
|
||||
private fun fillEntryDataInContentsView(entry: EntryVersioned) {
|
||||
override fun onPause() {
|
||||
mAttachmentFileBinderManager?.unregisterProgressTask()
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun fillEntryDataInContentsView(entry: Entry) {
|
||||
|
||||
val entryInfo = entry.getEntryInfo(mDatabase)
|
||||
|
||||
val database = Database.getInstance()
|
||||
database.startManageEntry(entry)
|
||||
// Assign title icon
|
||||
titleIconView?.assignDatabaseIcon(database.drawFactory, entry.icon, iconColor)
|
||||
titleIconView?.let { iconView ->
|
||||
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, iconColor)
|
||||
}
|
||||
|
||||
// Assign title text
|
||||
val entryTitle = entry.title
|
||||
val entryTitle = entryInfo.title
|
||||
collapsingToolbarLayout?.title = entryTitle
|
||||
toolbar?.title = entryTitle
|
||||
|
||||
// Assign basic fields
|
||||
entryContentsView?.assignUserName(entry.username)
|
||||
entryContentsView?.assignUserNameCopyListener(View.OnClickListener {
|
||||
database.startManageEntry(entry)
|
||||
clipboardHelper?.timeoutCopyToClipboard(entry.username,
|
||||
getString(R.string.copy_field,
|
||||
entryContentsView?.assignUserName(entryInfo.username) {
|
||||
clipboardHelper?.timeoutCopyToClipboard(entryInfo.username,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_user_name)))
|
||||
database.stopManageEntry(entry)
|
||||
})
|
||||
}
|
||||
|
||||
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
||||
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
|
||||
@@ -191,7 +274,7 @@ class EntryActivity : LockingHideActivity() {
|
||||
"\n\n" +
|
||||
getString(R.string.clipboard_warning))
|
||||
.create().apply {
|
||||
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) {dialog, _ ->
|
||||
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) { dialog, _ ->
|
||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true)
|
||||
dialog.dismiss()
|
||||
fillEntryDataInContentsView(entry)
|
||||
@@ -205,79 +288,77 @@ class EntryActivity : LockingHideActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
entryContentsView?.assignPassword(entry.password, allowCopyPasswordAndProtectedFields)
|
||||
if (allowCopyPasswordAndProtectedFields) {
|
||||
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
|
||||
database.startManageEntry(entry)
|
||||
clipboardHelper?.timeoutCopyToClipboard(entry.password,
|
||||
getString(R.string.copy_field,
|
||||
val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) {
|
||||
View.OnClickListener {
|
||||
clipboardHelper?.timeoutCopyToClipboard(entryInfo.password,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_password)))
|
||||
database.stopManageEntry(entry)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// If dialog not already shown
|
||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||
entryContentsView?.assignPasswordCopyListener(showWarningClipboardDialogOnClickListener)
|
||||
showWarningClipboardDialogOnClickListener
|
||||
} else {
|
||||
entryContentsView?.assignPasswordCopyListener(null)
|
||||
null
|
||||
}
|
||||
}
|
||||
entryContentsView?.assignPassword(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))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//Assign OTP field
|
||||
entryContentsView?.assignOtp(entry.getOtpElement(), entryProgress,
|
||||
View.OnClickListener {
|
||||
entry.getOtpElement()?.let { otpElement ->
|
||||
clipboardHelper?.timeoutCopyToClipboard(
|
||||
otpElement.token,
|
||||
getString(R.string.copy_field, getString(R.string.entry_otp))
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
entryContentsView?.assignURL(entry.url)
|
||||
entryContentsView?.assignComment(entry.notes)
|
||||
entryContentsView?.assignURL(entryInfo.url)
|
||||
entryContentsView?.assignNotes(entryInfo.notes)
|
||||
|
||||
// Assign custom fields
|
||||
if (entry.allowCustomFields()) {
|
||||
if (mDatabase?.allowEntryCustomFields() == true) {
|
||||
entryContentsView?.clearExtraFields()
|
||||
|
||||
for (element in entry.customFields.entries) {
|
||||
val label = element.key
|
||||
val value = element.value
|
||||
|
||||
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
||||
if (allowCopyProtectedField) {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, View.OnClickListener {
|
||||
clipboardHelper?.timeoutCopyToClipboard(
|
||||
value.toString(),
|
||||
getString(R.string.copy_field, label)
|
||||
)
|
||||
})
|
||||
} else {
|
||||
// If dialog not already shown
|
||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
|
||||
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 {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null)
|
||||
// If dialog not already shown
|
||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
|
||||
} else {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
|
||||
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
|
||||
|
||||
// Assign dates
|
||||
entryContentsView?.assignCreationDate(entry.creationTime)
|
||||
entryContentsView?.assignModificationDate(entry.lastModificationTime)
|
||||
entryContentsView?.assignLastAccessDate(entry.lastAccessTime)
|
||||
entryContentsView?.setExpires(entry.isCurrentlyExpires)
|
||||
if (entry.expires) {
|
||||
entryContentsView?.assignExpiresDate(entry.expiryTime)
|
||||
} else {
|
||||
entryContentsView?.assignExpiresDate(getString(R.string.never))
|
||||
// Manage attachments
|
||||
entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
|
||||
mExternalFileHelper?.createDocument(attachmentItem.name)?.let { requestCode ->
|
||||
mAttachmentsToDownload[requestCode] = attachmentItem
|
||||
}
|
||||
}
|
||||
|
||||
// Assign special data
|
||||
entryContentsView?.assignUUID(entry.nodeId.id)
|
||||
// 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
|
||||
@@ -286,22 +367,17 @@ class EntryActivity : LockingHideActivity() {
|
||||
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
||||
taColorAccent.recycle()
|
||||
}
|
||||
val entryHistory = entry.getHistory()
|
||||
// isMainEntry = not an history
|
||||
val showHistoryView = entryHistory.isNotEmpty()
|
||||
entryContentsView?.showHistory(showHistoryView)
|
||||
if (showHistoryView) {
|
||||
entryContentsView?.assignHistory(entryHistory)
|
||||
entryContentsView?.onHistoryClick { historyItem, position ->
|
||||
launch(this, historyItem, true, position)
|
||||
}
|
||||
entryContentsView?.assignHistory(entry.getHistory()) { historyItem, position ->
|
||||
launch(this, historyItem, mReadOnly, position)
|
||||
}
|
||||
|
||||
database.stopManageEntry(entry)
|
||||
// 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
|
||||
@@ -309,15 +385,14 @@ class EntryActivity : LockingHideActivity() {
|
||||
fillEntryDataInContentsView(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeShowPasswordIcon(togglePassword: MenuItem?) {
|
||||
if (mShowPassword) {
|
||||
togglePassword?.setTitle(R.string.menu_hide_password)
|
||||
togglePassword?.setIcon(R.drawable.ic_visibility_off_white_24dp)
|
||||
} else {
|
||||
togglePassword?.setTitle(R.string.menu_showpass)
|
||||
togglePassword?.setIcon(R.drawable.ic_visibility_white_24dp)
|
||||
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
||||
if (createdFileUri != null) {
|
||||
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
|
||||
mAttachmentFileBinderManager
|
||||
?.startDownloadAttachment(createdFileUri, attachmentToDownload)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,19 +402,16 @@ class EntryActivity : LockingHideActivity() {
|
||||
val inflater = menuInflater
|
||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||
inflater.inflate(R.menu.entry, menu)
|
||||
inflater.inflate(R.menu.database_lock, menu)
|
||||
|
||||
if (mReadOnly) {
|
||||
inflater.inflate(R.menu.database, menu)
|
||||
if (mIsHistory && !mReadOnly) {
|
||||
inflater.inflate(R.menu.entry_history, menu)
|
||||
}
|
||||
if (mIsHistory || mReadOnly) {
|
||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||
menu.findItem(R.id.menu_edit)?.isVisible = false
|
||||
}
|
||||
|
||||
val togglePassword = menu.findItem(R.id.menu_toggle_pass)
|
||||
entryContentsView?.let {
|
||||
if (it.isPasswordPresent || it.atLeastOneFieldProtectedPresent()) {
|
||||
changeShowPasswordIcon(togglePassword)
|
||||
} else {
|
||||
togglePassword?.isVisible = false
|
||||
}
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
menu.findItem(R.id.menu_reload_database)?.isVisible = false
|
||||
}
|
||||
|
||||
val gotoUrl = menu.findItem(R.id.menu_goto_url)
|
||||
@@ -357,38 +429,38 @@ class EntryActivity : LockingHideActivity() {
|
||||
}
|
||||
|
||||
// Show education views
|
||||
Handler().post { performedNextEducation(EntryActivityEducation(this), menu) }
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(EntryActivityEducation(this), menu) }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
||||
menu: Menu) {
|
||||
val entryCopyEducationPerformed = entryContentsView?.isUserNamePresent == true
|
||||
val entryFieldCopyView = entryContentsView?.firstEntryFieldCopyView()
|
||||
val entryCopyEducationPerformed = entryFieldCopyView != null
|
||||
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
||||
findViewById(R.id.entry_user_name_action_image),
|
||||
entryFieldCopyView,
|
||||
{
|
||||
clipboardHelper?.timeoutCopyToClipboard(mEntry!!.username,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_user_name)))
|
||||
val appNameString = getString(R.string.app_name)
|
||||
clipboardHelper?.timeoutCopyToClipboard(appNameString,
|
||||
getString(R.string.copy_field, appNameString))
|
||||
},
|
||||
{
|
||||
// Launch autofill settings
|
||||
startActivity(Intent(this@EntryActivity, SettingsAutofillActivity::class.java))
|
||||
performedNextEducation(entryActivityEducation, menu)
|
||||
})
|
||||
|
||||
if (!entryCopyEducationPerformed) {
|
||||
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
|
||||
// entryEditEducationPerformed
|
||||
toolbar?.findViewById<View>(R.id.menu_edit) != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
|
||||
toolbar!!.findViewById(R.id.menu_edit),
|
||||
{
|
||||
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
||||
},
|
||||
{
|
||||
// Open Keepass doc to create field references
|
||||
startActivity(Intent(Intent.ACTION_VIEW,
|
||||
UriUtil.parse(getString(R.string.field_references_url))))
|
||||
})
|
||||
menuEditView != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
|
||||
menuEditView,
|
||||
{
|
||||
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryActivityEducation, menu)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,21 +470,12 @@ class EntryActivity : LockingHideActivity() {
|
||||
MenuUtil.onContributionItemSelected(this)
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_toggle_pass -> {
|
||||
mShowPassword = !mShowPassword
|
||||
changeShowPasswordIcon(item)
|
||||
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_edit -> {
|
||||
mEntry?.let {
|
||||
EntryEditActivity.launch(this@EntryActivity, it)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_goto_url -> {
|
||||
var url: String = mEntry?.url ?: ""
|
||||
|
||||
@@ -422,40 +485,59 @@ class EntryActivity : LockingHideActivity() {
|
||||
}
|
||||
|
||||
UriUtil.gotoUrl(this, url)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_lock -> {
|
||||
lockAndExit()
|
||||
return true
|
||||
R.id.menu_restore_entry_history -> {
|
||||
mEntryLastVersion?.let { mainEntry ->
|
||||
mProgressDatabaseTaskProvider?.startDatabaseRestoreEntryHistory(
|
||||
mainEntry,
|
||||
mEntryHistoryPosition,
|
||||
!mReadOnly && mAutoSaveEnable)
|
||||
}
|
||||
}
|
||||
R.id.menu_delete_entry_history -> {
|
||||
mEntryLastVersion?.let { mainEntry ->
|
||||
mProgressDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(
|
||||
mainEntry,
|
||||
mEntryHistoryPosition,
|
||||
!mReadOnly && mAutoSaveEnable)
|
||||
}
|
||||
}
|
||||
R.id.menu_save_database -> {
|
||||
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
|
||||
}
|
||||
R.id.menu_reload_database -> {
|
||||
mProgressDatabaseTaskProvider?.startDatabaseReload(false)
|
||||
}
|
||||
|
||||
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
|
||||
/*
|
||||
TODO Slowdown when add entry as result
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry);
|
||||
onFinish(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent);
|
||||
*/
|
||||
Intent().apply {
|
||||
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry)
|
||||
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, this)
|
||||
}
|
||||
super.finish()
|
||||
}
|
||||
|
||||
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: EntryVersioned, readOnly: Boolean, historyPosition: Int? = null) {
|
||||
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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
@@ -1,72 +1,81 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.assist.AssistStructure
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.preference.PreferenceManager
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
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.dialogs.BrowserDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||
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.ProgressDialogThread
|
||||
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
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.view.asError
|
||||
import kotlinx.android.synthetic.main.activity_file_selection.*
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
class FileDatabaseSelectActivity : StylishActivity(),
|
||||
class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
||||
|
||||
// Views
|
||||
private var fileListContainer: View? = null
|
||||
private var createButtonView: View? = null
|
||||
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||
private var createDatabaseButtonView: View? = null
|
||||
private var openDatabaseButtonView: View? = null
|
||||
|
||||
private val databaseFilesViewModel: DatabaseFilesViewModel by viewModels()
|
||||
|
||||
// Adapter to manage database history list
|
||||
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
|
||||
|
||||
@@ -74,9 +83,9 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
|
||||
private var mDatabaseFileUri: Uri? = null
|
||||
|
||||
private var mOpenFileHelper: OpenFileHelper? = null
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
private var progressDialogThread: ProgressDialogThread? = null
|
||||
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -84,31 +93,20 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
|
||||
|
||||
setContentView(R.layout.activity_file_selection)
|
||||
fileListContainer = findViewById(R.id.container_file_list)
|
||||
coordinatorLayout = findViewById(R.id.activity_file_selection_coordinator_layout)
|
||||
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
toolbar.title = ""
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
// Create button
|
||||
createButtonView = findViewById(R.id.create_database_button)
|
||||
if (Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/x-keepass"
|
||||
}.resolveActivity(packageManager) == null) {
|
||||
// No Activity found that can handle this intent.
|
||||
createButtonView?.visibility = View.GONE
|
||||
}
|
||||
else{
|
||||
// There is an activity which can handle this intent.
|
||||
createButtonView?.visibility = View.VISIBLE
|
||||
}
|
||||
// Create database button
|
||||
createDatabaseButtonView = findViewById(R.id.create_database_button)
|
||||
createDatabaseButtonView?.setOnClickListener { createNewFile() }
|
||||
|
||||
createButtonView?.setOnClickListener { createNewFile() }
|
||||
|
||||
mOpenFileHelper = OpenFileHelper(this)
|
||||
openDatabaseButtonView = findViewById(R.id.open_database_button)
|
||||
openDatabaseButtonView?.setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener)
|
||||
// Open database button
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
||||
openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||
|
||||
// History list
|
||||
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
||||
@@ -117,28 +115,25 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
(fileDatabaseHistoryRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
// Construct adapter with listeners
|
||||
mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this)
|
||||
mAdapterDatabaseHistory?.setOnDefaultDatabaseListener { databaseFile ->
|
||||
databaseFilesViewModel.setDefaultDatabase(databaseFile)
|
||||
}
|
||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
||||
UriUtil.parse(fileDatabaseHistoryEntityToOpen.databaseUri)?.let { databaseFileUri ->
|
||||
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
|
||||
launchPasswordActivity(
|
||||
databaseFileUri,
|
||||
UriUtil.parse(fileDatabaseHistoryEntityToOpen.keyFileUri))
|
||||
fileDatabaseHistoryEntityToOpen.keyFileUri
|
||||
)
|
||||
}
|
||||
updateFileListVisibility()
|
||||
}
|
||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
||||
// Remove from app database
|
||||
mFileDatabaseHistoryAction?.deleteFileDatabaseHistory(fileDatabaseHistoryToDelete) { fileHistoryDeleted ->
|
||||
// Remove from adapter
|
||||
fileHistoryDeleted?.let { databaseFileHistoryDeleted ->
|
||||
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileHistoryDeleted)
|
||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
updateFileListVisibility()
|
||||
}
|
||||
}
|
||||
databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete)
|
||||
true
|
||||
}
|
||||
mAdapterDatabaseHistory?.setOnSaveAliasListener { fileDatabaseHistoryWithNewAlias ->
|
||||
mFileDatabaseHistoryAction?.addOrUpdateFileDatabaseHistory(fileDatabaseHistoryWithNewAlias)
|
||||
// Update in app database
|
||||
databaseFilesViewModel.updateDatabaseFile(fileDatabaseHistoryWithNewAlias)
|
||||
}
|
||||
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
|
||||
|
||||
@@ -146,13 +141,12 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
if (!(savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(EXTRA_STAY)
|
||||
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val databasePath = prefs.getString(PasswordActivity.KEY_DEFAULT_DATABASE_PATH, "")
|
||||
val databasePath = PreferencesUtil.getDefaultDatabasePath(this)
|
||||
|
||||
UriUtil.parse(databasePath)?.let { databaseFileUri ->
|
||||
launchPasswordActivityWithPath(databaseFileUri)
|
||||
} ?: run {
|
||||
Log.i(TAG, "Unable to launch Password Activity")
|
||||
Log.i(TAG, "No default database to prepare")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,14 +156,71 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
mDatabaseFileUri = savedInstanceState.getParcelable(EXTRA_DATABASE_URI)
|
||||
}
|
||||
|
||||
// Observe list of databases
|
||||
databaseFilesViewModel.databaseFilesLoaded.observe(this) { databaseFiles ->
|
||||
try {
|
||||
when (databaseFiles.databaseFileAction) {
|
||||
DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
|
||||
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
|
||||
}
|
||||
DatabaseFilesViewModel.DatabaseFileAction.ADD -> {
|
||||
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
|
||||
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
|
||||
}
|
||||
}
|
||||
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
|
||||
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
|
||||
mAdapterDatabaseHistory?.updateDatabaseFileHistory(databaseFileToUpdate)
|
||||
}
|
||||
}
|
||||
DatabaseFilesViewModel.DatabaseFileAction.DELETE -> {
|
||||
databaseFiles.databaseFileToActivate?.let { databaseFileToDelete ->
|
||||
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileToDelete)
|
||||
}
|
||||
}
|
||||
}
|
||||
databaseFilesViewModel.consumeAction()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to observe database action", e)
|
||||
}
|
||||
}
|
||||
|
||||
// Observe default database
|
||||
databaseFilesViewModel.defaultDatabase.observe(this) {
|
||||
// Retrieve settings for default database
|
||||
mAdapterDatabaseHistory?.setDefaultDatabase(it)
|
||||
}
|
||||
|
||||
// Attach the dialog thread to this activity
|
||||
progressDialogThread = ProgressDialogThread(this) { actionTask, _ ->
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_CREATE_TASK -> {
|
||||
// TODO Check
|
||||
// mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
// updateFileListVisibility()
|
||||
GroupActivity.launch(this)
|
||||
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)
|
||||
}
|
||||
GroupActivity.launch(this@FileDatabaseSelectActivity,
|
||||
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity))
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,76 +229,44 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
/**
|
||||
* Create a new file by calling the content provider
|
||||
*/
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun createNewFile() {
|
||||
try {
|
||||
startActivityForResult(Intent(
|
||||
Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/x-keepass"
|
||||
putExtra(Intent.EXTRA_TITLE, getString(R.string.database_file_name_default) +
|
||||
getString(R.string.database_file_extension_default))
|
||||
},
|
||||
CREATE_FILE_REQUEST_CODE)
|
||||
} catch (e: Exception) {
|
||||
BrowserDialogFragment().show(supportFragmentManager, "browserDialog")
|
||||
}
|
||||
mExternalFileHelper?.createDocument( getString(R.string.database_file_name_default) +
|
||||
getString(R.string.database_file_extension_default), "application/x-keepass")
|
||||
}
|
||||
|
||||
private fun fileNoFoundAction(e: FileNotFoundException) {
|
||||
val error = getString(R.string.file_not_found_content)
|
||||
Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||
Log.e(TAG, error, e)
|
||||
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
|
||||
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||
{
|
||||
try {
|
||||
PasswordActivity.launch(this@FileDatabaseSelectActivity,
|
||||
databaseUri, keyFile)
|
||||
} catch (e: FileNotFoundException) {
|
||||
fileNoFoundAction(e)
|
||||
}
|
||||
PasswordActivity.launch(this,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
{ exception ->
|
||||
fileNoFoundAction(exception)
|
||||
},
|
||||
{
|
||||
try {
|
||||
PasswordActivity.launchForKeyboardResult(this@FileDatabaseSelectActivity,
|
||||
databaseUri, keyFile)
|
||||
finish()
|
||||
} catch (e: FileNotFoundException) {
|
||||
fileNoFoundAction(e)
|
||||
}
|
||||
},
|
||||
{ assistStructure ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
try {
|
||||
PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
||||
databaseUri, keyFile,
|
||||
assistStructure)
|
||||
} catch (e: FileNotFoundException) {
|
||||
fileNoFoundAction(e)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() })
|
||||
}
|
||||
|
||||
private fun launchGroupActivity(readOnly: Boolean) {
|
||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||
{
|
||||
GroupActivity.launch(this@FileDatabaseSelectActivity, readOnly)
|
||||
},
|
||||
{
|
||||
GroupActivity.launchForKeyboardSelection(this@FileDatabaseSelectActivity, readOnly)
|
||||
// Do not keep history
|
||||
finish()
|
||||
},
|
||||
{ assistStructure ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
GroupActivity.launchForAutofillResult(this@FileDatabaseSelectActivity, assistStructure, readOnly)
|
||||
}
|
||||
})
|
||||
private fun launchGroupActivity(database: Database) {
|
||||
GroupActivity.launch(this,
|
||||
database.isReadOnly,
|
||||
{ onValidateSpecialMode() },
|
||||
{ onCancelSpecialMode() },
|
||||
{ onLaunchActivitySpecialMode() })
|
||||
}
|
||||
|
||||
override fun onValidateSpecialMode() {
|
||||
super.onValidateSpecialMode()
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onCancelSpecialMode() {
|
||||
super.onCancelSpecialMode()
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
||||
@@ -257,51 +276,46 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
overridePendingTransition(0, 0)
|
||||
}
|
||||
|
||||
private fun updateExternalStorageWarning() {
|
||||
// To show errors
|
||||
var warning = -1
|
||||
val state = Environment.getExternalStorageState()
|
||||
if (state == Environment.MEDIA_MOUNTED_READ_ONLY) {
|
||||
warning = R.string.read_only_warning
|
||||
} else if (state != Environment.MEDIA_MOUNTED) {
|
||||
warning = R.string.warning_unmounted
|
||||
}
|
||||
|
||||
val labelWarningView = findViewById<TextView>(R.id.label_warning)
|
||||
if (warning != -1) {
|
||||
labelWarningView.setText(warning)
|
||||
labelWarningView.visibility = View.VISIBLE
|
||||
} else {
|
||||
labelWarningView.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
val database = Database.getInstance()
|
||||
if (database.loaded) {
|
||||
launchGroupActivity(database.isReadOnly)
|
||||
}
|
||||
|
||||
super.onResume()
|
||||
|
||||
updateExternalStorageWarning()
|
||||
|
||||
// Construct adapter with listeners
|
||||
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
|
||||
databaseFileHistoryList?.let {
|
||||
mAdapterDatabaseHistory?.addDatabaseFileHistoryList(it)
|
||||
updateFileListVisibility()
|
||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
// Show open and create button or special mode
|
||||
when (mSpecialMode) {
|
||||
SpecialMode.DEFAULT -> {
|
||||
if (ExternalFileHelper.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
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Disable create button if in selection mode or request for autofill
|
||||
createDatabaseButtonView?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
// Register progress task
|
||||
progressDialogThread?.registerProgressTask()
|
||||
val database = Database.getInstance()
|
||||
if (database.loaded) {
|
||||
launchGroupActivity(database)
|
||||
} else {
|
||||
// Construct adapter with listeners
|
||||
if (PreferencesUtil.showRecentFiles(this)) {
|
||||
databaseFilesViewModel.loadListOfDatabases()
|
||||
} else {
|
||||
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
// Register progress task
|
||||
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
// Unregister progress task
|
||||
progressDialogThread?.unregisterProgressTask()
|
||||
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
@@ -314,41 +328,25 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
||||
}
|
||||
|
||||
private fun updateFileListVisibility() {
|
||||
if (mAdapterDatabaseHistory?.itemCount == 0)
|
||||
fileListContainer?.visibility = View.INVISIBLE
|
||||
else
|
||||
fileListContainer?.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
override fun onAssignKeyDialogPositiveClick(
|
||||
masterPasswordChecked: Boolean, masterPassword: String?,
|
||||
keyFileChecked: Boolean, keyFile: Uri?) {
|
||||
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
|
||||
|
||||
try {
|
||||
mDatabaseFileUri?.let { databaseUri ->
|
||||
|
||||
// Create the new database
|
||||
progressDialogThread?.startDatabaseCreate(
|
||||
mProgressDatabaseTaskProvider?.startDatabaseCreate(
|
||||
databaseUri,
|
||||
masterPasswordChecked,
|
||||
masterPassword,
|
||||
keyFileChecked,
|
||||
keyFile
|
||||
mainCredential
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
val error = getString(R.string.error_create_database_file)
|
||||
Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||
Log.e(TAG, error, e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAssignKeyDialogNegativeClick(
|
||||
masterPasswordChecked: Boolean, masterPassword: String?,
|
||||
keyFileChecked: Boolean, keyFile: Uri?) {
|
||||
|
||||
}
|
||||
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
@@ -357,42 +355,47 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
||||
) { uri ->
|
||||
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||
if (uri != null) {
|
||||
launchPasswordActivityWithPath(uri)
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve the created URI from the file manager
|
||||
if (requestCode == CREATE_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
|
||||
mDatabaseFileUri = data?.data
|
||||
mExternalFileHelper?.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)
|
||||
}
|
||||
// else {
|
||||
// TODO Show error
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
MenuUtil.defaultMenuInflater(menuInflater, menu)
|
||||
|
||||
Handler().post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
||||
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||
MenuUtil.defaultMenuInflater(menuInflater, menu)
|
||||
}
|
||||
|
||||
Handler(Looper.getMainLooper()).post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
|
||||
// If no recent files
|
||||
val createDatabaseEducationPerformed = createButtonView != null && createButtonView!!.visibility == View.VISIBLE
|
||||
val createDatabaseEducationPerformed =
|
||||
createDatabaseButtonView != null
|
||||
&& createDatabaseButtonView!!.visibility == View.VISIBLE
|
||||
&& mAdapterDatabaseHistory != null
|
||||
&& mAdapterDatabaseHistory!!.itemCount > 0
|
||||
&& mAdapterDatabaseHistory!!.itemCount == 0
|
||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
||||
createButtonView!!,
|
||||
createDatabaseButtonView!!,
|
||||
{
|
||||
createNewFile()
|
||||
},
|
||||
@@ -405,9 +408,9 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
openDatabaseButtonView != null
|
||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
||||
openDatabaseButtonView!!,
|
||||
{tapTargetView ->
|
||||
{ tapTargetView ->
|
||||
tapTargetView?.let {
|
||||
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it)
|
||||
mExternalFileHelper?.openDocument()
|
||||
}
|
||||
},
|
||||
{}
|
||||
@@ -416,7 +419,11 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
|
||||
}
|
||||
MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -425,22 +432,53 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
private const val EXTRA_STAY = "EXTRA_STAY"
|
||||
private const val EXTRA_DATABASE_URI = "EXTRA_DATABASE_URI"
|
||||
|
||||
private const val CREATE_FILE_REQUEST_CODE = 3853
|
||||
/*
|
||||
* -------------------------
|
||||
* Standard Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
fun launch(context: Context) {
|
||||
context.startActivity(Intent(context, FileDatabaseSelectActivity::class.java))
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* No Standard Launch, pass by PasswordActivity
|
||||
* Search Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
fun launchForSearchResult(context: Context,
|
||||
searchInfo: SearchInfo) {
|
||||
EntrySelectionHelper.startActivityForSearchModeResult(context,
|
||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
searchInfo)
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Save Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
fun launchForSaveResult(context: Context,
|
||||
searchInfo: SearchInfo) {
|
||||
EntrySelectionHelper.startActivityForSaveModeResult(context,
|
||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
searchInfo)
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Keyboard Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
fun launchForKeyboardSelection(activity: Activity) {
|
||||
EntrySelectionHelper.startActivityForEntrySelection(activity, Intent(activity, FileDatabaseSelectActivity::class.java))
|
||||
fun launchForKeyboardSelectionResult(activity: Activity,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(activity,
|
||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||
searchInfo)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -450,10 +488,25 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
||||
*/
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
fun launchForAutofillResult(activity: Activity, assistStructure: AssistStructure) {
|
||||
fun launchForAutofillResult(activity: Activity,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo? = null) {
|
||||
AutofillHelper.startActivityForAutofillResult(activity,
|
||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||
assistStructure)
|
||||
autofillComponent,
|
||||
searchInfo)
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Registration Launch
|
||||
* -------------------------
|
||||
*/
|
||||
fun launchForRegistration(context: Context,
|
||||
registerInfo: RegisterInfo? = null) {
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
|
||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||
registerInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,324 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.fragment.app.commit
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||
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.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.view.asError
|
||||
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
||||
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
|
||||
class IconPickerActivity : LockingActivity() {
|
||||
|
||||
private lateinit var toolbar: Toolbar
|
||||
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||
private lateinit var uploadButton: View
|
||||
private var lockView: View? = null
|
||||
|
||||
private var mIconImage: IconImage = IconImage()
|
||||
|
||||
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
private val iconPickerViewModel: IconPickerViewModel by viewModels()
|
||||
private var mCustomIconsSelectionMode = false
|
||||
private var mIconsSelected: List<IconImageCustom> = ArrayList()
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
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)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
updateIconsSelectedViews()
|
||||
|
||||
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
|
||||
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
|
||||
uploadButton = findViewById(R.id.icon_picker_upload)
|
||||
if (mDatabase?.allowCustomIcons == true) {
|
||||
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
|
||||
} else {
|
||||
uploadButton.visibility = View.GONE
|
||||
}
|
||||
|
||||
lockView = findViewById(R.id.lock_button)
|
||||
lockView?.setOnClickListener {
|
||||
lockAndExit()
|
||||
}
|
||||
|
||||
intent?.getParcelableExtra<IconImage>(EXTRA_ICON)?.let {
|
||||
mIconImage = it
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager.commit {
|
||||
setReorderingAllowed(true)
|
||||
add(R.id.icon_picker_fragment, IconPickerFragment.getInstance(
|
||||
// Default selection tab
|
||||
if (mIconImage.custom.isUnknown)
|
||||
IconPickerFragment.IconTab.STANDARD
|
||||
else
|
||||
IconPickerFragment.IconTab.CUSTOM
|
||||
), ICON_PICKER_FRAGMENT_TAG)
|
||||
}
|
||||
} else {
|
||||
mIconImage = savedInstanceState.getParcelable(EXTRA_ICON) ?: mIconImage
|
||||
}
|
||||
|
||||
// Focus view to reinitialize timeout
|
||||
findViewById<ViewGroup>(R.id.icon_picker_container)?.resetAppTimeoutWhenViewFocusedOrChanged(this)
|
||||
|
||||
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
|
||||
mIconImage.standard = iconStandard
|
||||
// Remove the custom icon if a standard one is selected
|
||||
mIconImage.custom = IconImageCustom()
|
||||
setResult()
|
||||
finish()
|
||||
}
|
||||
iconPickerViewModel.customIconPicked.observe(this) { iconCustom ->
|
||||
// Keep the standard icon if a custom one is selected
|
||||
mIconImage.custom = iconCustom
|
||||
setResult()
|
||||
finish()
|
||||
}
|
||||
iconPickerViewModel.customIconsSelected.observe(this) { iconsSelected ->
|
||||
mIconsSelected = iconsSelected
|
||||
updateIconsSelectedViews()
|
||||
}
|
||||
iconPickerViewModel.customIconAdded.observe(this) { iconCustomAdded ->
|
||||
if (iconCustomAdded.error && !iconCustomAdded.errorConsumed) {
|
||||
Snackbar.make(coordinatorLayout, iconCustomAdded.errorStringId, Snackbar.LENGTH_LONG).asError().show()
|
||||
iconCustomAdded.errorConsumed = true
|
||||
}
|
||||
uploadButton.isEnabled = true
|
||||
}
|
||||
iconPickerViewModel.customIconRemoved.observe(this) { iconCustomRemoved ->
|
||||
if (iconCustomRemoved.error && !iconCustomRemoved.errorConsumed) {
|
||||
Snackbar.make(coordinatorLayout, iconCustomRemoved.errorStringId, Snackbar.LENGTH_LONG).asError().show()
|
||||
iconCustomRemoved.errorConsumed = true
|
||||
}
|
||||
uploadButton.isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateIconsSelectedViews() {
|
||||
if (mIconsSelected.isEmpty()) {
|
||||
mCustomIconsSelectionMode = false
|
||||
toolbar.title = " "
|
||||
} else {
|
||||
mCustomIconsSelectionMode = true
|
||||
toolbar.title = mIconsSelected.size.toString()
|
||||
}
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
outState.putParcelable(EXTRA_ICON, mIconImage)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// Show the lock button
|
||||
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
// Padding if lock button visible
|
||||
toolbar.updateLockPaddingLeft()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
if (mCustomIconsSelectionMode) {
|
||||
menuInflater.inflate(R.menu.icon, menu)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
if (mCustomIconsSelectionMode) {
|
||||
iconPickerViewModel.deselectAllCustomIcons()
|
||||
} else {
|
||||
onBackPressed()
|
||||
}
|
||||
}
|
||||
R.id.menu_delete -> {
|
||||
mIconsSelected.forEach { iconToRemove ->
|
||||
removeCustomIcon(iconToRemove)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun addCustomIcon(iconToUploadUri: Uri?) {
|
||||
uploadButton.isEnabled = false
|
||||
mainScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
// on Progress with thread
|
||||
val asyncResult: Deferred<IconPickerViewModel.IconCustomState?> = async {
|
||||
val iconCustomState = IconPickerViewModel.IconCustomState(null, true, R.string.error_upload_file)
|
||||
UriUtil.getFileData(this@IconPickerActivity, iconToUploadUri)?.also { documentFile ->
|
||||
if (documentFile.length() > MAX_ICON_SIZE) {
|
||||
iconCustomState.errorStringId = R.string.error_file_to_big
|
||||
} else {
|
||||
mDatabase?.buildNewCustomIcon { customIcon, binary ->
|
||||
if (customIcon != null) {
|
||||
iconCustomState.iconCustom = customIcon
|
||||
mDatabase?.let { database ->
|
||||
BinaryDatabaseManager.resizeBitmapAndStoreDataInBinaryFile(
|
||||
contentResolver,
|
||||
database,
|
||||
iconToUploadUri,
|
||||
binary)
|
||||
when {
|
||||
binary == null -> {
|
||||
}
|
||||
binary.getSize() <= 0 -> {
|
||||
}
|
||||
database.isCustomIconBinaryDuplicate(binary) -> {
|
||||
iconCustomState.errorStringId = R.string.error_duplicate_file
|
||||
}
|
||||
else -> {
|
||||
iconCustomState.error = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if (iconCustomState.error) {
|
||||
mDatabase?.removeCustomIcon(customIcon)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
iconCustomState
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
asyncResult.await()?.let { customIcon ->
|
||||
iconPickerViewModel.addCustomIcon(customIcon)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeCustomIcon(iconImageCustom: IconImageCustom) {
|
||||
uploadButton.isEnabled = false
|
||||
iconPickerViewModel.deselectAllCustomIcons()
|
||||
mDatabase?.removeCustomIcon(iconImageCustom)
|
||||
iconPickerViewModel.removeCustomIcon(
|
||||
IconPickerViewModel.IconCustomState(iconImageCustom, false, R.string.error_remove_file)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||
addCustomIcon(uri)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setResult() {
|
||||
setResult(Activity.RESULT_OK, Intent().apply {
|
||||
putExtra(EXTRA_ICON, mIconImage)
|
||||
})
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
setResult()
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
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 launch(context: Activity,
|
||||
previousIcon: IconImage?) {
|
||||
// Create an instance to return the picker icon
|
||||
context.startActivityForResult(
|
||||
Intent(context,
|
||||
IconPickerActivity::class.java).apply {
|
||||
if (previousIcon != null)
|
||||
putExtra(EXTRA_ICON, previousIcon)
|
||||
},
|
||||
ICON_SELECTED_REQUEST)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.format.Formatter
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import com.igreenwood.loupe.Loupe
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||
import kotlin.math.max
|
||||
|
||||
class ImageViewerActivity : LockingActivity() {
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_image_viewer)
|
||||
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
Loupe.create(imageView, imageContainerView) {
|
||||
onViewTranslateListener = object : Loupe.OnViewTranslateListener {
|
||||
|
||||
override fun onStart(view: ImageView) {
|
||||
// called when the view starts moving
|
||||
}
|
||||
|
||||
override fun onViewTranslate(view: ImageView, amount: Float) {
|
||||
// called whenever the view position changed
|
||||
}
|
||||
|
||||
override fun onRestore(view: ImageView) {
|
||||
// called when the view drag gesture ended
|
||||
}
|
||||
|
||||
override fun onDismiss(view: ImageView) {
|
||||
// called when the view drag gesture ended
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> finish()
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = ImageViewerActivity::class.simpleName
|
||||
|
||||
private const val IMAGE_ATTACHMENT_TAG = "IMAGE_ATTACHMENT_TAG"
|
||||
|
||||
fun getInstance(context: Context, imageAttachment: Attachment) {
|
||||
context.startActivity(Intent(context, ImageViewerActivity::class.java).apply {
|
||||
putExtra(IMAGE_ATTACHMENT_TAG, imageAttachment)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
|
||||
/**
|
||||
* Activity to select entry in database and populate it in Magikeyboard
|
||||
*/
|
||||
class MagikeyboardLauncherActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val database = Database.getInstance()
|
||||
val readOnly = database.isReadOnly
|
||||
SearchHelper.checkAutoSearchInfo(this,
|
||||
database,
|
||||
null,
|
||||
{
|
||||
// Not called
|
||||
// if items found directly returns before calling this activity
|
||||
},
|
||||
{
|
||||
// Select if not found
|
||||
GroupActivity.launchForKeyboardSelectionResult(this, readOnly)
|
||||
},
|
||||
{
|
||||
// Pass extra to get entry
|
||||
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this)
|
||||
}
|
||||
)
|
||||
finish()
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,20 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
@@ -25,17 +25,21 @@ import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.text.Editable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||
|
||||
class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
|
||||
@@ -51,13 +55,16 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
private var passwordRepeatTextInputLayout: TextInputLayout? = null
|
||||
private var passwordRepeatView: TextView? = null
|
||||
|
||||
private var keyFileTextInputLayout: TextInputLayout? = null
|
||||
private var keyFileCheckBox: CompoundButton? = null
|
||||
private var keyFileView: TextView? = null
|
||||
private var keyFileSelectionView: KeyFileSelectionView? = null
|
||||
|
||||
private var mListener: AssignPasswordDialogListener? = null
|
||||
|
||||
private var mOpenFileHelper: OpenFileHelper? = null
|
||||
private var mExternalFileHelper: ExternalFileHelper? = 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) {}
|
||||
@@ -69,21 +76,9 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private val keyFileTextWatcher = 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) {
|
||||
keyFileCheckBox?.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
interface AssignPasswordDialogListener {
|
||||
fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?,
|
||||
keyFileChecked: Boolean, keyFile: Uri?)
|
||||
fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean, masterPassword: String?,
|
||||
keyFileChecked: Boolean, keyFile: Uri?)
|
||||
fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential)
|
||||
fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential)
|
||||
}
|
||||
|
||||
override fun onAttach(activity: Context) {
|
||||
@@ -96,6 +91,17 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
mEmptyPasswordConfirmationDialog?.dismiss()
|
||||
mEmptyPasswordConfirmationDialog = null
|
||||
mNoKeyConfirmationDialog?.dismiss()
|
||||
mNoKeyConfirmationDialog = null
|
||||
mEmptyKeyFileConfirmationDialog?.dismiss()
|
||||
mEmptyKeyFileConfirmationDialog = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
|
||||
@@ -110,24 +116,25 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
|
||||
rootView = inflater.inflate(R.layout.fragment_set_password, null)
|
||||
builder.setView(rootView)
|
||||
.setTitle(R.string.assign_master_key)
|
||||
// Add action buttons
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
|
||||
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)
|
||||
|
||||
keyFileTextInputLayout = rootView?.findViewById(R.id.keyfile_input_layout)
|
||||
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
||||
keyFileView = rootView?.findViewById(R.id.pass_keyfile)
|
||||
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
||||
|
||||
mOpenFileHelper = OpenFileHelper(this)
|
||||
rootView?.findViewById<View>(R.id.open_database_button)?.setOnClickListener { view ->
|
||||
mOpenFileHelper?.openFileOnClickViewListener?.onClick(view) }
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||
|
||||
val dialog = builder.create()
|
||||
|
||||
@@ -139,7 +146,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
mMasterPassword = ""
|
||||
mKeyFile = null
|
||||
|
||||
var error = verifyPassword() || verifyFile()
|
||||
var error = verifyPassword() || verifyKeyFile()
|
||||
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
|
||||
error = true
|
||||
if (allowNoMasterKey)
|
||||
@@ -149,17 +156,13 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
if (!error) {
|
||||
mListener?.onAssignKeyDialogPositiveClick(
|
||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
|
||||
negativeButton.setOnClickListener {
|
||||
mListener?.onAssignKeyDialogNegativeClick(
|
||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
||||
mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
@@ -171,19 +174,23 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
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)
|
||||
keyFileView?.addTextChangedListener(keyFileTextWatcher)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
passwordView?.removeTextChangedListener(passwordTextWatcher)
|
||||
keyFileView?.removeTextChangedListener(keyFileTextWatcher)
|
||||
}
|
||||
|
||||
private fun verifyPassword(): Boolean {
|
||||
@@ -211,16 +218,16 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
return error
|
||||
}
|
||||
|
||||
private fun verifyFile(): Boolean {
|
||||
private fun verifyKeyFile(): Boolean {
|
||||
var error = false
|
||||
if (keyFileCheckBox != null
|
||||
&& keyFileCheckBox!!.isChecked) {
|
||||
|
||||
UriUtil.parse(keyFileView?.text?.toString())?.let { uri ->
|
||||
keyFileSelectionView?.uri?.let { uri ->
|
||||
mKeyFile = uri
|
||||
} ?: run {
|
||||
error = true
|
||||
keyFileTextInputLayout?.error = getString(R.string.error_nokeyfile)
|
||||
keyFileSelectionView?.error = getString(R.string.error_nokeyfile)
|
||||
}
|
||||
}
|
||||
return error
|
||||
@@ -231,15 +238,14 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
val builder = AlertDialog.Builder(it)
|
||||
builder.setMessage(R.string.warning_empty_password)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
if (!verifyFile()) {
|
||||
mListener?.onAssignKeyDialogPositiveClick(
|
||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
||||
if (!verifyKeyFile()) {
|
||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||
this@AssignMasterKeyDialogFragment.dismiss()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
builder.create().show()
|
||||
mEmptyPasswordConfirmationDialog = builder.create()
|
||||
mEmptyPasswordConfirmationDialog?.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,25 +254,48 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
val builder = AlertDialog.Builder(it)
|
||||
builder.setMessage(R.string.warning_no_encryption_key)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mListener?.onAssignKeyDialogPositiveClick(
|
||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||
this@AssignMasterKeyDialogFragment.dismiss()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
builder.create().show()
|
||||
mNoKeyConfirmationDialog = builder.create()
|
||||
mNoKeyConfirmationDialog?.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showEmptyKeyFileConfirmationDialog() {
|
||||
activity?.let {
|
||||
val builder = AlertDialog.Builder(it)
|
||||
builder.setMessage(SpannableStringBuilder().apply {
|
||||
append(getString(R.string.warning_empty_keyfile))
|
||||
append("\n\n")
|
||||
append(getString(R.string.warning_empty_keyfile_explanation))
|
||||
append("\n\n")
|
||||
append(getString(R.string.warning_sure_add_file))
|
||||
})
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
keyFileCheckBox?.isChecked = false
|
||||
keyFileSelectionView?.uri = null
|
||||
}
|
||||
mEmptyKeyFileConfirmationDialog = builder.create()
|
||||
mEmptyKeyFileConfirmationDialog?.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
||||
) { uri ->
|
||||
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||
uri?.let { pathUri ->
|
||||
keyFileCheckBox?.isChecked = true
|
||||
keyFileView?.text = pathUri.toString()
|
||||
|
||||
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
||||
keyFileSelectionView?.error = null
|
||||
keyFileCheckBox?.isChecked = true
|
||||
keyFileSelectionView?.uri = pathUri
|
||||
if (lengthFile <= 0L) {
|
||||
showEmptyKeyFileConfirmationDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||
|
||||
|
||||
class DatabaseChangedDialogFragment : DialogFragment() {
|
||||
|
||||
var actionDatabaseListener: ActionDatabaseChangedListener? = null
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
actionDatabaseListener = null
|
||||
this.dismiss()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
|
||||
val oldSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelable(OLD_FILE_DATABASE_INFO)
|
||||
val newSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelable(NEW_FILE_DATABASE_INFO)
|
||||
|
||||
if (oldSnapFileDatabaseInfo != null && newSnapFileDatabaseInfo != null) {
|
||||
// Use the Builder class for convenient dialog construction
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
val stringBuilder = SpannableStringBuilder()
|
||||
if (newSnapFileDatabaseInfo.exists) {
|
||||
stringBuilder.append(getString(R.string.warning_database_info_changed))
|
||||
stringBuilder.append("\n\n" +oldSnapFileDatabaseInfo.toString(activity)
|
||||
+ "\n→\n" +
|
||||
newSnapFileDatabaseInfo.toString(activity) + "\n\n")
|
||||
stringBuilder.append(getString(R.string.warning_database_info_changed_options))
|
||||
} else {
|
||||
stringBuilder.append(getString(R.string.warning_database_revoked))
|
||||
}
|
||||
builder.setMessage(stringBuilder)
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
actionDatabaseListener?.validateDatabaseChanged()
|
||||
}
|
||||
return builder.create()
|
||||
}
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
interface ActionDatabaseChangedListener {
|
||||
fun validateDatabaseChanged()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val DATABASE_CHANGED_DIALOG_TAG = "databaseChangedDialogFragment"
|
||||
private const val OLD_FILE_DATABASE_INFO = "OLD_FILE_DATABASE_INFO"
|
||||
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
|
||||
|
||||
fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||
newSnapFileDatabaseInfo: SnapFileDatabaseInfo)
|
||||
: DatabaseChangedDialogFragment {
|
||||
val fragment = DatabaseChangedDialogFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
putParcelable(OLD_FILE_DATABASE_INFO, oldSnapFileDatabaseInfo)
|
||||
putParcelable(NEW_FILE_DATABASE_INFO, newSnapFileDatabaseInfo)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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 androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
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
|
||||
|
||||
open class DeleteNodesDialogFragment : DialogFragment() {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
activity?.let { activity ->
|
||||
// Use the Builder class for convenient dialog construction
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
builder.setMessage(retrieveMessage())
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mListener?.permanentlyDeleteNodes(mNodesToDelete)
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||
// Create the AlertDialog object and return it
|
||||
return builder.create()
|
||||
}
|
||||
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 {
|
||||
return DeleteNodesDialogFragment().apply {
|
||||
arguments = getBundleFromListNodes(nodesToDelete)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,27 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* 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 androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
@@ -31,7 +32,7 @@ class DuplicateUuidDialog : DialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
// Use the Builder class for convenient dialog construction
|
||||
val builder = androidx.appcompat.app.AlertDialog.Builder(activity).apply {
|
||||
val builder = AlertDialog.Builder(activity).apply {
|
||||
val message = getString(R.string.contains_duplicate_uuid) +
|
||||
"\n\n" + getString(R.string.contains_duplicate_uuid_procedure)
|
||||
setMessage(message)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||
|
||||
class EmptyRecycleBinDialogFragment : DeleteNodesDialogFragment() {
|
||||
|
||||
override fun retrieveMessage(): String {
|
||||
return getString(R.string.warning_empty_recycle_bin)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getInstance(nodesToDelete: List<Node>): EmptyRecycleBinDialogFragment {
|
||||
return EmptyRecycleBinDialogFragment().apply {
|
||||
arguments = getBundleFromListNodes(nodesToDelete)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.Button
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.model.Field
|
||||
|
||||
|
||||
class EntryCustomFieldDialogFragment: DialogFragment() {
|
||||
|
||||
private var oldField: Field? = null
|
||||
|
||||
private var entryCustomFieldListener: EntryCustomFieldListener? = null
|
||||
|
||||
private var customFieldLabelContainer: TextInputLayout? = null
|
||||
private var customFieldLabel: TextView? = null
|
||||
private var customFieldDeleteButton: ImageView? = null
|
||||
private var customFieldProtectionButton: CompoundButton? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
try {
|
||||
entryCustomFieldListener = context as EntryCustomFieldListener
|
||||
} catch (e: ClassCastException) {
|
||||
// The activity doesn't implement the interface, throw exception
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + EntryCustomFieldListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
entryCustomFieldListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_entry_new_field, null)
|
||||
customFieldLabelContainer = root?.findViewById(R.id.entry_custom_field_label_container)
|
||||
customFieldLabel = root?.findViewById(R.id.entry_custom_field_label)
|
||||
customFieldDeleteButton = root?.findViewById(R.id.entry_custom_field_delete)
|
||||
customFieldProtectionButton = root?.findViewById(R.id.entry_custom_field_protection)
|
||||
|
||||
oldField = arguments?.getParcelable(KEY_FIELD)
|
||||
oldField?.let { oldCustomField ->
|
||||
customFieldLabel?.text = oldCustomField.name
|
||||
customFieldProtectionButton?.isChecked = oldCustomField.protectedValue.isProtected
|
||||
|
||||
customFieldDeleteButton?.visibility = View.VISIBLE
|
||||
customFieldDeleteButton?.setOnClickListener {
|
||||
entryCustomFieldListener?.onDeleteCustomFieldApproved(oldCustomField)
|
||||
(dialog as AlertDialog?)?.dismiss()
|
||||
}
|
||||
} ?: run {
|
||||
customFieldDeleteButton?.visibility = View.GONE
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setView(root)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
val dialogCreated = builder.create()
|
||||
|
||||
customFieldLabel?.requestFocus()
|
||||
customFieldLabel?.imeOptions = EditorInfo.IME_ACTION_DONE
|
||||
customFieldLabel?.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
approveIfValid()
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
dialogCreated.window?.setSoftInputMode(SOFT_INPUT_STATE_VISIBLE)
|
||||
return dialogCreated
|
||||
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// To prevent auto dismiss
|
||||
val d = dialog as AlertDialog?
|
||||
if (d != null) {
|
||||
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
|
||||
positiveButton.setOnClickListener {
|
||||
approveIfValid()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun approveIfValid() {
|
||||
if (isValid()) {
|
||||
oldField?.let {
|
||||
// New property with old value
|
||||
entryCustomFieldListener?.onEditCustomFieldApproved(it,
|
||||
Field(customFieldLabel?.text?.toString() ?: "",
|
||||
ProtectedString(customFieldProtectionButton?.isChecked == true,
|
||||
it.protectedValue.stringValue))
|
||||
)
|
||||
} ?: run {
|
||||
entryCustomFieldListener?.onNewCustomFieldApproved(
|
||||
Field(customFieldLabel?.text?.toString() ?: "",
|
||||
ProtectedString(customFieldProtectionButton?.isChecked == true))
|
||||
)
|
||||
}
|
||||
(dialog as AlertDialog?)?.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValid(): Boolean {
|
||||
return if (customFieldLabel?.text?.toString()?.isNotEmpty() != true) {
|
||||
setError(R.string.error_string_key)
|
||||
false
|
||||
} else {
|
||||
setError(null)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun setError(@StringRes errorId: Int?) {
|
||||
customFieldLabelContainer?.error = if (errorId == null) null else {
|
||||
requireContext().getString(errorId)
|
||||
}
|
||||
}
|
||||
|
||||
interface EntryCustomFieldListener {
|
||||
fun onNewCustomFieldApproved(newField: Field)
|
||||
fun onEditCustomFieldApproved(oldField: Field, newField: Field)
|
||||
fun onDeleteCustomFieldApproved(oldField: Field)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_FIELD = "KEY_FIELD"
|
||||
|
||||
fun getInstance(): EntryCustomFieldDialogFragment {
|
||||
return EntryCustomFieldDialogFragment()
|
||||
}
|
||||
|
||||
fun getInstance(field: Field): EntryCustomFieldDialogFragment {
|
||||
return EntryCustomFieldDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(KEY_FIELD, field)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
@@ -28,7 +28,7 @@ import android.widget.TextView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
class BrowserDialogFragment : DialogFragment() {
|
||||
class FileManagerDialogFragment : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
@@ -41,15 +41,8 @@ class BrowserDialogFragment : DialogFragment() {
|
||||
val textDescription = root.findViewById<TextView>(R.id.file_manager_install_description)
|
||||
textDescription.text = getString(R.string.file_manager_install_description)
|
||||
|
||||
val market = root.findViewById<Button>(R.id.file_manager_install_play_store)
|
||||
market.setOnClickListener {
|
||||
UriUtil.gotoUrl(context!!, R.string.file_manager_play_store)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
val web = root.findViewById<Button>(R.id.file_manager_install_f_droid)
|
||||
web.setOnClickListener {
|
||||
UriUtil.gotoUrl(context!!, R.string.file_manager_f_droid)
|
||||
root.findViewById<Button>(R.id.file_manager_button).setOnClickListener {
|
||||
UriUtil.gotoUrl(requireContext(), R.string.file_manager_explanation_url)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
/**
|
||||
* Custom Dialog to confirm big file to upload
|
||||
*/
|
||||
class FileTooBigDialogFragment : DialogFragment() {
|
||||
|
||||
private var mActionChooseListener: ActionChooseListener? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
// Verify that the host activity implements the callback interface
|
||||
try {
|
||||
mActionChooseListener = context as ActionChooseListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + ActionChooseListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mActionChooseListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
// Use the Builder class for convenient dialog construction
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setMessage(SpannableStringBuilder().apply {
|
||||
append(getString(R.string.warning_file_too_big))
|
||||
append("\n\n")
|
||||
append(getString(R.string.warning_sure_add_file))
|
||||
})
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mActionChooseListener?.onValidateUploadFileTooBig(
|
||||
arguments?.getParcelable(KEY_FILE_URI),
|
||||
arguments?.getString(KEY_FILE_NAME))
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
dismiss()
|
||||
}
|
||||
// Create the AlertDialog object and return it
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
interface ActionChooseListener {
|
||||
fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MAX_WARNING_BINARY_FILE = 5242880
|
||||
|
||||
private const val KEY_FILE_URI = "KEY_FILE_URI"
|
||||
private const val KEY_FILE_NAME = "KEY_FILE_NAME"
|
||||
|
||||
fun build(attachmentToUploadUri: Uri,
|
||||
fileName: String): FileTooBigDialogFragment {
|
||||
val fragment = FileTooBigDialogFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
putParcelable(KEY_FILE_URI, attachmentToUploadUri)
|
||||
putString(KEY_FILE_NAME, fileName)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
@@ -26,13 +26,11 @@ import com.google.android.material.textfield.TextInputLayout
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.EditText
|
||||
import android.widget.SeekBar
|
||||
import android.widget.*
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.password.PasswordGenerator
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||
|
||||
class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
@@ -64,6 +62,11 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
@@ -73,6 +76,15 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
passwordInputLayoutView = root?.findViewById(R.id.password_input_layout)
|
||||
passwordView = root?.findViewById(R.id.password)
|
||||
passwordView?.applyFontVisibility()
|
||||
val passwordCopyView: ImageView? = root?.findViewById(R.id.password_copy_button)
|
||||
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyPasswordAndProtectedFields(activity))
|
||||
View.VISIBLE else View.GONE
|
||||
val clipboardHelper = ClipboardHelper(activity)
|
||||
passwordCopyView?.setOnClickListener {
|
||||
clipboardHelper.timeoutCopyToClipboard(passwordView!!.text.toString(),
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_password)))
|
||||
}
|
||||
|
||||
lengthTextView = root?.findViewById(R.id.length)
|
||||
|
||||
@@ -167,7 +179,6 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
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,
|
||||
@@ -178,6 +189,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
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) {
|
||||
@@ -193,7 +205,6 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val KEY_PASSWORD_ID = "KEY_PASSWORD_ID"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
@@ -23,34 +23,40 @@ import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.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.IconPickerActivity
|
||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION
|
||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.GroupVersioned
|
||||
import com.kunzisoft.keepass.database.element.PwIcon
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
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
|
||||
|
||||
class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconPickerListener {
|
||||
class GroupEditDialogFragment : DialogFragment() {
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
|
||||
private var editGroupListener: EditGroupListener? = null
|
||||
private var mEditGroupListener: EditGroupListener? = null
|
||||
|
||||
private var editGroupDialogAction: EditGroupDialogAction? = null
|
||||
private var nameGroup: String? = null
|
||||
private var iconGroup: PwIcon? = null
|
||||
private var mEditGroupDialogAction = EditGroupDialogAction.NONE
|
||||
private var mGroupInfo = GroupInfo()
|
||||
|
||||
private var nameTextLayoutView: TextInputLayout? = null
|
||||
private var nameTextView: TextView? = null
|
||||
private var iconButtonView: ImageView? = null
|
||||
private lateinit var iconButtonView: ImageView
|
||||
private var iconColor: 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
|
||||
|
||||
enum class EditGroupDialogAction {
|
||||
CREATION, UPDATE, NONE;
|
||||
@@ -67,21 +73,28 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
||||
// Verify that the host activity implements the callback interface
|
||||
try {
|
||||
// Instantiate the NoticeDialogListener so we can send events to the host
|
||||
editGroupListener = context as EditGroupListener
|
||||
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 onDetach() {
|
||||
mEditGroupListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_group_edit, null)
|
||||
nameTextLayoutView = root?.findViewById(R.id.group_edit_name_container)
|
||||
nameTextView = root?.findViewById(R.id.group_edit_name)
|
||||
iconButtonView = root?.findViewById(R.id.group_edit_icon_button)
|
||||
iconButtonView = root.findViewById(R.id.group_edit_icon_button)
|
||||
nameTextLayoutView = root.findViewById(R.id.group_edit_name_container)
|
||||
nameTextView = root.findViewById(R.id.group_edit_name)
|
||||
notesTextLayoutView = root.findViewById(R.id.group_edit_note_container)
|
||||
notesTextView = root.findViewById(R.id.group_edit_note)
|
||||
expirationView = root.findViewById(R.id.group_edit_expiration)
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
@@ -90,49 +103,47 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
||||
|
||||
// Init elements
|
||||
mDatabase = Database.getInstance()
|
||||
editGroupDialogAction = EditGroupDialogAction.NONE
|
||||
nameGroup = ""
|
||||
iconGroup = mDatabase?.iconFactory?.folderIcon
|
||||
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(KEY_ACTION_ID)
|
||||
&& savedInstanceState.containsKey(KEY_NAME)
|
||||
&& savedInstanceState.containsKey(KEY_ICON)) {
|
||||
editGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID))
|
||||
nameGroup = savedInstanceState.getString(KEY_NAME)
|
||||
iconGroup = savedInstanceState.getParcelable(KEY_ICON)
|
||||
|
||||
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
|
||||
mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID))
|
||||
mGroupInfo = savedInstanceState.getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
||||
} else {
|
||||
arguments?.apply {
|
||||
if (containsKey(KEY_ACTION_ID))
|
||||
editGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID))
|
||||
|
||||
if (containsKey(KEY_NAME) && containsKey(KEY_ICON)) {
|
||||
nameGroup = getString(KEY_NAME)
|
||||
iconGroup = getParcelable(KEY_ICON)
|
||||
mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID))
|
||||
if (containsKey(KEY_GROUP_INFO)) {
|
||||
mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// populate the name
|
||||
nameTextView?.text = nameGroup
|
||||
// populate the icon
|
||||
assignIconView()
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setView(root)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
editGroupListener?.cancelEditGroup(
|
||||
editGroupDialogAction,
|
||||
nameTextView?.text?.toString(),
|
||||
iconGroup)
|
||||
retrieveGroupInfoFromViews()
|
||||
mEditGroupListener?.cancelEditGroup(
|
||||
mEditGroupDialogAction,
|
||||
mGroupInfo)
|
||||
}
|
||||
|
||||
iconButtonView?.setOnClickListener { _ ->
|
||||
fragmentManager?.let {
|
||||
IconPickerDialogFragment().show(it, "IconPickerDialogFragment")
|
||||
}
|
||||
iconButtonView.setOnClickListener { _ ->
|
||||
IconPickerActivity.launch(activity, mGroupInfo.icon)
|
||||
}
|
||||
|
||||
return builder.create()
|
||||
@@ -148,69 +159,104 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
||||
if (d != null) {
|
||||
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
|
||||
positiveButton.setOnClickListener {
|
||||
retrieveGroupInfoFromViews()
|
||||
if (isValid()) {
|
||||
editGroupListener?.approveEditGroup(
|
||||
editGroupDialogAction,
|
||||
nameTextView?.text?.toString(),
|
||||
iconGroup)
|
||||
mEditGroupListener?.approveEditGroup(
|
||||
mEditGroupDialogAction,
|
||||
mGroupInfo)
|
||||
d.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun assignIconView() {
|
||||
if (mDatabase?.drawFactory != null && iconGroup != null) {
|
||||
iconButtonView?.assignDatabaseIcon(mDatabase?.drawFactory!!, iconGroup!!, iconColor)
|
||||
}
|
||||
fun getExpiryTime(): DateInstant {
|
||||
retrieveGroupInfoFromViews()
|
||||
return mGroupInfo.expiryTime
|
||||
}
|
||||
|
||||
override fun iconPicked(bundle: Bundle) {
|
||||
iconGroup = IconPickerDialogFragment.getIconStandardFromBundle(bundle)
|
||||
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 {
|
||||
notesTextView.text = it
|
||||
}
|
||||
expirationView.expires = mGroupInfo.expires
|
||||
expirationView.expiryTime = mGroupInfo.expiryTime
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putInt(KEY_ACTION_ID, editGroupDialogAction!!.ordinal)
|
||||
outState.putString(KEY_NAME, nameGroup)
|
||||
outState.putParcelable(KEY_ICON, iconGroup)
|
||||
retrieveGroupInfoFromViews()
|
||||
outState.putInt(KEY_ACTION_ID, mEditGroupDialogAction.ordinal)
|
||||
outState.putParcelable(KEY_GROUP_INFO, mGroupInfo)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
private fun isValid(): Boolean {
|
||||
if (nameTextView?.text?.toString()?.isNotEmpty() != true) {
|
||||
nameTextLayoutView?.error = getString(R.string.error_no_name)
|
||||
return false
|
||||
val error = mEditGroupListener?.isValidGroupName(nameTextView.text.toString()) ?: Error(false, null)
|
||||
error.messageId?.let { messageId ->
|
||||
nameTextLayoutView.error = getString(messageId)
|
||||
} ?: kotlin.run {
|
||||
nameTextLayoutView.error = null
|
||||
}
|
||||
return true
|
||||
return !error.isError
|
||||
}
|
||||
|
||||
data class Error(val isError: Boolean, val messageId: Int?)
|
||||
|
||||
interface EditGroupListener {
|
||||
fun approveEditGroup(action: EditGroupDialogAction?, name: String?, icon: PwIcon?)
|
||||
fun cancelEditGroup(action: EditGroupDialogAction?, name: String?, icon: PwIcon?)
|
||||
fun isValidGroupName(name: String): Error
|
||||
fun approveEditGroup(action: EditGroupDialogAction,
|
||||
groupInfo: GroupInfo)
|
||||
fun cancelEditGroup(action: EditGroupDialogAction,
|
||||
groupInfo: GroupInfo)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
|
||||
|
||||
const val KEY_NAME = "KEY_NAME"
|
||||
const val KEY_ICON = "KEY_ICON"
|
||||
const val KEY_ACTION_ID = "KEY_ACTION_ID"
|
||||
const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
||||
|
||||
fun build(): GroupEditDialogFragment {
|
||||
fun create(groupInfo: GroupInfo): GroupEditDialogFragment {
|
||||
val bundle = Bundle()
|
||||
bundle.putInt(KEY_ACTION_ID, CREATION.ordinal)
|
||||
bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
|
||||
val fragment = GroupEditDialogFragment()
|
||||
fragment.arguments = bundle
|
||||
return fragment
|
||||
}
|
||||
|
||||
fun build(group: GroupVersioned): GroupEditDialogFragment {
|
||||
fun update(groupInfo: GroupInfo): GroupEditDialogFragment {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(KEY_NAME, group.title)
|
||||
bundle.putParcelable(KEY_ICON, group.icon)
|
||||
bundle.putInt(KEY_ACTION_ID, UPDATE.ordinal)
|
||||
bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
|
||||
val fragment = GroupEditDialogFragment()
|
||||
fragment.arguments = bundle
|
||||
return fragment
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.core.widget.ImageViewCompat
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.GridView
|
||||
import android.widget.ImageView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.database.element.PwIconStandard
|
||||
import com.kunzisoft.keepass.icons.IconPack
|
||||
import com.kunzisoft.keepass.icons.IconPackChooser
|
||||
|
||||
|
||||
class IconPickerDialogFragment : DialogFragment() {
|
||||
|
||||
private var iconPickerListener: IconPickerListener? = null
|
||||
private var iconPack: IconPack? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
try {
|
||||
iconPickerListener = context as IconPickerListener
|
||||
} catch (e: ClassCastException) {
|
||||
// The activity doesn't implement the interface, throw exception
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + IconPickerListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
iconPack = IconPackChooser.getSelectedIconPack(context!!)
|
||||
|
||||
// Inflate and set the layout for the dialog
|
||||
// Pass null as the parent view because its going in the dialog layout
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_icon_picker, null)
|
||||
builder.setView(root)
|
||||
|
||||
val currIconGridView = root.findViewById<GridView>(R.id.IconGridView)
|
||||
currIconGridView.adapter = ImageAdapter(activity)
|
||||
|
||||
currIconGridView.setOnItemClickListener { _, _, position, _ ->
|
||||
val bundle = Bundle()
|
||||
bundle.putParcelable(KEY_ICON_STANDARD, PwIconStandard(position))
|
||||
iconPickerListener?.iconPicked(bundle)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() }
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
inner class ImageAdapter internal constructor(private val context: Context) : BaseAdapter() {
|
||||
|
||||
override fun getCount(): Int {
|
||||
return iconPack?.numberOfIcons() ?: 0
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): Any? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val currentView: View = convertView
|
||||
?: (context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)
|
||||
.inflate(R.layout.item_icon, parent, false)
|
||||
|
||||
iconPack?.let { iconPack ->
|
||||
val iconImageView = currentView.findViewById<ImageView>(R.id.icon_image)
|
||||
iconImageView.setImageResource(iconPack.iconToResId(position))
|
||||
|
||||
// Assign color if icons are tintable
|
||||
if (iconPack.tintable()) {
|
||||
// Retrieve the textColor to tint the icon
|
||||
val ta = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
ImageViewCompat.setImageTintList(iconImageView, ColorStateList.valueOf(ta.getColor(0, Color.BLACK)))
|
||||
ta?.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
return currentView
|
||||
}
|
||||
}
|
||||
|
||||
interface IconPickerListener {
|
||||
fun iconPicked(bundle: Bundle)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_ICON_STANDARD = "KEY_ICON_STANDARD"
|
||||
|
||||
fun getIconStandardFromBundle(bundle: Bundle): PwIconStandard? {
|
||||
return bundle.getParcelable(KEY_ICON_STANDARD)
|
||||
}
|
||||
|
||||
fun launch(activity: StylishActivity) {
|
||||
// Create an instance of the dialog fragment and show it
|
||||
val dialog = IconPickerDialogFragment()
|
||||
dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,66 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
|
||||
class PasswordEncodingDialogFragment : DialogFragment() {
|
||||
|
||||
var positiveButtonClickListener: DialogInterface.OnClickListener? = null
|
||||
private var mListener: Listener? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
try {
|
||||
mListener = context as Listener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + Listener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
||||
val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY)
|
||||
val mainCredential: MainCredential = savedInstanceState?.getParcelable(MAIN_CREDENTIAL) ?: MainCredential()
|
||||
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning)
|
||||
builder.setPositiveButton(android.R.string.ok, positiveButtonClickListener)
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mListener?.onPasswordEncodingValidateListener(
|
||||
databaseUri,
|
||||
mainCredential
|
||||
)
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
|
||||
|
||||
return builder.create()
|
||||
@@ -42,5 +68,24 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun onPasswordEncodingValidateListener(databaseUri: Uri?,
|
||||
mainCredential: MainCredential)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
|
||||
private const val MAIN_CREDENTIAL = "MAIN_CREDENTIAL"
|
||||
|
||||
fun getInstance(databaseUri: Uri,
|
||||
mainCredential: MainCredential): SortDialogFragment {
|
||||
val fragment = SortDialogFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
putParcelable(DATABASE_URI_KEY, databaseUri)
|
||||
putParcelable(MAIN_CREDENTIAL, mainCredential)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
@@ -45,13 +45,13 @@ class ProFeatureDialogFragment : DialogFragment() {
|
||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_ad_free), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_buy_pro), FROM_HTML_MODE_LEGACY))
|
||||
builder.setPositiveButton(R.string.download) { _, _ ->
|
||||
UriUtil.gotoUrl(context!!, R.string.app_pro_url)
|
||||
UriUtil.gotoUrl(requireContext(), R.string.app_pro_url)
|
||||
}
|
||||
} 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(context!!, R.string.contribution_url)
|
||||
UriUtil.gotoUrl(requireContext(), R.string.contribution_url)
|
||||
}
|
||||
}
|
||||
builder.setMessage(stringBuilder)
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
class ReadOnlyDialog : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
// Use the Builder class for convenient dialog construction
|
||||
val builder = androidx.appcompat.app.AlertDialog.Builder(activity)
|
||||
|
||||
var warning = getString(R.string.read_only_warning)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
warning = warning + "\n\n" + getString(R.string.read_only_kitkat_warning)
|
||||
}
|
||||
builder.setMessage(warning)
|
||||
|
||||
builder.setPositiveButton(getString(android.R.string.ok)) { _, _ -> dismiss() }
|
||||
builder.setNegativeButton(getString(R.string.beta_dontask)) { _, _ ->
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val edit = prefs.edit()
|
||||
edit.putBoolean(getString(R.string.show_read_only_warning), false)
|
||||
edit.apply()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
// Create the AlertDialog object and return it
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
|
||||
/**
|
||||
* Custom Dialog to confirm big file to upload
|
||||
*/
|
||||
class ReplaceFileDialogFragment : DialogFragment() {
|
||||
|
||||
private var mActionChooseListener: ActionChooseListener? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
// Verify that the host activity implements the callback interface
|
||||
try {
|
||||
mActionChooseListener = context as ActionChooseListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + ActionChooseListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mActionChooseListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
// Use the Builder class for convenient dialog construction
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setMessage(SpannableStringBuilder().apply {
|
||||
append(getString(R.string.warning_replace_file))
|
||||
append("\n\n")
|
||||
append(getString(R.string.warning_sure_add_file))
|
||||
})
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mActionChooseListener?.onValidateReplaceFile(
|
||||
arguments?.getParcelable(KEY_FILE_URI),
|
||||
arguments?.getParcelable(KEY_ENTRY_ATTACHMENT))
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
dismiss()
|
||||
}
|
||||
// Create the AlertDialog object and return it
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
interface ActionChooseListener {
|
||||
fun onValidateReplaceFile(attachmentToUploadUri: Uri?, attachment: Attachment?)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_FILE_URI = "KEY_FILE_URI"
|
||||
private const val KEY_ENTRY_ATTACHMENT = "KEY_ENTRY_ATTACHMENT"
|
||||
|
||||
fun build(attachmentToUploadUri: Uri,
|
||||
attachment: Attachment): ReplaceFileDialogFragment {
|
||||
val fragment = ReplaceFileDialogFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
putParcelable(KEY_FILE_URI, attachmentToUploadUri)
|
||||
putParcelable(KEY_ENTRY_ATTACHMENT, attachment)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.annotation.SuppressLint
|
||||
@@ -9,10 +28,8 @@ import android.text.TextWatcher
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.Spinner
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
@@ -29,6 +46,8 @@ 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.*
|
||||
|
||||
class SetOTPDialogFragment : DialogFragment() {
|
||||
|
||||
@@ -36,6 +55,7 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
|
||||
private var mOtpElement: OtpElement = OtpElement()
|
||||
|
||||
private var otpTypeMessage: TextView? = null
|
||||
private var otpTypeSpinner: Spinner? = null
|
||||
private var otpTokenTypeSpinner: Spinner? = null
|
||||
private var otpSecretContainer: TextInputLayout? = null
|
||||
@@ -53,6 +73,8 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
private var totpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
||||
private var hotpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
||||
private var otpAlgorithmAdapter: ArrayAdapter<TokenCalculator.HashAlgorithm>? = null
|
||||
private var mHotpTokenTypeArray: Array<OtpTokenType>? = null
|
||||
private var mTotpTokenTypeArray: Array<OtpTokenType>? = null
|
||||
|
||||
private var mManualEvent = false
|
||||
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
|
||||
@@ -69,9 +91,9 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
}
|
||||
|
||||
private var mSecretWellFormed = false
|
||||
private var mCounterWellFormed = true
|
||||
private var mPeriodWellFormed = true
|
||||
private var mDigitsWellFormed = true
|
||||
private var mCounterWellFormed = false
|
||||
private var mPeriodWellFormed = false
|
||||
private var mDigitsWellFormed = false
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
@@ -86,6 +108,11 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mCreateOTPElementListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
|
||||
@@ -108,6 +135,7 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
|
||||
activity?.let { activity ->
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_set_otp, null) as ViewGroup?
|
||||
otpTypeMessage = root?.findViewById(R.id.setup_otp_type_message)
|
||||
otpTypeSpinner = root?.findViewById(R.id.setup_otp_type)
|
||||
otpTokenTypeSpinner = root?.findViewById(R.id.setup_otp_token_type)
|
||||
otpSecretContainer = root?.findViewById(R.id.setup_otp_secret_label)
|
||||
@@ -132,26 +160,48 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
otpCounterTextView?.setOnTouchListener(mOnTouchListener)
|
||||
otpDigitsTextView?.setOnTouchListener(mOnTouchListener)
|
||||
|
||||
// To manage focus
|
||||
otpPeriodTextView?.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_NEXT) {
|
||||
otpDigitsTextView?.requestFocus()
|
||||
true
|
||||
} else
|
||||
false
|
||||
}
|
||||
otpCounterTextView?.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_NEXT) {
|
||||
otpDigitsTextView?.requestFocus()
|
||||
true
|
||||
} else
|
||||
false
|
||||
}
|
||||
otpCounterTextView?.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_NEXT) {
|
||||
root?.requestFocus(View.FOCUS_DOWN)
|
||||
true
|
||||
} else
|
||||
false
|
||||
}
|
||||
|
||||
// HOTP / TOTP Type selection
|
||||
val otpTypeArray = OtpType.values()
|
||||
otpTypeAdapter = ArrayAdapter<OtpType>(activity,
|
||||
otpTypeAdapter = ArrayAdapter(activity,
|
||||
android.R.layout.simple_spinner_item, otpTypeArray).apply {
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
}
|
||||
otpTypeSpinner?.adapter = otpTypeAdapter
|
||||
|
||||
// Otp Token type selection
|
||||
val hotpTokenTypeArray = OtpTokenType.getHotpTokenTypeValues()
|
||||
mHotpTokenTypeArray = OtpTokenType.getHotpTokenTypeValues()
|
||||
hotpTokenTypeAdapter = ArrayAdapter(activity,
|
||||
android.R.layout.simple_spinner_item, hotpTokenTypeArray).apply {
|
||||
android.R.layout.simple_spinner_item, mHotpTokenTypeArray!!).apply {
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
}
|
||||
// Proprietary only on closed and full version
|
||||
val totpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
|
||||
mTotpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
|
||||
BuildConfig.CLOSED_STORE && BuildConfig.FULL_VERSION)
|
||||
totpTokenTypeAdapter = ArrayAdapter(activity,
|
||||
android.R.layout.simple_spinner_item, totpTokenTypeArray).apply {
|
||||
android.R.layout.simple_spinner_item, mTotpTokenTypeArray!!).apply {
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
}
|
||||
otpTokenTypeAdapter = hotpTokenTypeAdapter
|
||||
@@ -159,7 +209,7 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
|
||||
// OTP Algorithm
|
||||
val otpAlgorithmArray = TokenCalculator.HashAlgorithm.values()
|
||||
otpAlgorithmAdapter = ArrayAdapter<TokenCalculator.HashAlgorithm>(activity,
|
||||
otpAlgorithmAdapter = ArrayAdapter(activity,
|
||||
android.R.layout.simple_spinner_item, otpAlgorithmArray).apply {
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
}
|
||||
@@ -174,13 +224,16 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.apply {
|
||||
setTitle(R.string.entry_setup_otp)
|
||||
setView(root)
|
||||
.setPositiveButton(android.R.string.ok) {_, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
}
|
||||
}
|
||||
|
||||
root?.findViewById<View>(R.id.otp_information)?.setOnClickListener {
|
||||
UriUtil.gotoUrl(activity, R.string.otp_explanation_url)
|
||||
}
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
@@ -246,7 +299,7 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
s?.toString()?.let { userString ->
|
||||
try {
|
||||
mOtpElement.setBase32Secret(userString)
|
||||
mOtpElement.setBase32Secret(userString.toUpperCase(Locale.ENGLISH))
|
||||
otpSecretContainer?.error = null
|
||||
} catch (exception: Exception) {
|
||||
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
|
||||
@@ -324,35 +377,63 @@ class SetOTPDialogFragment : DialogFragment() {
|
||||
}
|
||||
|
||||
private fun upgradeTokenType() {
|
||||
val tokenType = mOtpElement.tokenType
|
||||
when (mOtpElement.type) {
|
||||
OtpType.HOTP -> {
|
||||
otpPeriodContainer?.visibility = View.GONE
|
||||
otpCounterContainer?.visibility = View.VISIBLE
|
||||
otpTokenTypeSpinner?.adapter = hotpTokenTypeAdapter
|
||||
otpTokenTypeSpinner?.setSelection(OtpTokenType
|
||||
.getHotpTokenTypeValues().indexOf(mOtpElement.tokenType))
|
||||
mHotpTokenTypeArray?.let { otpTokenTypeArray ->
|
||||
defineOtpTokenTypeSpinner(otpTokenTypeArray, tokenType, OtpTokenType.RFC4226)
|
||||
}
|
||||
}
|
||||
OtpType.TOTP -> {
|
||||
otpPeriodContainer?.visibility = View.VISIBLE
|
||||
otpCounterContainer?.visibility = View.GONE
|
||||
otpTokenTypeSpinner?.adapter = totpTokenTypeAdapter
|
||||
otpTokenTypeSpinner?.setSelection(OtpTokenType
|
||||
.getTotpTokenTypeValues().indexOf(mOtpElement.tokenType))
|
||||
mTotpTokenTypeArray?.let { otpTokenTypeArray ->
|
||||
defineOtpTokenTypeSpinner(otpTokenTypeArray, tokenType, OtpTokenType.RFC6238)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun defineOtpTokenTypeSpinner(otpTokenTypeArray: Array<OtpTokenType>,
|
||||
tokenType: OtpTokenType,
|
||||
defaultTokenType: OtpTokenType) {
|
||||
val formTokenType = if (otpTokenTypeArray.contains(tokenType)) {
|
||||
otpTypeMessage?.visibility = View.GONE
|
||||
tokenType
|
||||
} else {
|
||||
otpTypeMessage?.visibility = View.VISIBLE
|
||||
defaultTokenType
|
||||
}
|
||||
otpTokenTypeSpinner?.setSelection(otpTokenTypeArray.indexOf(formTokenType))
|
||||
}
|
||||
|
||||
private fun upgradeParameters() {
|
||||
otpAlgorithmSpinner?.setSelection(TokenCalculator.HashAlgorithm.values()
|
||||
.indexOf(mOtpElement.algorithm))
|
||||
|
||||
val secret = mOtpElement.getBase32Secret()
|
||||
otpSecretTextView?.apply {
|
||||
setText(mOtpElement.getBase32Secret())
|
||||
setText(secret)
|
||||
// Cursor at end
|
||||
setSelection(this.text.length)
|
||||
}
|
||||
otpCounterTextView?.setText(mOtpElement.counter.toString())
|
||||
otpPeriodTextView?.setText(mOtpElement.period.toString())
|
||||
otpDigitsTextView?.setText(mOtpElement.digits.toString())
|
||||
mSecretWellFormed = OtpElement.isValidBase32(secret)
|
||||
|
||||
val counter = mOtpElement.counter
|
||||
otpCounterTextView?.setText(counter.toString())
|
||||
mCounterWellFormed = OtpElement.isValidCounter(counter)
|
||||
|
||||
val period = mOtpElement.period
|
||||
otpPeriodTextView?.setText(period.toString())
|
||||
mPeriodWellFormed = OtpElement.isValidPeriod(period)
|
||||
|
||||
val digits = mOtpElement.digits
|
||||
otpDigitsTextView?.setText(digits.toString())
|
||||
mDigitsWellFormed = OtpElement.isValidDigits(digits)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
@@ -29,7 +29,7 @@ import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.RadioGroup
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.SortNodeEnum
|
||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||
|
||||
class SortDialogFragment : DialogFragment() {
|
||||
|
||||
@@ -54,6 +54,11 @@ class SortDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
mListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
@@ -82,7 +87,9 @@ class SortDialogFragment : DialogFragment() {
|
||||
builder.setView(rootView)
|
||||
// Add action buttons
|
||||
.setPositiveButton(android.R.string.ok
|
||||
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum, mAscending, mGroupsBefore, mRecycleBinBottom) }
|
||||
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum,
|
||||
SortNodeEnum.SortNodeParameters(mAscending, mGroupsBefore, mRecycleBinBottom))
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
|
||||
val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending)
|
||||
@@ -150,10 +157,7 @@ class SortDialogFragment : DialogFragment() {
|
||||
}
|
||||
|
||||
interface SortSelectionListener {
|
||||
fun onSortSelected(sortNodeEnum: SortNodeEnum,
|
||||
ascending: Boolean,
|
||||
groupsBefore: Boolean,
|
||||
recycleBinBottom: Boolean)
|
||||
fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
@@ -24,7 +24,6 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.text.Html
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.widget.TextView
|
||||
@@ -91,12 +90,12 @@ class UnavailableFeatureDialogFragment : DialogFragment() {
|
||||
}
|
||||
}
|
||||
if (apiName.isEmpty()) {
|
||||
val mapper = arrayOf("ANDROID BASE", "ANDROID BASE 1.1", "CUPCAKE", "DONUT", "ECLAIR", "ECLAIR_0_1", "ECLAIR_MR1", "FROYO", "GINGERBREAD", "GINGERBREAD_MR1", "HONEYCOMB", "HONEYCOMB_MR1", "HONEYCOMB_MR2", "ICE_CREAM_SANDWICH", "ICE_CREAM_SANDWICH_MR1", "JELLY_BEAN", "JELLY_BEAN", "JELLY_BEAN", "KITKAT", "KITKAT", "LOLLIPOOP", "LOLLIPOOP_MR1", "MARSHMALLOW", "NOUGAT", "NOUGAT", "OREO", "OREO")
|
||||
val mapper = arrayOf("ANDROID BASE", "ANDROID BASE 1.1", "CUPCAKE", "DONUT", "ECLAIR", "ECLAIR_0_1", "ECLAIR_MR1", "FROYO", "GINGERBREAD", "GINGERBREAD_MR1", "HONEYCOMB", "HONEYCOMB_MR1", "HONEYCOMB_MR2", "ICE_CREAM_SANDWICH", "ICE_CREAM_SANDWICH_MR1", "JELLY_BEAN", "JELLY_BEAN", "JELLY_BEAN", "KITKAT", "KITKAT", "LOLLIPOOP", "LOLLIPOOP_MR1", "MARSHMALLOW", "NOUGAT", "NOUGAT", "OREO", "OREO", "PIE", "", "")
|
||||
val index = apiNumber - 1
|
||||
apiName = if (index < mapper.size) mapper[index] else "UNKNOWN_VERSION"
|
||||
}
|
||||
if (version.isEmpty()) {
|
||||
val versions = arrayOf("1.0", "1.1", "1.5", "1.6", "2.0", "2.0.1", "2.1", "2.2.X", "2.3", "2.3.3", "3.0", "3.1", "3.2.0", "4.0.1", "4.0.3", "4.1.0", "4.2.0", "4.3.0", "4.4", "4.4", "5.0", "5.1", "6.0", "7.0", "7.1", "8.0.0", "8.1.0")
|
||||
val versions = arrayOf("1.0", "1.1", "1.5", "1.6", "2.0", "2.0.1", "2.1", "2.2.X", "2.3", "2.3.3", "3.0", "3.1", "3.2.0", "4.0.1", "4.0.3", "4.1.0", "4.2.0", "4.3.0", "4.4", "4.4", "5.0", "5.1", "6.0", "7.0", "7.1", "8.0.0", "8.1.0", "9", "10", "11")
|
||||
val index = apiNumber - 1
|
||||
version = if (index < versions.size) versions[index] else "UNKNOWN_VERSION"
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
@@ -52,7 +52,7 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_buy_pro), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||
builder.setPositiveButton(R.string.download) { _, _ ->
|
||||
UriUtil.gotoUrl(context!!, R.string.app_pro_url)
|
||||
UriUtil.gotoUrl(requireContext(), R.string.app_pro_url)
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||
}
|
||||
@@ -61,7 +61,7 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_contibute), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||
UriUtil.gotoUrl(context!!, R.string.contribution_url)
|
||||
UriUtil.gotoUrl(requireContext(), R.string.contribution_url)
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,528 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.fragments
|
||||
|
||||
import android.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.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.adapters.EntryAttachmentsItemsAdapter
|
||||
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.view.collapse
|
||||
import com.kunzisoft.keepass.view.expand
|
||||
|
||||
class EntryEditFragment: StylishFragment() {
|
||||
|
||||
private lateinit var entryTitleLayoutView: TextInputLayout
|
||||
private lateinit var entryTitleView: EditText
|
||||
private lateinit var entryIconView: ImageView
|
||||
private lateinit var entryUserNameView: EditText
|
||||
private lateinit var entryUrlView: EditText
|
||||
private lateinit var entryPasswordLayoutView: TextInputLayout
|
||||
private lateinit var entryPasswordView: EditText
|
||||
private lateinit var entryPasswordGeneratorView: View
|
||||
private lateinit var entryExpirationView: ExpirationView
|
||||
private lateinit var entryNotesView: EditText
|
||||
private lateinit var extraFieldsContainerView: View
|
||||
private lateinit var extraFieldsListView: ViewGroup
|
||||
private lateinit var attachmentsContainerView: View
|
||||
private lateinit var attachmentsListView: RecyclerView
|
||||
|
||||
private lateinit var attachmentsAdapter: EntryAttachmentsItemsAdapter
|
||||
|
||||
private var fontInVisibility: Boolean = false
|
||||
private var iconColor: Int = 0
|
||||
|
||||
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? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
val rootView = inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.fragment_entry_edit_contents, container, false)
|
||||
|
||||
fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(requireContext())
|
||||
|
||||
entryTitleLayoutView = rootView.findViewById(R.id.entry_edit_container_title)
|
||||
entryTitleView = rootView.findViewById(R.id.entry_edit_title)
|
||||
entryIconView = rootView.findViewById(R.id.entry_edit_icon_button)
|
||||
entryIconView.setOnClickListener {
|
||||
setOnIconViewClickListener?.invoke(mEntryInfo.icon)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if (savedInstanceState?.containsKey(KEY_LAST_FOCUSED_FIELD) == true) {
|
||||
mLastFocusedEditField = savedInstanceState.getParcelable(KEY_LAST_FOCUSED_FIELD) ?: mLastFocusedEditField
|
||||
}
|
||||
|
||||
populateViewsWithEntry()
|
||||
|
||||
return rootView
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
super.onDetach()
|
||||
|
||||
drawFactory = null
|
||||
setOnDateClickListener = null
|
||||
setOnPasswordGeneratorClickListener = null
|
||||
setOnIconViewClickListener = null
|
||||
setOnRemoveAttachment = null
|
||||
setOnEditCustomField = null
|
||||
}
|
||||
|
||||
fun getEntryInfo(): EntryInfo {
|
||||
populateEntryWithViews()
|
||||
return mEntryInfo
|
||||
}
|
||||
|
||||
fun generatePasswordEducationPerformed(entryEditActivityEducation: EntryEditActivityEducation): Boolean {
|
||||
return entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
||||
entryPasswordGeneratorView,
|
||||
{
|
||||
GeneratePasswordDialogFragment().show(parentFragmentManager, "PasswordGeneratorFragment")
|
||||
},
|
||||
{
|
||||
try {
|
||||
(activity as? EntryEditActivity?)?.performedNextEducation(entryEditActivityEducation)
|
||||
} catch (ignore: Exception) {}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun populateViewsWithEntry() {
|
||||
// Set info in view
|
||||
icon = mEntryInfo.icon
|
||||
title = mEntryInfo.title
|
||||
username = mEntryInfo.username
|
||||
url = mEntryInfo.url
|
||||
password = mEntryInfo.password
|
||||
expires = mEntryInfo.expires
|
||||
expiryTime = mEntryInfo.expiryTime
|
||||
notes = mEntryInfo.notes
|
||||
assignExtraFields(mEntryInfo.customFields) { fields ->
|
||||
setOnEditCustomField?.invoke(fields)
|
||||
}
|
||||
assignAttachments(mEntryInfo.attachments, StreamDirection.UPLOAD) { attachment ->
|
||||
setOnRemoveAttachment?.invoke(attachment)
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateEntryWithViews() {
|
||||
// Icon already populate
|
||||
mEntryInfo.title = title
|
||||
mEntryInfo.username = username
|
||||
mEntryInfo.url = url
|
||||
mEntryInfo.password = password
|
||||
mEntryInfo.expires = expires
|
||||
mEntryInfo.expiryTime = expiryTime
|
||||
mEntryInfo.notes = notes
|
||||
mEntryInfo.customFields = getExtraFields()
|
||||
mEntryInfo.otpModel = OtpEntryFields.parseFields { key ->
|
||||
getExtraFields().firstOrNull { it.name == key }?.protectedValue?.toString()
|
||||
}?.otpModel
|
||||
mEntryInfo.attachments = getAttachments()
|
||||
}
|
||||
|
||||
var title: String
|
||||
get() {
|
||||
return entryTitleView.text.toString()
|
||||
}
|
||||
set(value) {
|
||||
entryTitleView.setText(value)
|
||||
if (fontInVisibility)
|
||||
entryTitleView.applyFontVisibility()
|
||||
}
|
||||
|
||||
var icon: IconImage
|
||||
get() {
|
||||
return mEntryInfo.icon
|
||||
}
|
||||
set(value) {
|
||||
mEntryInfo.icon = value
|
||||
drawFactory?.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()
|
||||
}
|
||||
}
|
||||
|
||||
var expires: Boolean
|
||||
get() {
|
||||
return entryExpirationView.expires
|
||||
}
|
||||
set(value) {
|
||||
entryExpirationView.expires = value
|
||||
}
|
||||
|
||||
var expiryTime: DateInstant
|
||||
get() {
|
||||
return entryExpirationView.expiryTime
|
||||
}
|
||||
set(value) {
|
||||
entryExpirationView.expiryTime = value
|
||||
}
|
||||
|
||||
var notes: String
|
||||
get() {
|
||||
return entryNotesView.text.toString()
|
||||
}
|
||||
set(value) {
|
||||
entryNotesView.setText(value)
|
||||
if (fontInVisibility)
|
||||
entryNotesView.applyFontVisibility()
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* Extra Fields
|
||||
* -------------
|
||||
*/
|
||||
|
||||
private var mExtraFieldsList: MutableList<Field> = ArrayList()
|
||||
private var mOnEditButtonClickListener: ((item: Field)->Unit)? = null
|
||||
|
||||
private fun buildViewFromField(extraField: Field): View? {
|
||||
val inflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
val itemView: View? = inflater?.inflate(R.layout.item_entry_edit_extra_field, extraFieldsListView, false)
|
||||
itemView?.id = View.NO_ID
|
||||
|
||||
val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container)
|
||||
extraFieldValueContainer?.endIconMode = if (extraField.protectedValue.isProtected)
|
||||
TextInputLayout.END_ICON_PASSWORD_TOGGLE else TextInputLayout.END_ICON_NONE
|
||||
extraFieldValueContainer?.hint = extraField.name
|
||||
extraFieldValueContainer?.id = View.NO_ID
|
||||
|
||||
val extraFieldValue: TextInputEditText? = itemView?.findViewById(R.id.entry_extra_field_value)
|
||||
extraFieldValue?.apply {
|
||||
if (extraField.protectedValue.isProtected) {
|
||||
inputType = extraFieldValue.inputType or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
|
||||
}
|
||||
setText(extraField.protectedValue.toString())
|
||||
if (fontInVisibility)
|
||||
applyFontVisibility()
|
||||
}
|
||||
extraFieldValue?.id = View.NO_ID
|
||||
extraFieldValue?.tag = "FIELD_VALUE_TAG"
|
||||
if (mLastFocusedEditField?.field == extraField) {
|
||||
mExtraViewToRequestFocus = extraFieldValue
|
||||
}
|
||||
|
||||
val extraFieldEditButton: View? = itemView?.findViewById(R.id.entry_extra_field_edit)
|
||||
extraFieldEditButton?.setOnClickListener {
|
||||
mOnEditButtonClickListener?.invoke(extraField)
|
||||
}
|
||||
extraFieldEditButton?.id = View.NO_ID
|
||||
|
||||
return itemView
|
||||
}
|
||||
|
||||
fun getExtraFields(): List<Field> {
|
||||
mLastFocusedEditField = null
|
||||
for (index in 0 until extraFieldsListView.childCount) {
|
||||
val extraFieldValue: EditText = extraFieldsListView.getChildAt(index)
|
||||
.findViewWithTag("FIELD_VALUE_TAG")
|
||||
val extraField = mExtraFieldsList[index]
|
||||
extraField.protectedValue.stringValue = extraFieldValue.text?.toString() ?: ""
|
||||
if (extraFieldValue.isFocused) {
|
||||
mLastFocusedEditField = FocusedEditField().apply {
|
||||
field = extraField
|
||||
cursorSelectionStart = extraFieldValue.selectionStart
|
||||
cursorSelectionEnd = extraFieldValue.selectionEnd
|
||||
}
|
||||
}
|
||||
}
|
||||
return mExtraFieldsList
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all children and add new views for each field
|
||||
*/
|
||||
fun assignExtraFields(fields: List<Field>,
|
||||
onEditButtonClickListener: ((item: Field)->Unit)?) {
|
||||
extraFieldsContainerView.visibility = if (fields.isEmpty()) View.GONE else View.VISIBLE
|
||||
// Reinit focused field
|
||||
mExtraFieldsList.clear()
|
||||
mExtraFieldsList.addAll(fields)
|
||||
extraFieldsListView.removeAllViews()
|
||||
fields.forEach {
|
||||
extraFieldsListView.addView(buildViewFromField(it))
|
||||
}
|
||||
// Request last focus
|
||||
mLastFocusedEditField?.let { focusField ->
|
||||
mExtraViewToRequestFocus?.apply {
|
||||
requestFocus()
|
||||
setSelection(focusField.cursorSelectionStart,
|
||||
focusField.cursorSelectionEnd)
|
||||
}
|
||||
}
|
||||
mLastFocusedEditField = null
|
||||
mOnEditButtonClickListener = onEditButtonClickListener
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an extra field or create a new one if doesn't exists, the old value is lost
|
||||
*/
|
||||
fun putExtraField(extraField: Field) {
|
||||
extraFieldsContainerView.visibility = View.VISIBLE
|
||||
val oldField = mExtraFieldsList.firstOrNull { it.name == extraField.name }
|
||||
oldField?.let {
|
||||
val index = mExtraFieldsList.indexOf(oldField)
|
||||
mExtraFieldsList.removeAt(index)
|
||||
mExtraFieldsList.add(index, extraField)
|
||||
extraFieldsListView.removeViewAt(index)
|
||||
val newView = buildViewFromField(extraField)
|
||||
extraFieldsListView.addView(newView, index)
|
||||
newView?.requestFocus()
|
||||
} ?: kotlin.run {
|
||||
mExtraFieldsList.add(extraField)
|
||||
val newView = buildViewFromField(extraField)
|
||||
extraFieldsListView.addView(newView)
|
||||
newView?.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an extra field and keep the old value
|
||||
*/
|
||||
fun replaceExtraField(oldExtraField: Field, newExtraField: Field) {
|
||||
extraFieldsContainerView.visibility = View.VISIBLE
|
||||
val index = mExtraFieldsList.indexOf(oldExtraField)
|
||||
val oldValueEditText: EditText = extraFieldsListView.getChildAt(index)
|
||||
.findViewWithTag("FIELD_VALUE_TAG")
|
||||
val oldValue = oldValueEditText.text.toString()
|
||||
val newExtraFieldWithOldValue = Field(newExtraField).apply {
|
||||
this.protectedValue.stringValue = oldValue
|
||||
}
|
||||
mExtraFieldsList.removeAt(index)
|
||||
mExtraFieldsList.add(index, newExtraFieldWithOldValue)
|
||||
extraFieldsListView.removeViewAt(index)
|
||||
val newView = buildViewFromField(newExtraFieldWithOldValue)
|
||||
extraFieldsListView.addView(newView, index)
|
||||
newView?.requestFocus()
|
||||
}
|
||||
|
||||
fun removeExtraField(oldExtraField: Field) {
|
||||
val previousSize = mExtraFieldsList.size
|
||||
val index = mExtraFieldsList.indexOf(oldExtraField)
|
||||
extraFieldsListView.getChildAt(index)?.let {
|
||||
it.collapse(true) {
|
||||
mExtraFieldsList.removeAt(index)
|
||||
extraFieldsListView.removeViewAt(index)
|
||||
val newSize = mExtraFieldsList.size
|
||||
|
||||
if (previousSize > 0 && newSize == 0) {
|
||||
extraFieldsContainerView.collapse(true)
|
||||
} else if (previousSize == 0 && newSize == 1) {
|
||||
extraFieldsContainerView.expand(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* Attachments
|
||||
* -------------
|
||||
*/
|
||||
|
||||
fun getAttachments(): List<Attachment> {
|
||||
return attachmentsAdapter.itemsList.map { it.attachment }
|
||||
}
|
||||
|
||||
fun assignAttachments(attachments: List<Attachment>,
|
||||
streamDirection: StreamDirection,
|
||||
onDeleteItem: (attachment: Attachment)->Unit) {
|
||||
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
|
||||
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
|
||||
attachmentsAdapter.onDeleteButtonClickListener = { item ->
|
||||
onDeleteItem.invoke(item.attachment)
|
||||
}
|
||||
}
|
||||
|
||||
fun containsAttachment(): Boolean {
|
||||
return !attachmentsAdapter.isEmpty()
|
||||
}
|
||||
|
||||
fun containsAttachment(attachment: EntryAttachmentState): Boolean {
|
||||
return attachmentsAdapter.contains(attachment)
|
||||
}
|
||||
|
||||
fun putAttachment(attachment: EntryAttachmentState,
|
||||
onPreviewLoaded: (()-> Unit)? = null) {
|
||||
attachmentsContainerView.visibility = View.VISIBLE
|
||||
attachmentsAdapter.putItem(attachment)
|
||||
attachmentsAdapter.onBinaryPreviewLoaded = {
|
||||
onPreviewLoaded?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAttachment(attachment: EntryAttachmentState) {
|
||||
attachmentsAdapter.removeItem(attachment)
|
||||
}
|
||||
|
||||
fun clearAttachments() {
|
||||
attachmentsAdapter.clear()
|
||||
}
|
||||
|
||||
fun getAttachmentViewPosition(attachment: EntryAttachmentState, position: (Float) -> Unit) {
|
||||
attachmentsListView.postDelayed({
|
||||
position.invoke(attachmentsContainerView.y
|
||||
+ attachmentsListView.y
|
||||
+ (attachmentsListView.getChildAt(attachmentsAdapter.indexOf(attachment))?.y
|
||||
?: 0F)
|
||||
)
|
||||
}, 250)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
populateEntryWithViews()
|
||||
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
|
||||
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
|
||||
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
|
||||
const val KEY_DATABASE = "KEY_DATABASE"
|
||||
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||
|
||||
|
||||
class IconCustomFragment : IconFragment<IconImageCustom>() {
|
||||
|
||||
override fun retrieveMainLayoutId(): Int {
|
||||
return R.layout.fragment_icon_grid
|
||||
}
|
||||
|
||||
override fun defineIconList() {
|
||||
mDatabase?.doForEachCustomIcons { customIcon, _ ->
|
||||
iconPickerAdapter.addIcon(customIcon, false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
iconPickerViewModel.customIconsSelected.observe(viewLifecycleOwner) { customIconsSelected ->
|
||||
if (customIconsSelected.isEmpty()) {
|
||||
iconActionSelectionMode = false
|
||||
iconPickerAdapter.deselectAllIcons()
|
||||
} else {
|
||||
iconActionSelectionMode = true
|
||||
iconPickerAdapter.updateIconSelectedState(customIconsSelected)
|
||||
}
|
||||
}
|
||||
iconPickerViewModel.customIconAdded.observe(viewLifecycleOwner) { iconCustomAdded ->
|
||||
if (!iconCustomAdded.error) {
|
||||
iconCustomAdded?.iconCustom?.let { icon ->
|
||||
iconPickerAdapter.addIcon(icon)
|
||||
iconCustomAdded.iconCustom = null
|
||||
}
|
||||
iconsGridView.smoothScrollToPosition(iconPickerAdapter.lastPosition)
|
||||
}
|
||||
}
|
||||
iconPickerViewModel.customIconRemoved.observe(viewLifecycleOwner) { iconCustomRemoved ->
|
||||
if (!iconCustomRemoved.error) {
|
||||
iconCustomRemoved?.iconCustom?.let { icon ->
|
||||
iconPickerAdapter.removeIcon(icon)
|
||||
iconCustomRemoved.iconCustom = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIconClickListener(icon: IconImageCustom) {
|
||||
if (iconActionSelectionMode) {
|
||||
// Same long click behavior after each single click
|
||||
onIconLongClickListener(icon)
|
||||
} else {
|
||||
iconPickerViewModel.pickCustomIcon(icon)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIconLongClickListener(icon: IconImageCustom) {
|
||||
// Select or deselect item if already selected
|
||||
icon.selected = !icon.selected
|
||||
iconPickerAdapter.updateIcon(icon)
|
||||
iconActionSelectionMode = iconPickerAdapter.containsAnySelectedIcon()
|
||||
iconPickerViewModel.selectCustomIcons(iconPickerAdapter.getSelectedIcons())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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.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 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.element.icon.IconImageDraw
|
||||
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
abstract class IconFragment<T: IconImageDraw> : StylishFragment(),
|
||||
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()
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
|
||||
mDatabase = Database.getInstance()
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val ta = contextThemed?.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
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val populateList = launch {
|
||||
iconPickerAdapter.clear()
|
||||
defineIconList()
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
populateList.join()
|
||||
iconPickerAdapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
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.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.activities.stylish.StylishFragment
|
||||
import com.kunzisoft.keepass.adapters.IconPickerPagerAdapter
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||
|
||||
class IconPickerFragment : StylishFragment() {
|
||||
|
||||
private var iconPickerPagerAdapter: IconPickerPagerAdapter? = null
|
||||
private lateinit var viewPager: ViewPager2
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
mDatabase = Database.getInstance()
|
||||
|
||||
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()
|
||||
|
||||
arguments?.apply {
|
||||
if (containsKey(ICON_TAB_ARG)) {
|
||||
viewPager.currentItem = getInt(ICON_TAB_ARG)
|
||||
}
|
||||
remove(ICON_TAB_ARG)
|
||||
}
|
||||
|
||||
iconPickerViewModel.customIconAdded.observe(viewLifecycleOwner) { _ ->
|
||||
viewPager.currentItem = 1
|
||||
}
|
||||
}
|
||||
|
||||
enum class IconTab {
|
||||
STANDARD, CUSTOM
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ICON_TAB_ARG = "ICON_TAB_ARG"
|
||||
|
||||
fun getInstance(iconTab: IconTab): IconPickerFragment {
|
||||
val fragment = IconPickerFragment()
|
||||
fragment.arguments = Bundle().apply {
|
||||
putInt(ICON_TAB_ARG, iconTab.ordinal)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.fragments
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||
|
||||
|
||||
class IconStandardFragment : IconFragment<IconImageStandard>() {
|
||||
|
||||
override fun retrieveMainLayoutId(): Int {
|
||||
return R.layout.fragment_icon_grid
|
||||
}
|
||||
|
||||
override fun defineIconList() {
|
||||
mDatabase?.doForEachStandardIcons { standardIcon ->
|
||||
iconPickerAdapter.addIcon(standardIcon, false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIconClickListener(icon: IconImageStandard) {
|
||||
iconPickerViewModel.pickStandardIcon(icon)
|
||||
}
|
||||
|
||||
override fun onIconLongClickListener(icon: IconImageStandard) {}
|
||||
}
|
||||
@@ -1,33 +1,46 @@
|
||||
package com.kunzisoft.keepass.activities
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.fragments
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.view.ActionMode
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.adapters.NodeAdapter
|
||||
import com.kunzisoft.keepass.database.SortNodeEnum
|
||||
import com.kunzisoft.keepass.database.element.GroupVersioned
|
||||
import com.kunzisoft.keepass.database.element.NodeVersioned
|
||||
import com.kunzisoft.keepass.activities.EntryEditActivity
|
||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.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.database.element.Type
|
||||
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.*
|
||||
|
||||
class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener {
|
||||
@@ -35,8 +48,8 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
private var nodeClickListener: NodeClickListener? = null
|
||||
private var onScrollListener: OnScrollListener? = null
|
||||
|
||||
private var listView: RecyclerView? = null
|
||||
var mainGroup: GroupVersioned? = null
|
||||
private var mNodesRecyclerView: RecyclerView? = null
|
||||
var mainGroup: Group? = null
|
||||
private set
|
||||
private var mAdapter: NodeAdapter? = null
|
||||
|
||||
@@ -44,20 +57,15 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
private set
|
||||
var nodeActionPasteMode: PasteMode = PasteMode.UNDEFINED
|
||||
private set
|
||||
private val listActionNodes = LinkedList<NodeVersioned>()
|
||||
private val listPasteNodes = LinkedList<NodeVersioned>()
|
||||
private val listActionNodes = LinkedList<Node>()
|
||||
private val listPasteNodes = LinkedList<Node>()
|
||||
|
||||
private var notFoundView: View? = null
|
||||
private var isASearchResult: Boolean = false
|
||||
|
||||
// Preferences for sorting
|
||||
private var prefs: SharedPreferences? = null
|
||||
|
||||
private var readOnly: Boolean = false
|
||||
get() {
|
||||
return field || selectionMode
|
||||
}
|
||||
private var selectionMode: Boolean = false
|
||||
private var specialMode: SpecialMode = SpecialMode.DEFAULT
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
|
||||
@@ -82,6 +90,12 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
nodeClickListener = null
|
||||
onScrollListener = null
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -103,7 +117,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
mAdapter = NodeAdapter(context)
|
||||
mAdapter?.apply {
|
||||
setOnNodeClickListener(object : NodeAdapter.NodeClickCallback {
|
||||
override fun onNodeClick(node: NodeVersioned) {
|
||||
override fun onNodeClick(node: Node) {
|
||||
if (nodeActionSelectionMode) {
|
||||
if (listActionNodes.contains(node)) {
|
||||
// Remove selected item if already selected
|
||||
@@ -120,7 +134,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNodeLongClick(node: NodeVersioned): Boolean {
|
||||
override fun onNodeLongClick(node: Node): Boolean {
|
||||
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
|
||||
// Select the first item after a long click
|
||||
if (!listActionNodes.contains(node))
|
||||
@@ -136,7 +150,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
})
|
||||
}
|
||||
}
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@@ -150,11 +163,17 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
// To apply theme
|
||||
val rootView = inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.fragment_list_nodes, container, false)
|
||||
listView = rootView.findViewById(R.id.nodes_list)
|
||||
mNodesRecyclerView = rootView.findViewById(R.id.nodes_list)
|
||||
notFoundView = rootView.findViewById(R.id.not_found_container)
|
||||
|
||||
mNodesRecyclerView?.apply {
|
||||
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = mAdapter
|
||||
}
|
||||
|
||||
onScrollListener?.let { onScrollListener ->
|
||||
listView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
mNodesRecyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
onScrollListener.onScrolled(dy)
|
||||
@@ -162,8 +181,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
})
|
||||
}
|
||||
|
||||
rebuildList()
|
||||
|
||||
return rootView
|
||||
}
|
||||
|
||||
@@ -171,48 +188,57 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
super.onResume()
|
||||
|
||||
activity?.intent?.let {
|
||||
selectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(it)
|
||||
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
|
||||
}
|
||||
|
||||
// Refresh data
|
||||
mAdapter?.notifyDataSetChanged()
|
||||
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 "
|
||||
listView?.visibility = View.GONE
|
||||
mNodesRecyclerView?.visibility = View.GONE
|
||||
notFoundView?.visibility = View.VISIBLE
|
||||
} else {
|
||||
listView?.visibility = View.VISIBLE
|
||||
mNodesRecyclerView?.visibility = View.VISIBLE
|
||||
notFoundView?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun rebuildList() {
|
||||
// Add elements to the list
|
||||
mainGroup?.let { mainGroup ->
|
||||
mAdapter?.rebuildList(mainGroup)
|
||||
}
|
||||
listView?.apply {
|
||||
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = mAdapter
|
||||
mAdapter?.apply {
|
||||
// Thrown an exception when sort cannot be performed
|
||||
rebuildList(mainGroup)
|
||||
// To visually change the elements
|
||||
if (PreferencesUtil.APPEARANCE_CHANGED) {
|
||||
notifyDataSetChanged()
|
||||
PreferencesUtil.APPEARANCE_CHANGED = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSortSelected(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) {
|
||||
// Toggle setting
|
||||
prefs?.edit()?.apply {
|
||||
putString(getString(R.string.sort_node_key), sortNodeEnum.name)
|
||||
putBoolean(getString(R.string.sort_ascending_key), ascending)
|
||||
putBoolean(getString(R.string.sort_group_before_key), groupsBefore)
|
||||
putBoolean(getString(R.string.sort_recycle_bin_bottom_key), recycleBinBottom)
|
||||
apply()
|
||||
override fun onSortSelected(sortNodeEnum: SortNodeEnum,
|
||||
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
||||
// Save setting
|
||||
context?.let {
|
||||
PreferencesUtil.saveNodeSort(it, sortNodeEnum, sortNodeParameters)
|
||||
}
|
||||
|
||||
// Tell the adapter to refresh it's list
|
||||
mAdapter?.notifyChangeSort(sortNodeEnum, ascending, groupsBefore)
|
||||
mainGroup?.let { mainGroup ->
|
||||
mAdapter?.rebuildList(mainGroup)
|
||||
try {
|
||||
mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters)
|
||||
rebuildList()
|
||||
} catch (e:Exception) {
|
||||
Log.e(TAG, "Unable to rebuild the list with the sort")
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,8 +254,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
R.id.menu_sort -> {
|
||||
context?.let { context ->
|
||||
val sortDialogFragment: SortDialogFragment =
|
||||
if (Database.getInstance().allowRecycleBin
|
||||
&& Database.getInstance().isRecycleBinEnabled) {
|
||||
if (Database.getInstance().isRecycleBinEnabled) {
|
||||
SortDialogFragment.getInstance(
|
||||
PreferencesUtil.getListSort(context),
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
@@ -251,15 +276,16 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
}
|
||||
}
|
||||
|
||||
fun actionNodesCallback(nodes: List<NodeVersioned>,
|
||||
menuListener: NodesActionMenuListener?) : ActionMode.Callback {
|
||||
fun actionNodesCallback(nodes: List<Node>,
|
||||
menuListener: NodesActionMenuListener?,
|
||||
actionModeCallback: ActionMode.Callback) : ActionMode.Callback {
|
||||
|
||||
return object : ActionMode.Callback {
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
nodeActionSelectionMode = false
|
||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||
return true
|
||||
return actionModeCallback.onCreateActionMode(mode, menu)
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
@@ -276,7 +302,8 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
// Open and Edit for a single item
|
||||
if (nodes.size == 1) {
|
||||
// Edition
|
||||
if (readOnly || nodes[0] == database.recycleBin) {
|
||||
if (readOnly
|
||||
|| (database.isRecycleBinEnabled && nodes[0] == database.recycleBin)) {
|
||||
menu?.removeItem(R.id.menu_edit)
|
||||
}
|
||||
} else {
|
||||
@@ -284,18 +311,22 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
menu?.removeItem(R.id.menu_edit)
|
||||
}
|
||||
|
||||
// Copy and Move (not for groups)
|
||||
// Move
|
||||
if (readOnly
|
||||
|| isASearchResult
|
||||
|| nodes.any { it == database.recycleBin }
|
||||
|| nodes.any { it.type == Type.GROUP }) {
|
||||
// TODO COPY For Group
|
||||
menu?.removeItem(R.id.menu_copy)
|
||||
|| isASearchResult) {
|
||||
menu?.removeItem(R.id.menu_move)
|
||||
}
|
||||
|
||||
// Copy (not allowed for group)
|
||||
if (readOnly
|
||||
|| isASearchResult
|
||||
|| nodes.any { it.type == Type.GROUP }) {
|
||||
menu?.removeItem(R.id.menu_copy)
|
||||
}
|
||||
|
||||
// Deletion
|
||||
if (readOnly || nodes.any { it == database.recycleBin }) {
|
||||
if (readOnly
|
||||
|| (database.isRecycleBinEnabled && nodes.any { it == database.recycleBin })) {
|
||||
menu?.removeItem(R.id.menu_delete)
|
||||
}
|
||||
}
|
||||
@@ -303,7 +334,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
// Add the number of items selected in title
|
||||
mode?.title = nodes.size.toString()
|
||||
|
||||
return true
|
||||
return actionModeCallback.onPrepareActionMode(mode, menu)
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
||||
@@ -333,7 +364,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
nodeActionSelectionMode = false
|
||||
returnValue
|
||||
}
|
||||
else -> false
|
||||
else -> actionModeCallback.onActionItemClicked(mode, item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,6 +374,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
mAdapter?.unselectActionNodes()
|
||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||
nodeActionSelectionMode = false
|
||||
actionModeCallback.onDestroyActionMode(mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -354,46 +386,42 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE
|
||||
|| resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
|
||||
data?.getParcelableExtra<NodeVersioned>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { newNode ->
|
||||
data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { changedNode ->
|
||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
|
||||
mAdapter?.addNode(newNode)
|
||||
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
|
||||
//mAdapter.updateLastNodeRegister(newNode);
|
||||
mainGroup?.let { mainGroup ->
|
||||
mAdapter?.rebuildList(mainGroup)
|
||||
}
|
||||
}
|
||||
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: NodeVersioned): Boolean {
|
||||
fun contains(node: Node): Boolean {
|
||||
return mAdapter?.contains(node) ?: false
|
||||
}
|
||||
|
||||
fun addNode(newNode: NodeVersioned) {
|
||||
fun addNode(newNode: Node) {
|
||||
mAdapter?.addNode(newNode)
|
||||
}
|
||||
|
||||
fun addNodes(newNodes: List<NodeVersioned>) {
|
||||
fun addNodes(newNodes: List<Node>) {
|
||||
mAdapter?.addNodes(newNodes)
|
||||
}
|
||||
|
||||
fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned? = null) {
|
||||
fun updateNode(oldNode: Node, newNode: Node? = null) {
|
||||
mAdapter?.updateNode(oldNode, newNode ?: oldNode)
|
||||
}
|
||||
|
||||
fun updateNodes(oldNodes: List<NodeVersioned>, newNodes: List<NodeVersioned>) {
|
||||
fun updateNodes(oldNodes: List<Node>, newNodes: List<Node>) {
|
||||
mAdapter?.updateNodes(oldNodes, newNodes)
|
||||
}
|
||||
|
||||
fun removeNode(pwNode: NodeVersioned) {
|
||||
fun removeNode(pwNode: Node) {
|
||||
mAdapter?.removeNode(pwNode)
|
||||
}
|
||||
|
||||
fun removeNodes(nodes: List<NodeVersioned>) {
|
||||
fun removeNodes(nodes: List<Node>) {
|
||||
mAdapter?.removeNodes(nodes)
|
||||
}
|
||||
|
||||
@@ -409,20 +437,20 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
* Callback listener to redefine to do an action when a node is click
|
||||
*/
|
||||
interface NodeClickListener {
|
||||
fun onNodeClick(node: NodeVersioned)
|
||||
fun onNodeSelected(nodes: List<NodeVersioned>): Boolean
|
||||
fun onNodeClick(node: Node)
|
||||
fun onNodeSelected(nodes: List<Node>): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu listener to redefine to do an action in menu
|
||||
*/
|
||||
interface NodesActionMenuListener {
|
||||
fun onOpenMenuClick(node: NodeVersioned): Boolean
|
||||
fun onEditMenuClick(node: NodeVersioned): Boolean
|
||||
fun onCopyMenuClick(nodes: List<NodeVersioned>): Boolean
|
||||
fun onMoveMenuClick(nodes: List<NodeVersioned>): Boolean
|
||||
fun onDeleteMenuClick(nodes: List<NodeVersioned>): Boolean
|
||||
fun onPasteMenuClick(pasteMode: PasteMode?, nodes: List<NodeVersioned>): Boolean
|
||||
fun onOpenMenuClick(node: Node): Boolean
|
||||
fun onEditMenuClick(node: Node): Boolean
|
||||
fun onCopyMenuClick(nodes: List<Node>): Boolean
|
||||
fun onMoveMenuClick(nodes: List<Node>): Boolean
|
||||
fun onDeleteMenuClick(nodes: List<Node>): Boolean
|
||||
fun onPasteMenuClick(pasteMode: PasteMode?, nodes: List<Node>): Boolean
|
||||
}
|
||||
|
||||
enum class PasteMode {
|
||||
@@ -447,7 +475,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
||||
private const val GROUP_KEY = "GROUP_KEY"
|
||||
private const val IS_SEARCH = "IS_SEARCH"
|
||||
|
||||
fun newInstance(group: GroupVersioned?, readOnly: Boolean, isASearch: Boolean): ListNodesFragment {
|
||||
fun newInstance(group: Group?, readOnly: Boolean, isASearch: Boolean): ListNodesFragment {
|
||||
val bundle = Bundle()
|
||||
if (group != null) {
|
||||
bundle.putParcelable(GROUP_KEY, group)
|
||||
@@ -1,51 +1,209 @@
|
||||
/*
|
||||
* 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.helpers
|
||||
|
||||
import android.app.assist.AssistStructure
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.model.RegisterInfo
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import java.io.Serializable
|
||||
|
||||
object EntrySelectionHelper {
|
||||
|
||||
private const val EXTRA_ENTRY_SELECTION_MODE = "com.kunzisoft.keepass.extra.ENTRY_SELECTION_MODE"
|
||||
private const val DEFAULT_ENTRY_SELECTION_MODE = false
|
||||
private const val KEY_SPECIAL_MODE = "com.kunzisoft.keepass.extra.SPECIAL_MODE"
|
||||
private const val KEY_TYPE_MODE = "com.kunzisoft.keepass.extra.TYPE_MODE"
|
||||
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
|
||||
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
|
||||
|
||||
fun startActivityForEntrySelection(context: Context, intent: Intent) {
|
||||
addEntrySelectionModeExtraInIntent(intent)
|
||||
// only to avoid visible flickering when redirecting
|
||||
fun startActivityForSearchModeResult(context: Context,
|
||||
intent: Intent,
|
||||
searchInfo: SearchInfo) {
|
||||
addSpecialModeInIntent(intent, SpecialMode.SEARCH)
|
||||
addSearchInfoInIntent(intent, searchInfo)
|
||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun addEntrySelectionModeExtraInIntent(intent: Intent) {
|
||||
intent.putExtra(EXTRA_ENTRY_SELECTION_MODE, true)
|
||||
fun startActivityForSaveModeResult(context: Context,
|
||||
intent: Intent,
|
||||
searchInfo: SearchInfo) {
|
||||
addSpecialModeInIntent(intent, SpecialMode.SAVE)
|
||||
addTypeModeInIntent(intent, TypeMode.DEFAULT)
|
||||
addSearchInfoInIntent(intent, searchInfo)
|
||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun removeEntrySelectionModeFromIntent(intent: Intent) {
|
||||
intent.removeExtra(EXTRA_ENTRY_SELECTION_MODE)
|
||||
fun startActivityForKeyboardSelectionModeResult(context: Context,
|
||||
intent: Intent,
|
||||
searchInfo: SearchInfo?) {
|
||||
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||
addTypeModeInIntent(intent, TypeMode.MAGIKEYBOARD)
|
||||
addSearchInfoInIntent(intent, searchInfo)
|
||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun retrieveEntrySelectionModeFromIntent(intent: Intent): Boolean {
|
||||
return intent.getBooleanExtra(EXTRA_ENTRY_SELECTION_MODE, DEFAULT_ENTRY_SELECTION_MODE)
|
||||
fun startActivityForRegistrationModeResult(context: Context,
|
||||
intent: Intent,
|
||||
registerInfo: RegisterInfo?) {
|
||||
addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
|
||||
// At the moment, only autofill for registration
|
||||
addTypeModeInIntent(intent, TypeMode.AUTOFILL)
|
||||
addRegisterInfoInIntent(intent, registerInfo)
|
||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun doEntrySelectionAction(intent: Intent,
|
||||
standardAction: () -> Unit,
|
||||
keyboardAction: () -> Unit,
|
||||
autofillAction: (assistStructure: AssistStructure) -> Unit) {
|
||||
var assistStructureInit = false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillHelper.retrieveAssistStructure(intent)?.let { assistStructure ->
|
||||
autofillAction.invoke(assistStructure)
|
||||
assistStructureInit = true
|
||||
}
|
||||
fun addSearchInfoInIntent(intent: Intent, searchInfo: SearchInfo?) {
|
||||
searchInfo?.let {
|
||||
intent.putExtra(KEY_SEARCH_INFO, it)
|
||||
}
|
||||
if (!assistStructureInit) {
|
||||
if (intent.getBooleanExtra(EXTRA_ENTRY_SELECTION_MODE, DEFAULT_ENTRY_SELECTION_MODE)) {
|
||||
intent.removeExtra(EXTRA_ENTRY_SELECTION_MODE)
|
||||
keyboardAction.invoke()
|
||||
} else {
|
||||
standardAction.invoke()
|
||||
}
|
||||
|
||||
fun retrieveSearchInfoFromIntent(intent: Intent): SearchInfo? {
|
||||
return intent.getParcelableExtra(KEY_SEARCH_INFO)
|
||||
}
|
||||
|
||||
fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
|
||||
registerInfo?.let {
|
||||
intent.putExtra(KEY_REGISTER_INFO, it)
|
||||
}
|
||||
}
|
||||
|
||||
fun retrieveRegisterInfoFromIntent(intent: Intent): RegisterInfo? {
|
||||
return intent.getParcelableExtra(KEY_REGISTER_INFO)
|
||||
}
|
||||
|
||||
fun removeInfoFromIntent(intent: Intent) {
|
||||
intent.removeExtra(KEY_SEARCH_INFO)
|
||||
intent.removeExtra(KEY_REGISTER_INFO)
|
||||
}
|
||||
|
||||
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
|
||||
intent.putExtra(KEY_SPECIAL_MODE, specialMode as Serializable)
|
||||
}
|
||||
|
||||
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
|
||||
return SpecialMode.SELECTION
|
||||
}
|
||||
return intent.getSerializableExtra(KEY_SPECIAL_MODE) as SpecialMode?
|
||||
?: SpecialMode.DEFAULT
|
||||
}
|
||||
|
||||
fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
|
||||
intent.putExtra(KEY_TYPE_MODE, typeMode as Serializable)
|
||||
}
|
||||
|
||||
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
|
||||
return TypeMode.AUTOFILL
|
||||
}
|
||||
return intent.getSerializableExtra(KEY_TYPE_MODE) as TypeMode? ?: TypeMode.DEFAULT
|
||||
}
|
||||
|
||||
fun removeModesFromIntent(intent: Intent) {
|
||||
intent.removeExtra(KEY_SPECIAL_MODE)
|
||||
intent.removeExtra(KEY_TYPE_MODE)
|
||||
}
|
||||
|
||||
fun doSpecialAction(intent: Intent,
|
||||
defaultAction: () -> Unit,
|
||||
searchAction: (searchInfo: SearchInfo) -> Unit,
|
||||
saveAction: (searchInfo: SearchInfo) -> Unit,
|
||||
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
||||
autofillSelectionAction: (searchInfo: SearchInfo?,
|
||||
autofillComponent: AutofillComponent) -> Unit,
|
||||
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
|
||||
|
||||
when (retrieveSpecialModeFromIntent(intent)) {
|
||||
SpecialMode.DEFAULT -> {
|
||||
removeModesFromIntent(intent)
|
||||
removeInfoFromIntent(intent)
|
||||
defaultAction.invoke()
|
||||
}
|
||||
SpecialMode.SEARCH -> {
|
||||
val searchInfo = retrieveSearchInfoFromIntent(intent)
|
||||
removeModesFromIntent(intent)
|
||||
removeInfoFromIntent(intent)
|
||||
if (searchInfo != null)
|
||||
searchAction.invoke(searchInfo)
|
||||
else {
|
||||
defaultAction.invoke()
|
||||
}
|
||||
}
|
||||
SpecialMode.SAVE -> {
|
||||
val searchInfo = retrieveSearchInfoFromIntent(intent)
|
||||
removeModesFromIntent(intent)
|
||||
removeInfoFromIntent(intent)
|
||||
if (searchInfo != null)
|
||||
saveAction.invoke(searchInfo)
|
||||
else {
|
||||
defaultAction.invoke()
|
||||
}
|
||||
}
|
||||
SpecialMode.SELECTION -> {
|
||||
val searchInfo: SearchInfo? = retrieveSearchInfoFromIntent(intent)
|
||||
var autofillComponentInit = false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillHelper.retrieveAutofillComponent(intent)?.let { autofillComponent ->
|
||||
autofillSelectionAction.invoke(searchInfo, autofillComponent)
|
||||
autofillComponentInit = true
|
||||
}
|
||||
}
|
||||
if (!autofillComponentInit) {
|
||||
if (intent.getSerializableExtra(KEY_SPECIAL_MODE) != null) {
|
||||
when (retrieveTypeModeFromIntent(intent)) {
|
||||
TypeMode.DEFAULT -> {
|
||||
removeModesFromIntent(intent)
|
||||
if (searchInfo != null)
|
||||
searchAction.invoke(searchInfo)
|
||||
else
|
||||
defaultAction.invoke()
|
||||
}
|
||||
TypeMode.MAGIKEYBOARD -> keyboardSelectionAction.invoke(searchInfo)
|
||||
else -> {
|
||||
// In this case, error
|
||||
removeModesFromIntent(intent)
|
||||
removeInfoFromIntent(intent)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (searchInfo != null)
|
||||
searchAction.invoke(searchInfo)
|
||||
else
|
||||
defaultAction.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
SpecialMode.REGISTRATION -> {
|
||||
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent)
|
||||
removeModesFromIntent(intent)
|
||||
removeInfoFromIntent(intent)
|
||||
autofillRegistrationAction.invoke(registerInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* 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.helpers
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
class ExternalFileHelper {
|
||||
|
||||
private var activity: FragmentActivity? = null
|
||||
private var fragment: Fragment? = null
|
||||
|
||||
constructor(context: FragmentActivity) {
|
||||
this.activity = context
|
||||
this.fragment = null
|
||||
}
|
||||
|
||||
constructor(context: Fragment) {
|
||||
this.activity = context.activity
|
||||
this.fragment = context
|
||||
}
|
||||
|
||||
fun openDocument(getContent: Boolean = false,
|
||||
typeString: String = "*/*") {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
try {
|
||||
if (getContent) {
|
||||
openActivityWithActionGetContent(typeString)
|
||||
} else {
|
||||
openActivityWithActionOpenDocument(typeString)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to open document", e)
|
||||
showFileManagerDialogFragment()
|
||||
}
|
||||
} else {
|
||||
showFileManagerDialogFragment()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||
private fun openActivityWithActionOpenDocument(typeString: String) {
|
||||
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = typeString
|
||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||
}
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||
else
|
||||
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||
private fun openActivityWithActionGetContent(typeString: String) {
|
||||
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = typeString
|
||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||
}
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||
else
|
||||
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* To use in onActivityResultCallback in Fragment or Activity
|
||||
* @param onFileSelected Callback retrieve from data
|
||||
* @return true if requestCode was captured, false elsewhere
|
||||
*/
|
||||
fun onOpenDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
|
||||
onFileSelected: ((uri: Uri?) -> Unit)?): Boolean {
|
||||
|
||||
when (requestCode) {
|
||||
FILE_BROWSE -> {
|
||||
if (resultCode == RESULT_OK) {
|
||||
val filename = data?.dataString
|
||||
var keyUri: Uri? = null
|
||||
if (filename != null) {
|
||||
keyUri = UriUtil.parse(filename)
|
||||
}
|
||||
onFileSelected?.invoke(keyUri)
|
||||
}
|
||||
return true
|
||||
}
|
||||
GET_CONTENT, OPEN_DOC -> {
|
||||
if (resultCode == RESULT_OK) {
|
||||
if (data != null) {
|
||||
val uri = data.data
|
||||
if (uri != null) {
|
||||
try {
|
||||
// try to persist read and write permissions
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
activity?.contentResolver?.apply {
|
||||
takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// nop
|
||||
}
|
||||
onFileSelected?.invoke(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Show Browser dialog to select file picker app
|
||||
*/
|
||||
private fun showFileManagerDialogFragment() {
|
||||
try {
|
||||
if (fragment != null) {
|
||||
fragment?.parentFragmentManager
|
||||
} else {
|
||||
activity?.supportFragmentManager
|
||||
}?.let { fragmentManager ->
|
||||
FileManagerDialogFragment().show(fragmentManager, "browserDialog")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Can't open BrowserDialog", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun createDocument(titleString: String,
|
||||
typeString: String = "application/octet-stream"): Int? {
|
||||
val idCode = getUnusedCreateFileRequestCode()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = typeString
|
||||
putExtra(Intent.EXTRA_TITLE, titleString)
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(intent, idCode)
|
||||
else
|
||||
activity?.startActivityForResult(intent, idCode)
|
||||
return idCode
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to create document", e)
|
||||
showFileManagerDialogFragment()
|
||||
}
|
||||
} else {
|
||||
showFileManagerDialogFragment()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* To use in onActivityResultCallback in Fragment or Activity
|
||||
* @param onFileCreated Callback retrieve from data
|
||||
* @return true if requestCode was captured, false elsewhere
|
||||
*/
|
||||
fun onCreateDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
|
||||
onFileCreated: (fileCreated: Uri?)->Unit) {
|
||||
// Retrieve the created URI from the file manager
|
||||
if (fileRequestCodes.contains(requestCode) && resultCode == RESULT_OK) {
|
||||
onFileCreated.invoke(data?.data)
|
||||
fileRequestCodes.remove(requestCode)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "OpenFileHelper"
|
||||
|
||||
private const val GET_CONTENT = 25745
|
||||
private const val OPEN_DOC = 25845
|
||||
private const val FILE_BROWSE = 25645
|
||||
|
||||
private var CREATE_FILE_REQUEST_CODE_DEFAULT = 3853
|
||||
private var fileRequestCodes = ArrayList<Int>()
|
||||
|
||||
private fun getUnusedCreateFileRequestCode(): Int {
|
||||
val newCreateFileRequestCode = CREATE_FILE_REQUEST_CODE_DEFAULT++
|
||||
fileRequestCodes.add(newCreateFileRequestCode)
|
||||
return newCreateFileRequestCode
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
fun allowCreateDocumentByStorageAccessFramework(packageManager: PackageManager,
|
||||
typeString: String = "application/octet-stream"): Boolean {
|
||||
return when {
|
||||
// To check if a custom file manager can manage the ACTION_CREATE_DOCUMENT
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT -> {
|
||||
packageManager.queryIntentActivities(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = typeString
|
||||
}, PackageManager.MATCH_DEFAULT_ONLY).isNotEmpty()
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun View.setOpenDocumentClickListener(externalFileHelper: ExternalFileHelper?) {
|
||||
externalFileHelper?.let { fileHelper ->
|
||||
setOnClickListener {
|
||||
fileHelper.openDocument()
|
||||
}
|
||||
setOnLongClickListener {
|
||||
fileHelper.openDocument(true)
|
||||
true
|
||||
}
|
||||
} ?: kotlin.run {
|
||||
setOnClickListener(null)
|
||||
setOnLongClickListener(null)
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.helpers
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
class OpenFileHelper {
|
||||
|
||||
private var activity: Activity? = null
|
||||
private var fragment: Fragment? = null
|
||||
|
||||
val openFileOnClickViewListener: OpenFileOnClickViewListener
|
||||
get() = OpenFileOnClickViewListener()
|
||||
|
||||
constructor(context: Activity) {
|
||||
this.activity = context
|
||||
this.fragment = null
|
||||
}
|
||||
|
||||
constructor(context: Fragment) {
|
||||
this.activity = context.activity
|
||||
this.fragment = context
|
||||
}
|
||||
|
||||
inner class OpenFileOnClickViewListener : View.OnClickListener {
|
||||
|
||||
override fun onClick(v: View) {
|
||||
try {
|
||||
try {
|
||||
openActivityWithActionOpenDocument()
|
||||
} catch(e: Exception) {
|
||||
openActivityWithActionGetContent()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Enable to start the file picker activity", e)
|
||||
// Open browser dialog
|
||||
if (lookForOpenIntentsFilePicker())
|
||||
showBrowserDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun openActivityWithActionOpenDocument() {
|
||||
val intentOpenDocument = Intent(APP_ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||
else
|
||||
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun openActivityWithActionGetContent() {
|
||||
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||
else
|
||||
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||
}
|
||||
|
||||
private fun lookForOpenIntentsFilePicker(): Boolean {
|
||||
var showBrowser = false
|
||||
try {
|
||||
if (isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) {
|
||||
val intent = Intent(OPEN_INTENTS_FILE_BROWSE)
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(intent, FILE_BROWSE)
|
||||
else
|
||||
activity?.startActivityForResult(intent, FILE_BROWSE)
|
||||
} else {
|
||||
showBrowser = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Enable to start OPEN_INTENTS_FILE_BROWSE", e)
|
||||
showBrowser = true
|
||||
}
|
||||
|
||||
return showBrowser
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the specified action can be used as an intent. This
|
||||
* method queries the package manager for installed packages that can
|
||||
* respond to an intent with the specified action. If no suitable package is
|
||||
* found, this method returns false.
|
||||
*
|
||||
* @param context The application's environment.
|
||||
* @param action The Intent action to check for availability.
|
||||
*
|
||||
* @return True if an Intent with the specified action can be sent and
|
||||
* responded to, false otherwise.
|
||||
*/
|
||||
private fun isIntentAvailable(context: Context, action: String): Boolean {
|
||||
val packageManager = context.packageManager
|
||||
val intent = Intent(action)
|
||||
val list = packageManager.queryIntentActivities(intent,
|
||||
PackageManager.MATCH_DEFAULT_ONLY)
|
||||
return list.size > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Show Browser dialog to select file picker app
|
||||
*/
|
||||
private fun showBrowserDialog() {
|
||||
try {
|
||||
val browserDialogFragment = BrowserDialogFragment()
|
||||
if (fragment != null && fragment!!.fragmentManager != null)
|
||||
browserDialogFragment.show(fragment!!.fragmentManager!!, "browserDialog")
|
||||
else
|
||||
browserDialogFragment.show((activity as FragmentActivity).supportFragmentManager, "browserDialog")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Can't open BrowserDialog", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* To use in onActivityResultCallback in Fragment or Activity
|
||||
* @param keyFileCallback Callback retrieve from data
|
||||
* @return true if requestCode was captured, false elsechere
|
||||
*/
|
||||
fun onActivityResultCallback(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
data: Intent?,
|
||||
keyFileCallback: ((uri: Uri?) -> Unit)?): Boolean {
|
||||
|
||||
when (requestCode) {
|
||||
FILE_BROWSE -> {
|
||||
if (resultCode == RESULT_OK) {
|
||||
val filename = data?.dataString
|
||||
var keyUri: Uri? = null
|
||||
if (filename != null) {
|
||||
keyUri = UriUtil.parse(filename)
|
||||
}
|
||||
keyFileCallback?.invoke(keyUri)
|
||||
}
|
||||
return true
|
||||
}
|
||||
GET_CONTENT, OPEN_DOC -> {
|
||||
if (resultCode == RESULT_OK) {
|
||||
if (data != null) {
|
||||
val uri = data.data
|
||||
if (uri != null) {
|
||||
try {
|
||||
// try to persist read and write permissions
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
activity?.contentResolver?.apply {
|
||||
takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// nop
|
||||
}
|
||||
keyFileCallback?.invoke(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "OpenFileHelper"
|
||||
|
||||
private var APP_ACTION_OPEN_DOCUMENT: String = try {
|
||||
Intent::class.java.getField("ACTION_OPEN_DOCUMENT").get(null) as String
|
||||
} catch (e: Exception) {
|
||||
"android.intent.action.OPEN_DOCUMENT"
|
||||
}
|
||||
|
||||
const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE"
|
||||
|
||||
private const val GET_CONTENT = 25745
|
||||
private const val OPEN_DOC = 25845
|
||||
private const val FILE_BROWSE = 25645
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.helpers
|
||||
|
||||
import android.content.Context
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.kunzisoft.keepass.activities.helpers
|
||||
|
||||
enum class SpecialMode {
|
||||
DEFAULT,
|
||||
SEARCH,
|
||||
SAVE,
|
||||
SELECTION,
|
||||
REGISTRATION;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.kunzisoft.keepass.activities.helpers
|
||||
|
||||
enum class TypeMode {
|
||||
DEFAULT, MAGIKEYBOARD, AUTOFILL
|
||||
}
|
||||
@@ -1,56 +1,44 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.lock
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
|
||||
abstract class LockingActivity : StylishActivity() {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "LockingActivity"
|
||||
|
||||
const val RESULT_EXIT_LOCK = 1450
|
||||
|
||||
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
||||
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
||||
}
|
||||
abstract class LockingActivity : SpecialModeActivity() {
|
||||
|
||||
protected var mTimeoutEnable: Boolean = true
|
||||
|
||||
@@ -58,13 +46,23 @@ abstract class LockingActivity : StylishActivity() {
|
||||
private var mExitLock: Boolean = false
|
||||
|
||||
// Force readOnly if Entry Selection mode
|
||||
protected var mReadOnly: Boolean = false
|
||||
protected var mReadOnly: Boolean
|
||||
get() {
|
||||
return field || mSelectionMode
|
||||
return mReadOnlyToSave
|
||||
}
|
||||
protected var mSelectionMode: Boolean = false
|
||||
set(value) {
|
||||
mReadOnlyToSave = value
|
||||
}
|
||||
private var mReadOnlyToSave: Boolean = false
|
||||
protected var mAutoSaveEnable: Boolean = true
|
||||
|
||||
var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||
private set
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (savedInstanceState != null
|
||||
@@ -76,16 +74,19 @@ abstract class LockingActivity : StylishActivity() {
|
||||
}
|
||||
|
||||
if (mTimeoutEnable) {
|
||||
mLockReceiver = LockReceiver()
|
||||
val intentFilter = IntentFilter().apply {
|
||||
addAction(Intent.ACTION_SCREEN_OFF)
|
||||
addAction(LOCK_ACTION)
|
||||
mLockReceiver = LockReceiver {
|
||||
closeDatabase()
|
||||
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
|
||||
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
|
||||
// Add onActivityForResult response
|
||||
setResult(RESULT_EXIT_LOCK)
|
||||
closeOptionsMenu()
|
||||
finish()
|
||||
}
|
||||
registerReceiver(mLockReceiver, intentFilter)
|
||||
registerLockReceiver(mLockReceiver)
|
||||
}
|
||||
|
||||
mExitLock = false
|
||||
mReadOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState, intent)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
@@ -101,8 +102,22 @@ abstract class LockingActivity : StylishActivity() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// If in ave or registration mode, don't allow read only
|
||||
if ((mSpecialMode == SpecialMode.SAVE
|
||||
|| mSpecialMode == SpecialMode.REGISTRATION)
|
||||
&& mReadOnly) {
|
||||
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||
|
||||
// To refresh when back to normal workflow from selection workflow
|
||||
mSelectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(intent)
|
||||
mReadOnlyToSave = ReadOnlyHelper.retrieveReadOnlyFromIntent(intent)
|
||||
mAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(this)
|
||||
|
||||
invalidateOptionsMenu()
|
||||
|
||||
if (mTimeoutEnable) {
|
||||
// End activity if database not loaded
|
||||
@@ -120,16 +135,19 @@ abstract class LockingActivity : StylishActivity() {
|
||||
TimeoutHelper.recordTime(this)
|
||||
}
|
||||
|
||||
invalidateOptionsMenu()
|
||||
LOCKING_ACTIVITY_UI_VISIBLE = true
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
ReadOnlyHelper.onSaveInstanceState(outState, mReadOnly)
|
||||
outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
LOCKING_ACTIVITY_UI_VISIBLE = false
|
||||
|
||||
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||
|
||||
super.onPause()
|
||||
|
||||
if (mTimeoutEnable) {
|
||||
@@ -139,49 +157,12 @@ abstract class LockingActivity : StylishActivity() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
unregisterLockReceiver(mLockReceiver)
|
||||
super.onDestroy()
|
||||
if (mLockReceiver != null)
|
||||
unregisterReceiver(mLockReceiver)
|
||||
}
|
||||
|
||||
inner class LockReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
// If allowed, lock and exit
|
||||
if (!TimeoutHelper.temporarilyDisableTimeout) {
|
||||
intent.action?.let {
|
||||
when (it) {
|
||||
Intent.ACTION_SCREEN_OFF -> if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(this@LockingActivity)) {
|
||||
lockAndExit()
|
||||
}
|
||||
LOCK_ACTION -> lockAndExit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun lockAndExit() {
|
||||
lock()
|
||||
}
|
||||
|
||||
/**
|
||||
* To reset the app timeout when a view is focused or changed
|
||||
*/
|
||||
protected fun resetAppTimeoutWhenViewFocusedOrChanged(vararg views: View?) {
|
||||
views.forEach {
|
||||
it?.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (hasFocus) {
|
||||
Log.d(TAG, "View focused, reset app timeout")
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
|
||||
}
|
||||
}
|
||||
if (it is ViewGroup) {
|
||||
for (i in 0..it.childCount) {
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(it.getChildAt(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
sendBroadcast(Intent(LOCK_ACTION))
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
@@ -193,24 +174,42 @@ abstract class LockingActivity : StylishActivity() {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Activity.lock() {
|
||||
// Stop the Magikeyboard service
|
||||
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
||||
MagikIME.removeEntry(this)
|
||||
companion object {
|
||||
|
||||
// Stop the notification service
|
||||
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
||||
const val TAG = "LockingActivity"
|
||||
|
||||
Log.i(Activity::class.java.name, "Shutdown " + localClassName +
|
||||
" after inactivity or manual lock")
|
||||
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).apply {
|
||||
cancelAll()
|
||||
const val RESULT_EXIT_LOCK = 1450
|
||||
|
||||
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
||||
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
||||
|
||||
private var LOCKING_ACTIVITY_UI_VISIBLE = false
|
||||
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* To reset the app timeout when a view is focused or changed
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
fun View.resetAppTimeoutWhenViewFocusedOrChanged(context: Context) {
|
||||
setOnTouchListener { _, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
//Log.d(LockingActivity.TAG, "View touched, try to reset app timeout")
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context)
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
setOnFocusChangeListener { _, _ ->
|
||||
//Log.d(LockingActivity.TAG, "View focused, try to reset app timeout")
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context)
|
||||
}
|
||||
if (this is ViewGroup) {
|
||||
for (i in 0..childCount) {
|
||||
getChildAt(i)?.resetAppTimeoutWhenViewFocusedOrChanged(context)
|
||||
}
|
||||
}
|
||||
// Clear data
|
||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
||||
// Add onActivityForResult response
|
||||
setResult(LockingActivity.RESULT_EXIT_LOCK)
|
||||
finish()
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX 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.
|
||||
*
|
||||
* KeePass DX 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.lock
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
|
||||
/**
|
||||
* Locking Hide Activity that sets FLAG_SECURE to prevent screenshots, and from
|
||||
* appearing in the recent app preview
|
||||
*/
|
||||
abstract class LockingHideActivity : LockingActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Several gingerbread devices have problems with FLAG_SECURE
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
|
||||
/* (non-Javadoc) Workaround for HTC Linkify issues
|
||||
* @see android.app.Activity#startActivity(android.content.Intent)
|
||||
*/
|
||||
override fun startActivity(intent: Intent) {
|
||||
try {
|
||||
if (intent.component != null && intent.component!!.shortClassName == ".HtcLinkifyDispatcherActivity") {
|
||||
intent.component = null
|
||||
}
|
||||
super.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
/* Catch the bad HTC implementation case */
|
||||
super.startActivity(Intent.createChooser(intent, null))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package com.kunzisoft.keepass.activities.selection
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.helpers.TypeMode
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.model.SearchInfo
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.SpecialModeView
|
||||
|
||||
/**
|
||||
* Activity to manage special mode (ie: selection mode)
|
||||
*/
|
||||
abstract class SpecialModeActivity : StylishActivity() {
|
||||
|
||||
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
|
||||
private var mTypeMode: TypeMode = TypeMode.DEFAULT
|
||||
|
||||
private var mSpecialModeView: SpecialModeView? = null
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (mSpecialMode != SpecialMode.DEFAULT)
|
||||
onCancelSpecialMode()
|
||||
else
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
/**
|
||||
* To call the regular onBackPressed() method in special mode
|
||||
*/
|
||||
protected fun onRegularBackPressed() {
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
/**
|
||||
* Intent sender uses special retains data in callback
|
||||
*/
|
||||
private fun isIntentSender(): Boolean {
|
||||
return (mSpecialMode == SpecialMode.SELECTION
|
||||
&& mTypeMode == TypeMode.AUTOFILL)
|
||||
/* TODO Registration callback #765
|
||||
|| (mSpecialMode == SpecialMode.REGISTRATION
|
||||
&& mTypeMode == TypeMode.AUTOFILL
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||
*/
|
||||
}
|
||||
|
||||
fun onLaunchActivitySpecialMode() {
|
||||
if (!isIntentSender()) {
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
open fun onValidateSpecialMode() {
|
||||
if (isIntentSender()) {
|
||||
super.finish()
|
||||
} else {
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
// To move the app in background
|
||||
moveTaskToBack(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun onCancelSpecialMode() {
|
||||
if (isIntentSender()) {
|
||||
// To get the app caller, only for IntentSender
|
||||
super.onBackPressed()
|
||||
} else {
|
||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
// To move the app in background
|
||||
moveTaskToBack(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun backToTheAppCaller() {
|
||||
if (isIntentSender()) {
|
||||
// To get the app caller, only for IntentSender
|
||||
super.onBackPressed()
|
||||
} else {
|
||||
// To move the app in background
|
||||
moveTaskToBack(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
||||
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
||||
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
||||
val searchInfo: SearchInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)?.searchInfo
|
||||
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
||||
|
||||
// To show the selection mode
|
||||
mSpecialModeView = findViewById(R.id.special_mode_view)
|
||||
mSpecialModeView?.apply {
|
||||
// Populate title
|
||||
val selectionModeStringId = when (mSpecialMode) {
|
||||
SpecialMode.DEFAULT, // Not important because hidden
|
||||
SpecialMode.SEARCH -> R.string.search_mode
|
||||
SpecialMode.SAVE -> R.string.save_mode
|
||||
SpecialMode.SELECTION -> R.string.selection_mode
|
||||
SpecialMode.REGISTRATION -> R.string.registration_mode
|
||||
}
|
||||
val typeModeStringId = when (mTypeMode) {
|
||||
TypeMode.DEFAULT, // Not important because hidden
|
||||
TypeMode.MAGIKEYBOARD -> R.string.magic_keyboard_title
|
||||
TypeMode.AUTOFILL -> R.string.autofill
|
||||
}
|
||||
title = getString(selectionModeStringId)
|
||||
if (mTypeMode != TypeMode.DEFAULT)
|
||||
title = "$title (${getString(typeModeStringId)})"
|
||||
// Populate subtitle
|
||||
subtitle = searchInfo?.getName(resources)
|
||||
|
||||
// Show the toolbar or not
|
||||
visible = when (mSpecialMode) {
|
||||
SpecialMode.DEFAULT -> false
|
||||
SpecialMode.SEARCH -> true
|
||||
SpecialMode.SAVE -> true
|
||||
SpecialMode.SELECTION -> true
|
||||
SpecialMode.REGISTRATION -> true
|
||||
}
|
||||
|
||||
// Add back listener
|
||||
onCancelButtonClickListener = View.OnClickListener {
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
|
||||
// Create menu
|
||||
menu.clear()
|
||||
if (mTypeMode == TypeMode.AUTOFILL) {
|
||||
menuInflater.inflate(R.menu.autofill, menu)
|
||||
setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.menu_block_autofill -> {
|
||||
blockAutofill(searchInfo)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// To hide home button from the regular toolbar in special mode
|
||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun blockAutofill(searchInfo: SearchInfo?) {
|
||||
val webDomain = searchInfo?.webDomain
|
||||
val applicationId = searchInfo?.applicationId
|
||||
if (webDomain != null) {
|
||||
PreferencesUtil.addWebDomainToBlocklist(this,
|
||||
webDomain)
|
||||
} else if (applicationId != null) {
|
||||
PreferencesUtil.addApplicationIdToBlocklist(this,
|
||||
applicationId)
|
||||
}
|
||||
onCancelSpecialMode()
|
||||
Toast.makeText(this.applicationContext,
|
||||
R.string.autofill_block_restart,
|
||||
Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,30 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.stylish
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.preference.PreferenceManager
|
||||
import android.content.res.Configuration
|
||||
import android.util.Log
|
||||
|
||||
import androidx.annotation.StyleRes
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
|
||||
/**
|
||||
* Class that provides functions to retrieve and assign a theme to a module
|
||||
@@ -37,18 +37,63 @@ object Stylish {
|
||||
* Initialize the class with a theme preference
|
||||
* @param context Context to retrieve the theme preference
|
||||
*/
|
||||
fun init(context: Context) {
|
||||
val stylishPrefKey = context.getString(R.string.setting_style_key)
|
||||
fun load(context: Context) {
|
||||
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
|
||||
themeString = PreferenceManager.getDefaultSharedPreferences(context).getString(stylishPrefKey, context.getString(R.string.list_style_name_light))
|
||||
themeString = PreferencesUtil.getStyle(context)
|
||||
}
|
||||
|
||||
fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {
|
||||
val systemNightMode = when (PreferencesUtil.getStyleBrightness(context)) {
|
||||
context.getString(R.string.list_style_brightness_light) -> false
|
||||
context.getString(R.string.list_style_brightness_night) -> true
|
||||
else -> {
|
||||
when (context.resources.configuration.uiMode.and(Configuration.UI_MODE_NIGHT_MASK)) {
|
||||
Configuration.UI_MODE_NIGHT_YES -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
return if (systemNightMode) {
|
||||
retrieveEquivalentNightStyle(context, styleString)
|
||||
} else {
|
||||
retrieveEquivalentLightStyle(context, styleString)
|
||||
}
|
||||
}
|
||||
|
||||
fun retrieveEquivalentLightStyle(context: Context, styleString: String): String {
|
||||
return when (styleString) {
|
||||
context.getString(R.string.list_style_name_night) -> context.getString(R.string.list_style_name_light)
|
||||
context.getString(R.string.list_style_name_black) -> context.getString(R.string.list_style_name_white)
|
||||
context.getString(R.string.list_style_name_dark) -> context.getString(R.string.list_style_name_clear)
|
||||
context.getString(R.string.list_style_name_blue_night) -> context.getString(R.string.list_style_name_blue)
|
||||
context.getString(R.string.list_style_name_red_night) -> context.getString(R.string.list_style_name_red)
|
||||
context.getString(R.string.list_style_name_purple_dark) -> context.getString(R.string.list_style_name_purple)
|
||||
else -> styleString
|
||||
}
|
||||
}
|
||||
|
||||
private fun retrieveEquivalentNightStyle(context: Context, styleString: String): String {
|
||||
return when (styleString) {
|
||||
context.getString(R.string.list_style_name_light) -> context.getString(R.string.list_style_name_night)
|
||||
context.getString(R.string.list_style_name_white) -> context.getString(R.string.list_style_name_black)
|
||||
context.getString(R.string.list_style_name_clear) -> context.getString(R.string.list_style_name_dark)
|
||||
context.getString(R.string.list_style_name_blue) -> context.getString(R.string.list_style_name_blue_night)
|
||||
context.getString(R.string.list_style_name_red) -> context.getString(R.string.list_style_name_red_night)
|
||||
context.getString(R.string.list_style_name_purple) -> context.getString(R.string.list_style_name_purple_dark)
|
||||
else -> styleString
|
||||
}
|
||||
}
|
||||
|
||||
fun defaultStyle(context: Context): String {
|
||||
return context.getString(R.string.list_style_name_light)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign the style to the class attribute
|
||||
* @param styleString Style id String
|
||||
*/
|
||||
fun assignStyle(styleString: String) {
|
||||
themeString = styleString
|
||||
fun assignStyle(context: Context, styleString: String) {
|
||||
PreferencesUtil.setStyle(context, styleString)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,13 +103,18 @@ object Stylish {
|
||||
*/
|
||||
@StyleRes
|
||||
fun getThemeId(context: Context): Int {
|
||||
|
||||
return when (themeString) {
|
||||
return when (retrieveEquivalentSystemStyle(context, themeString ?: context.getString(R.string.list_style_name_light))) {
|
||||
context.getString(R.string.list_style_name_night) -> R.style.KeepassDXStyle_Night
|
||||
context.getString(R.string.list_style_name_white) -> R.style.KeepassDXStyle_White
|
||||
context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black
|
||||
context.getString(R.string.list_style_name_clear) -> R.style.KeepassDXStyle_Clear
|
||||
context.getString(R.string.list_style_name_dark) -> R.style.KeepassDXStyle_Dark
|
||||
context.getString(R.string.list_style_name_blue) -> R.style.KeepassDXStyle_Blue
|
||||
context.getString(R.string.list_style_name_blue_night) -> R.style.KeepassDXStyle_Blue_Night
|
||||
context.getString(R.string.list_style_name_red) -> R.style.KeepassDXStyle_Red
|
||||
context.getString(R.string.list_style_name_red_night) -> R.style.KeepassDXStyle_Red_Night
|
||||
context.getString(R.string.list_style_name_purple) -> R.style.KeepassDXStyle_Purple
|
||||
context.getString(R.string.list_style_name_purple_dark) -> R.style.KeepassDXStyle_Purple_Dark
|
||||
else -> R.style.KeepassDXStyle_Light
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,64 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.stylish
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.util.Log
|
||||
import android.view.WindowManager
|
||||
|
||||
/**
|
||||
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
|
||||
* appearing in the recent app preview
|
||||
*/
|
||||
abstract class StylishActivity : AppCompatActivity() {
|
||||
|
||||
@StyleRes
|
||||
private var themeId: Int = 0
|
||||
|
||||
/* (non-Javadoc) Workaround for HTC Linkify issues
|
||||
* @see android.app.Activity#startActivity(android.content.Intent)
|
||||
*/
|
||||
override fun startActivity(intent: Intent) {
|
||||
try {
|
||||
intent.component?.let {
|
||||
if (it.shortClassName == ".HtcLinkifyDispatcherActivity")
|
||||
intent.component = null
|
||||
}
|
||||
super.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
/* Catch the bad HTC implementation case */
|
||||
super.startActivity(Intent.createChooser(intent, null))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
this.themeId = Stylish.getThemeId(this)
|
||||
setTheme(themeId)
|
||||
|
||||
// Several gingerbread devices have problems with FLAG_SECURE
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.stylish
|
||||
@@ -42,18 +42,33 @@ abstract class StylishFragment : Fragment() {
|
||||
contextThemed = ContextThemeWrapper(context, themeId)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
// To fix status bar color
|
||||
if (activity != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val window = activity!!.window
|
||||
|
||||
val attrColorPrimaryDark = intArrayOf(android.R.attr.colorPrimaryDark)
|
||||
val taColorPrimaryDark = contextThemed?.theme?.obtainStyledAttributes(attrColorPrimaryDark)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val window = requireActivity().window
|
||||
val defaultColor = Color.BLACK
|
||||
window.statusBarColor = taColorPrimaryDark?.getColor(0, defaultColor) ?: defaultColor
|
||||
taColorPrimaryDark?.recycle()
|
||||
}
|
||||
|
||||
try {
|
||||
val taStatusBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.statusBarColor))
|
||||
window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor
|
||||
taStatusBarColor?.recycle()
|
||||
} catch (e: Exception) {}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
try {
|
||||
val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar))
|
||||
if (taWindowStatusLight?.getBoolean(0, false) == true) {
|
||||
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
|
||||
}
|
||||
taWindowStatusLight?.recycle()
|
||||
} catch (e: Exception) {}
|
||||
}
|
||||
try {
|
||||
val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor))
|
||||
window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor
|
||||
taNavigationBarColor?.recycle()
|
||||
} catch (e: Exception) {}
|
||||
}
|
||||
return super.onCreateView(inflater, container, savedInstanceState)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.view.collapse
|
||||
|
||||
abstract class AnimatedItemsAdapter<Item, T: RecyclerView.ViewHolder>(val context: Context)
|
||||
: RecyclerView.Adapter<T>() {
|
||||
|
||||
protected val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
var itemsList: MutableList<Item> = ArrayList()
|
||||
private set
|
||||
|
||||
var onDeleteButtonClickListener: ((item: Item)->Unit)? = null
|
||||
private var mItemToRemove: Item? = null
|
||||
|
||||
var onListSizeChangedListener: ((previousSize: Int, newSize: Int)->Unit)? = null
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return itemsList.size
|
||||
}
|
||||
|
||||
open fun assignItems(items: List<Item>) {
|
||||
val previousSize = itemsList.size
|
||||
itemsList.apply {
|
||||
clear()
|
||||
addAll(items)
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
|
||||
}
|
||||
|
||||
open fun isEmpty(): Boolean {
|
||||
return itemsList.isEmpty()
|
||||
}
|
||||
|
||||
open fun contains(item: Item): Boolean {
|
||||
return itemsList.contains(item)
|
||||
}
|
||||
|
||||
open fun indexOf(item: Item): Int {
|
||||
return itemsList.indexOf(item)
|
||||
}
|
||||
|
||||
open fun putItem(item: Item) {
|
||||
val previousSize = itemsList.size
|
||||
if (itemsList.contains(item)) {
|
||||
val index = itemsList.indexOf(item)
|
||||
itemsList.removeAt(index)
|
||||
itemsList.add(index, item)
|
||||
notifyItemChanged(index)
|
||||
} else {
|
||||
itemsList.add(item)
|
||||
notifyItemInserted(itemsList.indexOf(item))
|
||||
}
|
||||
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
|
||||
}
|
||||
|
||||
/**
|
||||
* Only replace [oldItem] by [newItem] if [oldItem] exists
|
||||
*/
|
||||
open fun replaceItem(oldItem: Item, newItem: Item) {
|
||||
if (itemsList.contains(oldItem)) {
|
||||
val index = itemsList.indexOf(oldItem)
|
||||
itemsList.removeAt(index)
|
||||
itemsList.add(index, newItem)
|
||||
notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only remove [item] if doesn't exists
|
||||
*/
|
||||
open fun removeItem(item: Item) {
|
||||
if (itemsList.contains(item)) {
|
||||
mItemToRemove = item
|
||||
notifyItemChanged(itemsList.indexOf(item))
|
||||
}
|
||||
}
|
||||
|
||||
protected fun performDeletion(holder: T, item: Item): Boolean {
|
||||
val effectivelyDeletionPerformed = mItemToRemove == item
|
||||
if (effectivelyDeletionPerformed) {
|
||||
holder.itemView.collapse(true) {
|
||||
deleteItem(item)
|
||||
}
|
||||
}
|
||||
return effectivelyDeletionPerformed
|
||||
}
|
||||
|
||||
protected fun onBindDeleteButton(holder: T, deleteButton: View, item: Item, position: Int) {
|
||||
deleteButton.apply {
|
||||
visibility = View.VISIBLE
|
||||
if (performDeletion(holder, item)) {
|
||||
setOnClickListener(null)
|
||||
} else {
|
||||
setOnClickListener {
|
||||
onDeleteButtonClickListener?.invoke(item)
|
||||
mItemToRemove = item
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteItem(item: Item) {
|
||||
val previousSize = itemsList.size
|
||||
val position = itemsList.indexOf(item)
|
||||
if (position >= 0) {
|
||||
itemsList.removeAt(position)
|
||||
notifyItemRemoved(position)
|
||||
mItemToRemove = null
|
||||
for (i in 0 until itemsList.size) {
|
||||
notifyItemChanged(i)
|
||||
}
|
||||
}
|
||||
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
if (itemsList.size > 0) {
|
||||
itemsList.clear()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.TypedArray
|
||||
import android.graphics.Color
|
||||
import android.text.format.Formatter
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.ImageViewerActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.model.AttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||
import com.kunzisoft.keepass.view.expand
|
||||
import kotlin.math.max
|
||||
|
||||
|
||||
class EntryAttachmentsItemsAdapter(context: Context)
|
||||
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
|
||||
|
||||
var database: Database? = null
|
||||
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
|
||||
var onBinaryPreviewLoaded: ((item: EntryAttachmentState) -> Unit)? = null
|
||||
|
||||
// Approximately
|
||||
private val mImagePreviewMaxWidth = max(
|
||||
context.resources.displayMetrics.widthPixels,
|
||||
context.resources.getDimensionPixelSize(R.dimen.item_file_info_height)
|
||||
)
|
||||
private var mTitleColor: Int
|
||||
|
||||
init {
|
||||
// Get the primary text color of the theme
|
||||
val typedValue = TypedValue()
|
||||
context.theme.resolveAttribute(android.R.attr.textColorPrimary, typedValue, true)
|
||||
val typedArray: TypedArray = context.obtainStyledAttributes(typedValue.data, intArrayOf(
|
||||
android.R.attr.textColor))
|
||||
mTitleColor = typedArray.getColor(0, -1)
|
||||
typedArray.recycle()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryBinariesViewHolder {
|
||||
return EntryBinariesViewHolder(inflater.inflate(R.layout.item_attachment, parent, false))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: EntryBinariesViewHolder, position: Int) {
|
||||
val entryAttachmentState = itemsList[position]
|
||||
|
||||
holder.itemView.visibility = View.VISIBLE
|
||||
holder.binaryFileThumbnail.apply {
|
||||
// Perform image loading only if upload is finished
|
||||
if (entryAttachmentState.downloadState != AttachmentState.START
|
||||
&& entryAttachmentState.downloadState != AttachmentState.IN_PROGRESS) {
|
||||
// Show the bitmap image if loaded
|
||||
if (entryAttachmentState.previewState == AttachmentState.NULL) {
|
||||
entryAttachmentState.previewState = AttachmentState.IN_PROGRESS
|
||||
// Load the bitmap image
|
||||
database?.let { database ->
|
||||
BinaryDatabaseManager.loadBitmap(
|
||||
database,
|
||||
entryAttachmentState.attachment.binaryData,
|
||||
mImagePreviewMaxWidth
|
||||
) { imageLoaded ->
|
||||
if (imageLoaded == null) {
|
||||
entryAttachmentState.previewState = AttachmentState.ERROR
|
||||
visibility = View.GONE
|
||||
onBinaryPreviewLoaded?.invoke(entryAttachmentState)
|
||||
} else {
|
||||
entryAttachmentState.previewState = AttachmentState.COMPLETE
|
||||
setImageBitmap(imageLoaded)
|
||||
if (visibility != View.VISIBLE) {
|
||||
expand(true, resources.getDimensionPixelSize(R.dimen.item_file_info_height)) {
|
||||
onBinaryPreviewLoaded?.invoke(entryAttachmentState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
this.setOnClickListener {
|
||||
ImageViewerActivity.getInstance(context, entryAttachmentState.attachment)
|
||||
}
|
||||
}
|
||||
holder.binaryFileBroken.apply {
|
||||
setColorFilter(Color.RED)
|
||||
visibility = if (entryAttachmentState.attachment.binaryData.isCorrupted) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
holder.binaryFileTitle.text = entryAttachmentState.attachment.name
|
||||
if (entryAttachmentState.attachment.binaryData.isCorrupted) {
|
||||
holder.binaryFileTitle.setTextColor(Color.RED)
|
||||
} else {
|
||||
holder.binaryFileTitle.setTextColor(mTitleColor)
|
||||
}
|
||||
|
||||
val size = entryAttachmentState.attachment.binaryData.getSize()
|
||||
holder.binaryFileSize.text = Formatter.formatFileSize(context, size)
|
||||
holder.binaryFileCompression.apply {
|
||||
if (entryAttachmentState.attachment.binaryData.isCompressed) {
|
||||
text = CompressionAlgorithm.GZip.getName(context.resources)
|
||||
visibility = View.VISIBLE
|
||||
} else {
|
||||
text = ""
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
when (entryAttachmentState.streamDirection) {
|
||||
StreamDirection.UPLOAD -> {
|
||||
holder.binaryFileProgressIcon.isActivated = true
|
||||
when (entryAttachmentState.downloadState) {
|
||||
AttachmentState.START,
|
||||
AttachmentState.IN_PROGRESS -> {
|
||||
holder.binaryFileProgressContainer.visibility = View.VISIBLE
|
||||
holder.binaryFileProgress.apply {
|
||||
visibility = View.VISIBLE
|
||||
progress = entryAttachmentState.downloadProgression
|
||||
}
|
||||
holder.binaryFileDeleteButton.apply {
|
||||
visibility = View.GONE
|
||||
setOnClickListener(null)
|
||||
}
|
||||
}
|
||||
AttachmentState.NULL,
|
||||
AttachmentState.ERROR,
|
||||
AttachmentState.CANCELED,
|
||||
AttachmentState.COMPLETE -> {
|
||||
holder.binaryFileProgressContainer.visibility = View.GONE
|
||||
holder.binaryFileProgress.visibility = View.GONE
|
||||
holder.binaryFileDeleteButton.apply {
|
||||
visibility = View.VISIBLE
|
||||
onBindDeleteButton(holder, this, entryAttachmentState, position)
|
||||
}
|
||||
}
|
||||
}
|
||||
holder.binaryFileInfo.setOnClickListener(null)
|
||||
}
|
||||
StreamDirection.DOWNLOAD -> {
|
||||
holder.binaryFileProgressIcon.isActivated = false
|
||||
holder.binaryFileProgressContainer.visibility = View.VISIBLE
|
||||
holder.binaryFileDeleteButton.visibility = View.GONE
|
||||
holder.binaryFileProgress.apply {
|
||||
visibility = when (entryAttachmentState.downloadState) {
|
||||
AttachmentState.NULL,
|
||||
AttachmentState.COMPLETE,
|
||||
AttachmentState.CANCELED,
|
||||
AttachmentState.ERROR -> View.GONE
|
||||
|
||||
AttachmentState.START,
|
||||
AttachmentState.IN_PROGRESS -> View.VISIBLE
|
||||
}
|
||||
progress = entryAttachmentState.downloadProgression
|
||||
}
|
||||
holder.binaryFileInfo.setOnClickListener {
|
||||
onItemClickListener?.invoke(entryAttachmentState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
var binaryFileThumbnail: ImageView = itemView.findViewById(R.id.item_attachment_thumbnail)
|
||||
var binaryFileInfo: View = itemView.findViewById(R.id.item_attachment_info)
|
||||
var binaryFileBroken: ImageView = itemView.findViewById(R.id.item_attachment_broken)
|
||||
var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title)
|
||||
var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size)
|
||||
var binaryFileCompression: TextView = itemView.findViewById(R.id.item_attachment_compression)
|
||||
var binaryFileProgressContainer: View = itemView.findViewById(R.id.item_attachment_progress_container)
|
||||
var binaryFileProgressIcon: ImageView = itemView.findViewById(R.id.item_attachment_icon)
|
||||
var binaryFileProgress: ProgressBar = itemView.findViewById(R.id.item_attachment_progress)
|
||||
var binaryFileDeleteButton: View = itemView.findViewById(R.id.item_attachment_delete_button)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
@@ -7,13 +26,13 @@ import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.EntryVersioned
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
|
||||
class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHistoryAdapter.EntryHistoryViewHolder>() {
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
var entryHistoryList: MutableList<EntryVersioned> = ArrayList()
|
||||
var onItemClickListener: ((item: EntryVersioned, position: Int)->Unit)? = null
|
||||
var entryHistoryList: MutableList<Entry> = ArrayList()
|
||||
var onItemClickListener: ((item: Entry, position: Int)->Unit)? = null
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryHistoryViewHolder {
|
||||
return EntryHistoryViewHolder(inflater.inflate(R.layout.item_list_entry_history, parent, false))
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
@@ -15,7 +34,7 @@ import java.util.ArrayList
|
||||
class FieldsAdapter(context: Context) : RecyclerView.Adapter<FieldsAdapter.FieldViewHolder>() {
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
var fields: MutableList<Field> = ArrayList()
|
||||
private var fields: MutableList<Field> = ArrayList()
|
||||
var onItemClickListener: OnItemClickListener? = null
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FieldViewHolder {
|
||||
@@ -33,6 +52,11 @@ class FieldsAdapter(context: Context) : RecyclerView.Adapter<FieldsAdapter.Field
|
||||
return fields.size
|
||||
}
|
||||
|
||||
fun setFields(fieldsToAdd: List<Field>) {
|
||||
fields.clear()
|
||||
fields.addAll(fieldsToAdd)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
fields.clear()
|
||||
}
|
||||
|
||||
@@ -1,50 +1,54 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import android.net.Uri
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.util.TypedValue
|
||||
import android.view.*
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.ViewSwitcher
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryEntity
|
||||
import com.kunzisoft.keepass.utils.FileDatabaseInfo
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.model.DatabaseFile
|
||||
import com.kunzisoft.keepass.view.collapse
|
||||
import com.kunzisoft.keepass.view.expand
|
||||
|
||||
class FileDatabaseHistoryAdapter(private val context: Context)
|
||||
class FileDatabaseHistoryAdapter(context: Context)
|
||||
: RecyclerView.Adapter<FileDatabaseHistoryAdapter.FileDatabaseHistoryViewHolder>() {
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
private var fileItemOpenListener: ((FileDatabaseHistoryEntity)->Unit)? = null
|
||||
private var fileSelectClearListener: ((FileDatabaseHistoryEntity)->Boolean)? = null
|
||||
private var saveAliasListener: ((FileDatabaseHistoryEntity)->Unit)? = null
|
||||
private var defaultDatabaseListener: ((DatabaseFile?) -> Unit)? = null
|
||||
private var fileItemOpenListener: ((DatabaseFile)->Unit)? = null
|
||||
private var fileSelectClearListener: ((DatabaseFile)->Boolean)? = null
|
||||
private var saveAliasListener: ((DatabaseFile)->Unit)? = null
|
||||
|
||||
private val listDatabaseFiles = ArrayList<FileDatabaseHistoryEntity>()
|
||||
private val listDatabaseFiles = ArrayList<DatabaseFile>()
|
||||
|
||||
private var mExpandedPosition = -1
|
||||
private var mPreviousExpandedPosition = -1
|
||||
private var mDefaultDatabaseFile: DatabaseFile? = null
|
||||
private var mExpandedDatabaseFile: DatabaseFile? = null
|
||||
private var mPreviousExpandedDatabaseFile: DatabaseFile? = null
|
||||
|
||||
@ColorInt
|
||||
private val defaultColor: Int
|
||||
@@ -61,47 +65,74 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileDatabaseHistoryViewHolder {
|
||||
val view = inflater.inflate(R.layout.item_file_row, parent, false)
|
||||
val view = inflater.inflate(R.layout.item_file_info, parent, false)
|
||||
return FileDatabaseHistoryViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) {
|
||||
// Get info from position
|
||||
val fileHistoryEntity = listDatabaseFiles[position]
|
||||
val fileDatabaseInfo = FileDatabaseInfo(context, fileHistoryEntity.databaseUri)
|
||||
val databaseFile = listDatabaseFiles[position]
|
||||
|
||||
// Click item to open file
|
||||
if (fileItemOpenListener != null)
|
||||
holder.fileContainer.setOnClickListener {
|
||||
fileItemOpenListener?.invoke(fileHistoryEntity)
|
||||
holder.fileContainer.setOnClickListener {
|
||||
fileItemOpenListener?.invoke(databaseFile)
|
||||
}
|
||||
|
||||
// Default database
|
||||
holder.defaultFileButton.apply {
|
||||
this.isChecked = mDefaultDatabaseFile == databaseFile
|
||||
setOnClickListener {
|
||||
defaultDatabaseListener?.invoke(if (isChecked) databaseFile else null)
|
||||
}
|
||||
}
|
||||
|
||||
// File alias
|
||||
holder.fileAlias.text = fileDatabaseInfo.retrieveDatabaseAlias(fileHistoryEntity.databaseAlias)
|
||||
holder.fileAlias.text = databaseFile.databaseAlias
|
||||
|
||||
// File path
|
||||
holder.filePath.text = UriUtil.decode(fileDatabaseInfo.fileUri?.toString())
|
||||
holder.filePath.text = databaseFile.databaseDecodedPath
|
||||
|
||||
holder.filePreciseInfoContainer.visibility = if (fileDatabaseInfo.found()) {
|
||||
// Modification
|
||||
holder.fileModification.text = fileDatabaseInfo.getModificationString()
|
||||
// Size
|
||||
holder.fileSize.text = fileDatabaseInfo.getSizeString()
|
||||
if (databaseFile.databaseFileExists) {
|
||||
holder.fileInformationButton.clearColorFilter()
|
||||
} else {
|
||||
holder.fileInformationButton.setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)
|
||||
}
|
||||
|
||||
View.VISIBLE
|
||||
} else
|
||||
View.GONE
|
||||
// Modification
|
||||
databaseFile.databaseLastModified?.let {
|
||||
holder.fileModification.text = it
|
||||
holder.fileModificationContainer.visibility = View.VISIBLE
|
||||
} ?: run {
|
||||
holder.fileModificationContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
// Size
|
||||
databaseFile.databaseSize?.let {
|
||||
holder.fileSize.text = it
|
||||
holder.fileSize.visibility = View.VISIBLE
|
||||
} ?: run {
|
||||
holder.fileSize.visibility = View.GONE
|
||||
}
|
||||
|
||||
// Click on information
|
||||
val isExpanded = position == mExpandedPosition
|
||||
//This line hides or shows the layout in question
|
||||
holder.fileExpandContainer.visibility = if (isExpanded) View.VISIBLE else View.GONE
|
||||
val isExpanded = databaseFile == mExpandedDatabaseFile
|
||||
// Hides or shows info
|
||||
holder.fileExpandContainer.apply {
|
||||
if (isExpanded) {
|
||||
if (visibility != View.VISIBLE) {
|
||||
visibility = View.VISIBLE
|
||||
expand(true, resources.getDimensionPixelSize(R.dimen.item_file_info_height))
|
||||
}
|
||||
} else {
|
||||
collapse(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Save alias modification
|
||||
holder.fileAliasCloseButton.setOnClickListener {
|
||||
// Change the alias
|
||||
fileHistoryEntity.databaseAlias = holder.fileAliasEdit.text.toString()
|
||||
saveAliasListener?.invoke(fileHistoryEntity)
|
||||
databaseFile.databaseAlias = holder.fileAliasEdit.text.toString()
|
||||
saveAliasListener?.invoke(databaseFile)
|
||||
|
||||
// Finish save mode
|
||||
holder.fileMainSwitcher.showPrevious()
|
||||
@@ -116,20 +147,22 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
||||
}
|
||||
|
||||
holder.fileDeleteButton.setOnClickListener {
|
||||
fileSelectClearListener?.invoke(fileHistoryEntity)
|
||||
fileSelectClearListener?.invoke(databaseFile)
|
||||
}
|
||||
|
||||
if (isExpanded) {
|
||||
mPreviousExpandedPosition = position
|
||||
mPreviousExpandedDatabaseFile = databaseFile
|
||||
}
|
||||
|
||||
holder.fileInformation.setOnClickListener {
|
||||
mExpandedPosition = if (isExpanded) -1 else position
|
||||
|
||||
// Notify change
|
||||
if (mPreviousExpandedPosition < itemCount)
|
||||
notifyItemChanged(mPreviousExpandedPosition)
|
||||
notifyItemChanged(position)
|
||||
holder.fileInformationButton.apply {
|
||||
animate().rotation(if (isExpanded) 180F else 0F).start()
|
||||
setOnClickListener {
|
||||
mExpandedDatabaseFile = if (isExpanded) null else databaseFile
|
||||
// Notify change
|
||||
val previousExpandedPosition = listDatabaseFiles.indexOf(mPreviousExpandedDatabaseFile)
|
||||
notifyItemChanged(previousExpandedPosition)
|
||||
val expandedPosition = listDatabaseFiles.indexOf(mExpandedDatabaseFile)
|
||||
notifyItemChanged(expandedPosition)
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh View / Close alias modification if not contains fileAlias
|
||||
@@ -142,33 +175,72 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
||||
return listDatabaseFiles.size
|
||||
}
|
||||
|
||||
fun addDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<FileDatabaseHistoryEntity>) {
|
||||
fun clearDatabaseFileHistoryList() {
|
||||
listDatabaseFiles.clear()
|
||||
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
|
||||
}
|
||||
|
||||
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: FileDatabaseHistoryEntity) {
|
||||
listDatabaseFiles.remove(fileDatabaseHistoryToDelete)
|
||||
fun addDatabaseFileHistory(fileDatabaseHistoryToAdd: DatabaseFile) {
|
||||
listDatabaseFiles.add(0, fileDatabaseHistoryToAdd)
|
||||
notifyItemInserted(0)
|
||||
}
|
||||
|
||||
fun setOnFileDatabaseHistoryOpenListener(listener : ((FileDatabaseHistoryEntity)->Unit)?) {
|
||||
fun updateDatabaseFileHistory(fileDatabaseHistoryToUpdate: DatabaseFile) {
|
||||
val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToUpdate)
|
||||
if (listDatabaseFiles.remove(fileDatabaseHistoryToUpdate)) {
|
||||
listDatabaseFiles.add(index, fileDatabaseHistoryToUpdate)
|
||||
notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: DatabaseFile) {
|
||||
val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToDelete)
|
||||
if (listDatabaseFiles.remove(fileDatabaseHistoryToDelete)) {
|
||||
notifyItemRemoved(index)
|
||||
}
|
||||
}
|
||||
|
||||
fun replaceAllDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<DatabaseFile>) {
|
||||
if (listDatabaseFiles.isEmpty()) {
|
||||
listFileDatabaseHistoryToAdd.forEach {
|
||||
listDatabaseFiles.add(it)
|
||||
notifyItemInserted(listDatabaseFiles.size)
|
||||
}
|
||||
} else {
|
||||
listDatabaseFiles.clear()
|
||||
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
fun setDefaultDatabase(databaseUri: Uri?) {
|
||||
val defaultDatabaseFile = listDatabaseFiles.firstOrNull { it.databaseUri == databaseUri }
|
||||
mDefaultDatabaseFile = defaultDatabaseFile
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun setOnDefaultDatabaseListener(listener: ((DatabaseFile?) -> Unit)?) {
|
||||
this.defaultDatabaseListener = listener
|
||||
}
|
||||
|
||||
fun setOnFileDatabaseHistoryOpenListener(listener : ((DatabaseFile)->Unit)?) {
|
||||
this.fileItemOpenListener = listener
|
||||
}
|
||||
|
||||
fun setOnFileDatabaseHistoryDeleteListener(listener : ((FileDatabaseHistoryEntity)->Boolean)?) {
|
||||
fun setOnFileDatabaseHistoryDeleteListener(listener : ((DatabaseFile)->Boolean)?) {
|
||||
this.fileSelectClearListener = listener
|
||||
}
|
||||
|
||||
fun setOnSaveAliasListener(listener : ((FileDatabaseHistoryEntity)->Unit)?) {
|
||||
fun setOnSaveAliasListener(listener : ((DatabaseFile)->Unit)?) {
|
||||
this.saveAliasListener = listener
|
||||
}
|
||||
|
||||
inner class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
var fileContainer: ViewGroup = itemView.findViewById(R.id.file_container_basic_info)
|
||||
|
||||
var defaultFileButton: CompoundButton = itemView.findViewById(R.id.default_file_button)
|
||||
var fileAlias: TextView = itemView.findViewById(R.id.file_alias)
|
||||
var fileInformation: ImageView = itemView.findViewById(R.id.file_information)
|
||||
var fileInformationButton: ImageView = itemView.findViewById(R.id.file_information_button)
|
||||
|
||||
var fileMainSwitcher: ViewSwitcher = itemView.findViewById(R.id.file_main_switcher)
|
||||
var fileAliasEdit: EditText = itemView.findViewById(R.id.file_alias_edit)
|
||||
@@ -178,7 +250,7 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
||||
var fileModifyButton: ImageView = itemView.findViewById(R.id.file_modify_button)
|
||||
var fileDeleteButton: ImageView = itemView.findViewById(R.id.file_delete_button)
|
||||
var filePath: TextView = itemView.findViewById(R.id.file_path)
|
||||
var filePreciseInfoContainer: ViewGroup = itemView.findViewById(R.id.file_precise_info_container)
|
||||
var fileModificationContainer: ViewGroup = itemView.findViewById(R.id.file_modification_container)
|
||||
var fileModification: TextView = itemView.findViewById(R.id.file_modification)
|
||||
var fileSize: TextView = itemView.findViewById(R.id.file_size)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
|
||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
|
||||
class IconPickerAdapter<I: IconImageDraw>(val context: Context, private val tintIcon: Int)
|
||||
: RecyclerView.Adapter<IconPickerAdapter<I>.CustomIconViewHolder>() {
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
|
||||
private val iconList = ArrayList<I>()
|
||||
|
||||
var iconDrawableFactory: IconDrawableFactory? = null
|
||||
var iconPickerListener: IconPickerListener<I>? = null
|
||||
|
||||
val lastPosition: Int
|
||||
get() = iconList.lastIndex
|
||||
|
||||
fun addIcon(icon: I, notify: Boolean = true) {
|
||||
if (!iconList.contains(icon)) {
|
||||
iconList.add(icon)
|
||||
if (notify) {
|
||||
notifyItemInserted(iconList.indexOf(icon))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateIcon(icon: I) {
|
||||
val index = iconList.indexOf(icon)
|
||||
if (index != -1) {
|
||||
iconList[index] = icon
|
||||
notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateIconSelectedState(icons: List<I>) {
|
||||
icons.forEach { icon ->
|
||||
val index = iconList.indexOf(icon)
|
||||
if (index != -1
|
||||
&& iconList[index].selected != icon.selected) {
|
||||
iconList[index] = icon
|
||||
notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeIcon(icon: I) {
|
||||
if (iconList.contains(icon)) {
|
||||
val position = iconList.indexOf(icon)
|
||||
iconList.remove(icon)
|
||||
notifyItemRemoved(position)
|
||||
}
|
||||
}
|
||||
|
||||
fun containsAnySelectedIcon(): Boolean {
|
||||
return iconList.firstOrNull { it.selected } != null
|
||||
}
|
||||
|
||||
fun deselectAllIcons() {
|
||||
iconList.forEachIndexed { index, icon ->
|
||||
if (icon.selected) {
|
||||
icon.selected = false
|
||||
notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getSelectedIcons(): List<I> {
|
||||
return iconList.filter { it.selected }
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
iconList.clear()
|
||||
}
|
||||
|
||||
fun setList(icons: List<I>) {
|
||||
iconList.clear()
|
||||
icons.forEach { iconImage ->
|
||||
iconList.add(iconImage)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomIconViewHolder {
|
||||
val view = inflater.inflate(R.layout.item_icon, parent, false)
|
||||
return CustomIconViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: CustomIconViewHolder, position: Int) {
|
||||
val icon = iconList[position]
|
||||
iconDrawableFactory?.assignDatabaseIcon(holder.iconImageView, icon, tintIcon)
|
||||
icon.getIconImageToDraw().custom.name.let { iconName ->
|
||||
holder.iconTextView.apply {
|
||||
text = iconName
|
||||
visibility = if (iconName.isNotEmpty()) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
holder.iconContainerView.isSelected = icon.selected
|
||||
holder.itemView.setOnClickListener {
|
||||
iconPickerListener?.onIconClickListener(icon)
|
||||
}
|
||||
holder.itemView.setOnLongClickListener {
|
||||
iconPickerListener?.onIconLongClickListener(icon)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return iconList.size
|
||||
}
|
||||
|
||||
interface IconPickerListener<I: IconImageDraw> {
|
||||
fun onIconClickListener(icon: I)
|
||||
fun onIconLongClickListener(icon: I)
|
||||
}
|
||||
|
||||
inner class CustomIconViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
var iconContainerView: ViewGroup = itemView.findViewById(R.id.icon_container)
|
||||
var iconImageView: ImageView = itemView.findViewById(R.id.icon_image)
|
||||
var iconTextView: TextView = itemView.findViewById(R.id.icon_name)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.kunzisoft.keepass.activities.fragments.IconCustomFragment
|
||||
import com.kunzisoft.keepass.activities.fragments.IconStandardFragment
|
||||
|
||||
class IconPickerPagerAdapter(fragment: Fragment, val size: Int)
|
||||
: FragmentStateAdapter(fragment) {
|
||||
|
||||
private val iconStandardFragment = IconStandardFragment()
|
||||
private val iconCustomFragment = IconCustomFragment()
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return size
|
||||
}
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (position) {
|
||||
1 -> iconCustomFragment
|
||||
else -> iconStandardFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,185 +1,196 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.SortNodeEnum
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.setTextSize
|
||||
import com.kunzisoft.keepass.view.strikeOut
|
||||
import java.util.*
|
||||
|
||||
class NodeAdapter
|
||||
/**
|
||||
* Create node list adapter with contextMenu or not
|
||||
* @param context Context to use
|
||||
*/
|
||||
(private val context: Context)
|
||||
class NodeAdapter (private val context: Context)
|
||||
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
|
||||
|
||||
private val nodeSortedList: SortedList<NodeVersioned>
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
|
||||
private val mNodeSortedListCallback: NodeSortedListCallback
|
||||
private val mNodeSortedList: SortedList<Node>
|
||||
private val mInflater: LayoutInflater = LayoutInflater.from(context)
|
||||
|
||||
private var calculateViewTypeTextSize = Array(2) { true} // number of view type
|
||||
private var textSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
||||
private var prefTextSize: Float = 0F
|
||||
private var subtextSize: Float = 0F
|
||||
private var infoTextSize: Float = 0F
|
||||
private var numberChildrenTextSize: Float = 0F
|
||||
private var iconSize: Float = 0F
|
||||
private var listSort: SortNodeEnum = SortNodeEnum.DB
|
||||
private var ascendingSort: Boolean = true
|
||||
private var groupsBeforeSort: Boolean = true
|
||||
private var recycleBinBottomSort: Boolean = true
|
||||
private var showUserNames: Boolean = true
|
||||
private var showNumberEntries: Boolean = true
|
||||
private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type
|
||||
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
||||
private var mPrefSizeMultiplier: Float = 0F
|
||||
private var mSubtextDefaultDimension: Float = 0F
|
||||
private var mInfoTextDefaultDimension: Float = 0F
|
||||
private var mNumberChildrenTextDefaultDimension: Float = 0F
|
||||
private var mIconDefaultDimension: Float = 0F
|
||||
|
||||
private var actionNodesList = LinkedList<NodeVersioned>()
|
||||
private var nodeClickCallback: NodeClickCallback? = null
|
||||
private var mShowUserNames: Boolean = true
|
||||
private var mShowNumberEntries: Boolean = true
|
||||
private var mEntryFilters = arrayOf<Group.ChildFilter>()
|
||||
|
||||
private var mActionNodesList = LinkedList<Node>()
|
||||
private var mNodeClickCallback: NodeClickCallback? = null
|
||||
|
||||
private val mDatabase: Database
|
||||
|
||||
private val iconGroupColor: Int
|
||||
private val iconEntryColor: Int
|
||||
@ColorInt
|
||||
private val mContentSelectionColor: Int
|
||||
@ColorInt
|
||||
private val mIconGroupColor: Int
|
||||
@ColorInt
|
||||
private val mIconEntryColor: Int
|
||||
|
||||
/**
|
||||
* Determine if the adapter contains or not any element
|
||||
* @return true if the list is empty
|
||||
*/
|
||||
val isEmpty: Boolean
|
||||
get() = nodeSortedList.size() <= 0
|
||||
get() = mNodeSortedList.size() <= 0
|
||||
|
||||
init {
|
||||
this.mIconDefaultDimension = context.resources.getDimension(R.dimen.list_icon_size_default)
|
||||
|
||||
assignPreferences()
|
||||
|
||||
this.nodeSortedList = SortedList(NodeVersioned::class.java, object : SortedListAdapterCallback<NodeVersioned>(this) {
|
||||
override fun compare(item1: NodeVersioned, item2: NodeVersioned): Int {
|
||||
return listSort.getNodeComparator(ascendingSort, groupsBeforeSort, recycleBinBottomSort).compare(item1, item2)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: NodeVersioned, newItem: NodeVersioned): Boolean {
|
||||
return oldItem.type == newItem.type
|
||||
&& oldItem.title == newItem.title
|
||||
&& oldItem.icon == newItem.icon
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(item1: NodeVersioned, item2: NodeVersioned): Boolean {
|
||||
return item1 == item2
|
||||
}
|
||||
})
|
||||
this.mNodeSortedListCallback = NodeSortedListCallback()
|
||||
this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback)
|
||||
|
||||
// Database
|
||||
this.mDatabase = Database.getInstance()
|
||||
|
||||
// Color of content selection
|
||||
this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white)
|
||||
// Retrieve the color to tint the icon
|
||||
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
||||
this.iconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
|
||||
this.mIconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
|
||||
taTextColorPrimary.recycle()
|
||||
// In two times to fix bug compilation
|
||||
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
this.iconEntryColor = taTextColor.getColor(0, Color.BLACK)
|
||||
this.mIconEntryColor = taTextColor.getColor(0, Color.BLACK)
|
||||
taTextColor.recycle()
|
||||
}
|
||||
|
||||
private fun assignPreferences() {
|
||||
this.prefTextSize = PreferencesUtil.getListTextSize(context)
|
||||
this.infoTextSize = context.resources.getDimension(R.dimen.list_medium_size_default) * prefTextSize
|
||||
this.subtextSize = context.resources.getDimension(R.dimen.list_small_size_default) * prefTextSize
|
||||
this.numberChildrenTextSize = context.resources.getDimension(R.dimen.list_tiny_size_default) * prefTextSize
|
||||
this.iconSize = context.resources.getDimension(R.dimen.list_icon_size_default) * prefTextSize
|
||||
fun assignPreferences() {
|
||||
this.mPrefSizeMultiplier = PreferencesUtil.getListTextSize(context)
|
||||
|
||||
this.listSort = PreferencesUtil.getListSort(context)
|
||||
this.ascendingSort = PreferencesUtil.getAscendingSort(context)
|
||||
this.groupsBeforeSort = PreferencesUtil.getGroupsBeforeSort(context)
|
||||
this.recycleBinBottomSort = PreferencesUtil.getRecycleBinBottomSort(context)
|
||||
this.showUserNames = PreferencesUtil.showUsernamesListEntries(context)
|
||||
this.showNumberEntries = PreferencesUtil.showNumberEntries(context)
|
||||
notifyChangeSort(
|
||||
PreferencesUtil.getListSort(context),
|
||||
SortNodeEnum.SortNodeParameters(
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context),
|
||||
PreferencesUtil.getRecycleBinBottomSort(context)
|
||||
)
|
||||
)
|
||||
|
||||
this.mShowUserNames = PreferencesUtil.showUsernamesListEntries(context)
|
||||
this.mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
|
||||
|
||||
this.mEntryFilters = Group.ChildFilter.getDefaults(context)
|
||||
|
||||
// Reinit textSize for all view type
|
||||
calculateViewTypeTextSize.forEachIndexed { index, _ -> calculateViewTypeTextSize[index] = true }
|
||||
mCalculateViewTypeTextSize.forEachIndexed { index, _ -> mCalculateViewTypeTextSize[index] = true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the list by clear and build children from the group
|
||||
*/
|
||||
fun rebuildList(group: GroupVersioned) {
|
||||
this.nodeSortedList.clear()
|
||||
fun rebuildList(group: Group) {
|
||||
assignPreferences()
|
||||
try {
|
||||
this.nodeSortedList.addAll(group.getChildren())
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Can't add node elements to the list", e)
|
||||
Toast.makeText(context, "Can't add node elements to the list : " + e.message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters))
|
||||
}
|
||||
|
||||
fun contains(node: NodeVersioned): Boolean {
|
||||
return nodeSortedList.indexOf(node) != SortedList.INVALID_POSITION
|
||||
private inner class NodeSortedListCallback: SortedListAdapterCallback<Node>(this) {
|
||||
override fun compare(item1: Node, item2: Node): Int {
|
||||
return mNodeComparator!!.compare(item1, item2)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
|
||||
return oldItem.type == newItem.type
|
||||
&& oldItem.title == newItem.title
|
||||
&& oldItem.icon == newItem.icon
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(item1: Node, item2: Node): Boolean {
|
||||
return item1 == item2
|
||||
}
|
||||
}
|
||||
|
||||
fun contains(node: Node): Boolean {
|
||||
return mNodeSortedList.indexOf(node) != SortedList.INVALID_POSITION
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a node to the list
|
||||
* @param node Node to add
|
||||
*/
|
||||
fun addNode(node: NodeVersioned) {
|
||||
nodeSortedList.add(node)
|
||||
fun addNode(node: Node) {
|
||||
mNodeSortedList.add(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add nodes to the list
|
||||
* @param nodes Nodes to add
|
||||
*/
|
||||
fun addNodes(nodes: List<NodeVersioned>) {
|
||||
nodeSortedList.addAll(nodes)
|
||||
fun addNodes(nodes: List<Node>) {
|
||||
mNodeSortedList.addAll(nodes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a node in the list
|
||||
* @param node Node to delete
|
||||
*/
|
||||
fun removeNode(node: NodeVersioned) {
|
||||
nodeSortedList.remove(node)
|
||||
fun removeNode(node: Node) {
|
||||
mNodeSortedList.remove(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove nodes in the list
|
||||
* @param nodes Nodes to delete
|
||||
*/
|
||||
fun removeNodes(nodes: List<NodeVersioned>) {
|
||||
fun removeNodes(nodes: List<Node>) {
|
||||
nodes.forEach { node ->
|
||||
nodeSortedList.remove(node)
|
||||
mNodeSortedList.remove(node)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,9 +198,9 @@ class NodeAdapter
|
||||
* Remove a node at [position] in the list
|
||||
*/
|
||||
fun removeNodeAt(position: Int) {
|
||||
nodeSortedList.removeItemAt(position)
|
||||
mNodeSortedList.removeItemAt(position)
|
||||
// Refresh all the next items
|
||||
notifyItemRangeChanged(position, nodeSortedList.size() - position)
|
||||
notifyItemRangeChanged(position, mNodeSortedList.size() - position)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,11 +220,11 @@ class NodeAdapter
|
||||
* @param oldNode Node before the update
|
||||
* @param newNode Node after the update
|
||||
*/
|
||||
fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned) {
|
||||
nodeSortedList.beginBatchedUpdates()
|
||||
nodeSortedList.remove(oldNode)
|
||||
nodeSortedList.add(newNode)
|
||||
nodeSortedList.endBatchedUpdates()
|
||||
fun updateNode(oldNode: Node, newNode: Node) {
|
||||
mNodeSortedList.beginBatchedUpdates()
|
||||
mNodeSortedList.remove(oldNode)
|
||||
mNodeSortedList.add(newNode)
|
||||
mNodeSortedList.endBatchedUpdates()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -221,31 +232,31 @@ class NodeAdapter
|
||||
* @param oldNodes Nodes before the update
|
||||
* @param newNodes Node after the update
|
||||
*/
|
||||
fun updateNodes(oldNodes: List<NodeVersioned>, newNodes: List<NodeVersioned>) {
|
||||
nodeSortedList.beginBatchedUpdates()
|
||||
fun updateNodes(oldNodes: List<Node>, newNodes: List<Node>) {
|
||||
mNodeSortedList.beginBatchedUpdates()
|
||||
oldNodes.forEach { oldNode ->
|
||||
nodeSortedList.remove(oldNode)
|
||||
mNodeSortedList.remove(oldNode)
|
||||
}
|
||||
nodeSortedList.addAll(newNodes)
|
||||
nodeSortedList.endBatchedUpdates()
|
||||
mNodeSortedList.addAll(newNodes)
|
||||
mNodeSortedList.endBatchedUpdates()
|
||||
}
|
||||
|
||||
fun notifyNodeChanged(node: NodeVersioned) {
|
||||
notifyItemChanged(nodeSortedList.indexOf(node))
|
||||
fun notifyNodeChanged(node: Node) {
|
||||
notifyItemChanged(mNodeSortedList.indexOf(node))
|
||||
}
|
||||
|
||||
fun setActionNodes(actionNodes: List<NodeVersioned>) {
|
||||
this.actionNodesList.apply {
|
||||
fun setActionNodes(actionNodes: List<Node>) {
|
||||
this.mActionNodesList.apply {
|
||||
clear()
|
||||
addAll(actionNodes)
|
||||
}
|
||||
}
|
||||
|
||||
fun unselectActionNodes() {
|
||||
actionNodesList.forEach {
|
||||
notifyItemChanged(nodeSortedList.indexOf(it))
|
||||
mActionNodesList.forEach {
|
||||
notifyItemChanged(mNodeSortedList.indexOf(it))
|
||||
}
|
||||
this.actionNodesList.apply {
|
||||
this.mActionNodesList.apply {
|
||||
clear()
|
||||
}
|
||||
}
|
||||
@@ -253,124 +264,138 @@ class NodeAdapter
|
||||
/**
|
||||
* Notify a change sort of the list
|
||||
*/
|
||||
fun notifyChangeSort(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean) {
|
||||
this.listSort = sortNodeEnum
|
||||
this.ascendingSort = ascending
|
||||
this.groupsBeforeSort = groupsBefore
|
||||
fun notifyChangeSort(sortNodeEnum: SortNodeEnum,
|
||||
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
||||
this.mNodeComparator = sortNodeEnum.getNodeComparator(sortNodeParameters)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return nodeSortedList.get(position).type.ordinal
|
||||
return mNodeSortedList.get(position).type.ordinal
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NodeViewHolder {
|
||||
val view: View = if (viewType == Type.GROUP.ordinal) {
|
||||
inflater.inflate(R.layout.item_list_nodes_group, parent, false)
|
||||
mInflater.inflate(R.layout.item_list_nodes_group, parent, false)
|
||||
} else {
|
||||
inflater.inflate(R.layout.item_list_nodes_entry, parent, false)
|
||||
mInflater.inflate(R.layout.item_list_nodes_entry, parent, false)
|
||||
}
|
||||
return NodeViewHolder(view)
|
||||
val nodeViewHolder = NodeViewHolder(view)
|
||||
mInfoTextDefaultDimension = nodeViewHolder.text.textSize
|
||||
mSubtextDefaultDimension = nodeViewHolder.subText.textSize
|
||||
nodeViewHolder.numberChildren?.let {
|
||||
mNumberChildrenTextDefaultDimension = it.textSize
|
||||
}
|
||||
return nodeViewHolder
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: NodeViewHolder, position: Int) {
|
||||
val subNode = nodeSortedList.get(position)
|
||||
val subNode = mNodeSortedList.get(position)
|
||||
|
||||
// Node selection
|
||||
holder.container.isSelected = mActionNodesList.contains(subNode)
|
||||
|
||||
// Assign image
|
||||
val iconColor = when (subNode.type) {
|
||||
Type.GROUP -> iconGroupColor
|
||||
Type.ENTRY -> iconEntryColor
|
||||
val iconColor = if (holder.container.isSelected)
|
||||
mContentSelectionColor
|
||||
else when (subNode.type) {
|
||||
Type.GROUP -> mIconGroupColor
|
||||
Type.ENTRY -> mIconEntryColor
|
||||
}
|
||||
holder.imageIdentifier?.setColorFilter(iconColor)
|
||||
holder.icon.apply {
|
||||
assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
|
||||
mDatabase.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor)
|
||||
// Relative size of the icon
|
||||
layoutParams?.apply {
|
||||
height = iconSize.toInt()
|
||||
width = iconSize.toInt()
|
||||
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
|
||||
width = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
// Assign text
|
||||
holder.text.apply {
|
||||
text = subNode.title
|
||||
setTextSize(textSizeUnit, infoTextSize)
|
||||
paintFlags = if (subNode.isCurrentlyExpires)
|
||||
paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
|
||||
else
|
||||
paintFlags and Paint.STRIKE_THRU_TEXT_FLAG
|
||||
setTextSize(mTextSizeUnit, mInfoTextDefaultDimension, mPrefSizeMultiplier)
|
||||
strikeOut(subNode.isCurrentlyExpires)
|
||||
}
|
||||
// Assign click
|
||||
holder.container.setOnClickListener {
|
||||
nodeClickCallback?.onNodeClick(subNode)
|
||||
}
|
||||
holder.container.setOnLongClickListener {
|
||||
nodeClickCallback?.onNodeLongClick(subNode) ?: false
|
||||
}
|
||||
|
||||
holder.container.isSelected = actionNodesList.contains(subNode)
|
||||
|
||||
// Add subText with username
|
||||
holder.subText.apply {
|
||||
text = ""
|
||||
paintFlags = if (subNode.isCurrentlyExpires)
|
||||
paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
|
||||
else
|
||||
paintFlags and Paint.STRIKE_THRU_TEXT_FLAG
|
||||
strikeOut(subNode.isCurrentlyExpires)
|
||||
visibility = View.GONE
|
||||
if (subNode.type == Type.ENTRY) {
|
||||
val entry = subNode as EntryVersioned
|
||||
}
|
||||
|
||||
mDatabase.startManageEntry(entry)
|
||||
|
||||
holder.text.text = entry.getVisualTitle()
|
||||
// Specific elements for entry
|
||||
if (subNode.type == Type.ENTRY) {
|
||||
val entry = subNode as Entry
|
||||
mDatabase.startManageEntry(entry)
|
||||
|
||||
holder.text.text = entry.getVisualTitle()
|
||||
holder.subText.apply {
|
||||
val username = entry.username
|
||||
if (showUserNames && username.isNotEmpty()) {
|
||||
if (mShowUserNames && username.isNotEmpty()) {
|
||||
visibility = View.VISIBLE
|
||||
text = username
|
||||
setTextSize(textSizeUnit, subtextSize)
|
||||
setTextSize(mTextSizeUnit, mSubtextDefaultDimension, mPrefSizeMultiplier)
|
||||
}
|
||||
|
||||
mDatabase.stopManageEntry(entry)
|
||||
}
|
||||
|
||||
holder.attachmentIcon?.visibility =
|
||||
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
||||
|
||||
mDatabase.stopManageEntry(entry)
|
||||
}
|
||||
|
||||
// Add number of entries in groups
|
||||
if (subNode.type == Type.GROUP) {
|
||||
if (showNumberEntries) {
|
||||
if (mShowNumberEntries) {
|
||||
holder.numberChildren?.apply {
|
||||
text = (subNode as GroupVersioned).getChildEntries(true).size.toString()
|
||||
setTextSize(textSizeUnit, numberChildrenTextSize)
|
||||
text = (subNode as Group)
|
||||
.getNumberOfChildEntries(mEntryFilters)
|
||||
.toString()
|
||||
setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier)
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
holder.numberChildren?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assign click
|
||||
holder.container.setOnClickListener {
|
||||
mNodeClickCallback?.onNodeClick(subNode)
|
||||
}
|
||||
holder.container.setOnLongClickListener {
|
||||
mNodeClickCallback?.onNodeLongClick(subNode) ?: false
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return nodeSortedList.size()
|
||||
return mNodeSortedList.size()
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a listener when a node is clicked
|
||||
*/
|
||||
fun setOnNodeClickListener(nodeClickCallback: NodeClickCallback?) {
|
||||
this.nodeClickCallback = nodeClickCallback
|
||||
this.mNodeClickCallback = nodeClickCallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback listener to redefine to do an action when a node is click
|
||||
*/
|
||||
interface NodeClickCallback {
|
||||
fun onNodeClick(node: NodeVersioned)
|
||||
fun onNodeLongClick(node: NodeVersioned): Boolean
|
||||
fun onNodeClick(node: Node)
|
||||
fun onNodeLongClick(node: Node): Boolean
|
||||
}
|
||||
|
||||
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
var container: View = itemView.findViewById(R.id.node_container)
|
||||
var imageIdentifier: ImageView? = itemView.findViewById(R.id.node_image_identifier)
|
||||
var icon: ImageView = itemView.findViewById(R.id.node_icon)
|
||||
var text: TextView = itemView.findViewById(R.id.node_text)
|
||||
var subText: TextView = itemView.findViewById(R.id.node_subtext)
|
||||
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
||||
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.adapters
|
||||
@@ -22,27 +22,31 @@ package com.kunzisoft.keepass.adapters
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.graphics.Color
|
||||
import androidx.cursoradapter.widget.CursorAdapter
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.cursor.EntryCursor
|
||||
import com.kunzisoft.keepass.database.cursor.EntryCursorKDB
|
||||
import com.kunzisoft.keepass.database.cursor.EntryCursorKDBX
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.EntryVersioned
|
||||
import com.kunzisoft.keepass.database.element.PwIcon
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.Group
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import java.util.*
|
||||
import com.kunzisoft.keepass.view.strikeOut
|
||||
|
||||
class SearchEntryCursorAdapter(context: Context, private val database: Database)
|
||||
class SearchEntryCursorAdapter(private val context: Context,
|
||||
private val database: Database)
|
||||
: androidx.cursoradapter.widget.CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
|
||||
|
||||
private val cursorInflater: LayoutInflater = context.getSystemService(
|
||||
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
private var displayUsername: Boolean = false
|
||||
private val cursorInflater: LayoutInflater? = context.getSystemService(
|
||||
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
private var mDisplayUsername: Boolean = false
|
||||
private var mOmitBackup: Boolean = true
|
||||
private val iconColor: Int
|
||||
|
||||
init {
|
||||
@@ -55,12 +59,13 @@ class SearchEntryCursorAdapter(context: Context, private val database: Database)
|
||||
}
|
||||
|
||||
fun reInit(context: Context) {
|
||||
this.displayUsername = PreferencesUtil.showUsernamesListEntries(context)
|
||||
this.mDisplayUsername = PreferencesUtil.showUsernamesListEntries(context)
|
||||
this.mOmitBackup = PreferencesUtil.omitBackup(context)
|
||||
}
|
||||
|
||||
override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
|
||||
|
||||
val view = cursorInflater.inflate(R.layout.item_search_entry, parent, false)
|
||||
val view = cursorInflater!!.inflate(R.layout.item_search_entry, parent, false)
|
||||
val viewHolder = ViewHolder()
|
||||
viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon)
|
||||
viewHolder.textViewTitle = view.findViewById(R.id.entry_text)
|
||||
@@ -71,56 +76,105 @@ class SearchEntryCursorAdapter(context: Context, private val database: Database)
|
||||
}
|
||||
|
||||
override fun bindView(view: View, context: Context, cursor: Cursor) {
|
||||
getEntryFrom(cursor)?.let { currentEntry ->
|
||||
val viewHolder = view.tag as ViewHolder
|
||||
|
||||
// Retrieve elements from cursor
|
||||
val uuid = UUID(cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS)),
|
||||
cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS)))
|
||||
val iconFactory = database.iconFactory
|
||||
var icon: PwIcon = iconFactory.getIcon(
|
||||
UUID(cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS)),
|
||||
cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS))))
|
||||
if (icon.isUnknown) {
|
||||
icon = iconFactory.getIcon(cursor.getInt(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_STANDARD)))
|
||||
if (icon.isUnknown)
|
||||
icon = iconFactory.keyIcon
|
||||
}
|
||||
val title = cursor.getString(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_TITLE))
|
||||
val username = cursor.getString(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_USERNAME))
|
||||
val url = cursor.getString(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_URL))
|
||||
// Assign image
|
||||
viewHolder.imageViewIcon?.let { iconView ->
|
||||
database.iconDrawableFactory.assignDatabaseIcon(iconView, currentEntry.icon, iconColor)
|
||||
}
|
||||
|
||||
val viewHolder = view.tag as ViewHolder
|
||||
// Assign title
|
||||
viewHolder.textViewTitle?.apply {
|
||||
text = currentEntry.getVisualTitle()
|
||||
strikeOut(currentEntry.isCurrentlyExpires)
|
||||
}
|
||||
|
||||
// Assign image
|
||||
viewHolder.imageViewIcon?.assignDatabaseIcon(database.drawFactory, icon, iconColor)
|
||||
|
||||
// Assign title
|
||||
val showTitle = EntryVersioned.getVisualTitle(false, title, username, url, uuid.toString())
|
||||
viewHolder.textViewTitle?.text = showTitle
|
||||
if (displayUsername && username.isNotEmpty()) {
|
||||
viewHolder.textViewSubTitle?.text = String.format("(%s)", username)
|
||||
} else {
|
||||
viewHolder.textViewSubTitle?.text = ""
|
||||
// Assign subtitle
|
||||
viewHolder.textViewSubTitle?.apply {
|
||||
val entryUsername = currentEntry.username
|
||||
text = if (mDisplayUsername && entryUsername.isNotEmpty()) {
|
||||
String.format("(%s)", entryUsername)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
visibility = if (text.isEmpty()) View.GONE else View.VISIBLE
|
||||
strikeOut(currentEntry.isCurrentlyExpires)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ViewHolder {
|
||||
internal var imageViewIcon: ImageView? = null
|
||||
internal var textViewTitle: TextView? = null
|
||||
internal var textViewSubTitle: TextView? = null
|
||||
private fun getEntryFrom(cursor: Cursor): Entry? {
|
||||
return database.createEntry()?.apply {
|
||||
entryKDB?.let { entryKDB ->
|
||||
(cursor as EntryCursorKDB).populateEntry(entryKDB,
|
||||
{ standardIconId ->
|
||||
database.getStandardIcon(standardIconId)
|
||||
},
|
||||
{ customIconId ->
|
||||
database.getCustomIcon(customIconId)
|
||||
}
|
||||
)
|
||||
}
|
||||
entryKDBX?.let { entryKDBX ->
|
||||
(cursor as EntryCursorKDBX).populateEntry(entryKDBX,
|
||||
{ standardIconId ->
|
||||
database.getStandardIcon(standardIconId)
|
||||
},
|
||||
{ customIconId ->
|
||||
database.getCustomIcon(customIconId)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
|
||||
return database.searchEntries(constraint.toString())
|
||||
return searchEntries(context, constraint.toString())
|
||||
}
|
||||
|
||||
fun getEntryFromPosition(position: Int): EntryVersioned? {
|
||||
var pwEntry: EntryVersioned? = null
|
||||
private fun searchEntries(context: Context, query: String): Cursor? {
|
||||
var cursorKDB: EntryCursorKDB? = null
|
||||
var cursorKDBX: EntryCursorKDBX? = null
|
||||
|
||||
if (database.type == DatabaseKDB.TYPE)
|
||||
cursorKDB = EntryCursorKDB()
|
||||
if (database.type == DatabaseKDBX.TYPE)
|
||||
cursorKDBX = EntryCursorKDBX()
|
||||
|
||||
val searchGroup = database.createVirtualGroupFromSearch(query,
|
||||
mOmitBackup,
|
||||
SearchHelper.MAX_SEARCH_ENTRY)
|
||||
if (searchGroup != null) {
|
||||
// Search in hide entries but not meta-stream
|
||||
for (entry in searchGroup.getFilteredChildEntries(Group.ChildFilter.getDefaults(context))) {
|
||||
database.startManageEntry(entry)
|
||||
entry.entryKDB?.let {
|
||||
cursorKDB?.addEntry(it)
|
||||
}
|
||||
entry.entryKDBX?.let {
|
||||
cursorKDBX?.addEntry(it)
|
||||
}
|
||||
database.stopManageEntry(entry)
|
||||
}
|
||||
}
|
||||
|
||||
return cursorKDB ?: cursorKDBX
|
||||
}
|
||||
|
||||
fun getEntryFromPosition(position: Int): Entry? {
|
||||
var pwEntry: Entry? = null
|
||||
|
||||
val cursor = this.cursor
|
||||
if (cursor.moveToFirst() && cursor.move(position)) {
|
||||
pwEntry = database.getEntryFrom(cursor)
|
||||
pwEntry = getEntryFrom(cursor)
|
||||
}
|
||||
return pwEntry
|
||||
}
|
||||
|
||||
private class ViewHolder {
|
||||
var imageViewIcon: ImageView? = null
|
||||
var textViewTitle: TextView? = null
|
||||
var textViewSubTitle: TextView? = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* 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.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* 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 KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.app
|
||||
@@ -28,12 +28,12 @@ class App : MultiDexApplication() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
Stylish.init(this)
|
||||
Stylish.load(this)
|
||||
PRNGFixes.apply()
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
||||
Database.getInstance().clearAndClose(this)
|
||||
super.onTerminate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ package com.kunzisoft.keepass.app;
|
||||
import android.os.Build;
|
||||
import android.os.Process;
|
||||
|
||||
import com.kunzisoft.keepass.utils.StringUtil;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
@@ -66,7 +64,7 @@ public final class PRNGFixes {
|
||||
|
||||
private static boolean supportedOnThisDevice() {
|
||||
// Blacklist on samsung devices
|
||||
if (StringUtil.INSTANCE.indexOfIgnoreCase(Build.MANUFACTURER, "samsung", Locale.ENGLISH) >= 0) {
|
||||
if (Build.MANUFACTURER.toLowerCase(Locale.ENGLISH).contains("samsung")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.kunzisoft.keepass.app.database
|
||||
|
||||
import android.os.AsyncTask
|
||||
|
||||
/**
|
||||
* Private class to invoke each method in a separate thread
|
||||
*/
|
||||
class ActionDatabaseAsyncTask<T>(
|
||||
private val action: () -> T ,
|
||||
private val afterActionDatabaseListener: ((T?) -> Unit)? = null
|
||||
) : AsyncTask<Void, Void, T>() {
|
||||
|
||||
override fun doInBackground(vararg args: Void?): T? {
|
||||
return action.invoke()
|
||||
}
|
||||
|
||||
override fun onPostExecute(result: T?) {
|
||||
afterActionDatabaseListener?.invoke(result)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.app.database
|
||||
|
||||
import androidx.room.Database
|
||||
|
||||
@@ -1,26 +1,145 @@
|
||||
/*
|
||||
* 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.app.database
|
||||
|
||||
import android.content.Context
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
||||
import java.util.*
|
||||
|
||||
class CipherDatabaseAction(applicationContext: Context) {
|
||||
class CipherDatabaseAction(context: Context) {
|
||||
|
||||
private val applicationContext = context.applicationContext
|
||||
private val cipherDatabaseDao =
|
||||
AppDatabase
|
||||
.getDatabase(applicationContext)
|
||||
.cipherDatabaseDao()
|
||||
|
||||
// Temp DAO to easily remove content if object no longer in memory
|
||||
private var useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
|
||||
|
||||
private var mBinder: AdvancedUnlockNotificationService.AdvancedUnlockBinder? = null
|
||||
private var mServiceConnection: ServiceConnection? = null
|
||||
|
||||
private var mDatabaseListeners = LinkedList<CipherDatabaseListener>()
|
||||
private var mAdvancedUnlockBroadcastReceiver = AdvancedUnlockNotificationService.AdvancedUnlockReceiver {
|
||||
deleteAll()
|
||||
removeAllDataAndDetach()
|
||||
}
|
||||
|
||||
fun reloadPreferences() {
|
||||
useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun serviceActionTask(startService: Boolean = false, performedAction: () -> Unit) {
|
||||
// Check if a service is currently running else call action without info
|
||||
if (startService && mServiceConnection == null) {
|
||||
attachService(performedAction)
|
||||
} else {
|
||||
performedAction.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun attachService(performedAction: () -> Unit) {
|
||||
applicationContext.registerReceiver(mAdvancedUnlockBroadcastReceiver, IntentFilter().apply {
|
||||
addAction(AdvancedUnlockNotificationService.REMOVE_ADVANCED_UNLOCK_KEY_ACTION)
|
||||
})
|
||||
|
||||
mServiceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||
mBinder = (serviceBinder as AdvancedUnlockNotificationService.AdvancedUnlockBinder)
|
||||
performedAction.invoke()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
onClear()
|
||||
}
|
||||
}
|
||||
try {
|
||||
AdvancedUnlockNotificationService.bindService(applicationContext,
|
||||
mServiceConnection!!,
|
||||
Context.BIND_AUTO_CREATE)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to start cipher action", e)
|
||||
performedAction.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun detachService() {
|
||||
try {
|
||||
applicationContext.unregisterReceiver(mAdvancedUnlockBroadcastReceiver)
|
||||
} catch (e: Exception) {}
|
||||
|
||||
mServiceConnection?.let {
|
||||
AdvancedUnlockNotificationService.unbindService(applicationContext, it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeAllDataAndDetach() {
|
||||
detachService()
|
||||
onClear()
|
||||
}
|
||||
|
||||
fun registerDatabaseListener(listenerCipher: CipherDatabaseListener) {
|
||||
mDatabaseListeners.add(listenerCipher)
|
||||
}
|
||||
|
||||
fun unregisterDatabaseListener(listenerCipher: CipherDatabaseListener) {
|
||||
mDatabaseListeners.remove(listenerCipher)
|
||||
}
|
||||
|
||||
private fun onClear() {
|
||||
mBinder = null
|
||||
mServiceConnection = null
|
||||
mDatabaseListeners.forEach {
|
||||
it.onCipherDatabaseCleared()
|
||||
}
|
||||
}
|
||||
|
||||
interface CipherDatabaseListener {
|
||||
fun onCipherDatabaseCleared()
|
||||
}
|
||||
|
||||
fun getCipherDatabase(databaseUri: Uri,
|
||||
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
|
||||
ActionDatabaseAsyncTask(
|
||||
{
|
||||
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
|
||||
},
|
||||
{
|
||||
cipherDatabaseResultListener.invoke(it)
|
||||
}
|
||||
).execute()
|
||||
if (useTempDao) {
|
||||
serviceActionTask {
|
||||
cipherDatabaseResultListener.invoke(mBinder?.getCipherDatabase(databaseUri))
|
||||
}
|
||||
} else {
|
||||
IOActionTask(
|
||||
{
|
||||
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
|
||||
},
|
||||
{
|
||||
cipherDatabaseResultListener.invoke(it)
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
}
|
||||
|
||||
fun containsCipherDatabase(databaseUri: Uri,
|
||||
@@ -32,42 +151,66 @@ class CipherDatabaseAction(applicationContext: Context) {
|
||||
|
||||
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
|
||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||
ActionDatabaseAsyncTask(
|
||||
{
|
||||
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
|
||||
|
||||
// Update values if element not yet in the database
|
||||
if (cipherDatabaseRetrieve == null) {
|
||||
cipherDatabaseDao.add(cipherDatabaseEntity)
|
||||
} else {
|
||||
cipherDatabaseDao.update(cipherDatabaseEntity)
|
||||
if (useTempDao) {
|
||||
// The only case to create service (not needed to get an info)
|
||||
serviceActionTask(true) {
|
||||
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
|
||||
cipherDatabaseResultListener?.invoke()
|
||||
}
|
||||
} else {
|
||||
IOActionTask(
|
||||
{
|
||||
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
|
||||
// Update values if element not yet in the database
|
||||
if (cipherDatabaseRetrieve == null) {
|
||||
cipherDatabaseDao.add(cipherDatabaseEntity)
|
||||
} else {
|
||||
cipherDatabaseDao.update(cipherDatabaseEntity)
|
||||
}
|
||||
},
|
||||
{
|
||||
cipherDatabaseResultListener?.invoke()
|
||||
}
|
||||
},
|
||||
{
|
||||
cipherDatabaseResultListener?.invoke()
|
||||
}
|
||||
).execute()
|
||||
).execute()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteByDatabaseUri(databaseUri: Uri,
|
||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||
ActionDatabaseAsyncTask(
|
||||
{
|
||||
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
|
||||
},
|
||||
{
|
||||
cipherDatabaseResultListener?.invoke()
|
||||
}
|
||||
).execute()
|
||||
if (useTempDao) {
|
||||
serviceActionTask {
|
||||
mBinder?.deleteByDatabaseUri(databaseUri)
|
||||
cipherDatabaseResultListener?.invoke()
|
||||
}
|
||||
} else {
|
||||
IOActionTask(
|
||||
{
|
||||
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
|
||||
},
|
||||
{
|
||||
cipherDatabaseResultListener?.invoke()
|
||||
}
|
||||
).execute()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAll() {
|
||||
ActionDatabaseAsyncTask(
|
||||
if (useTempDao) {
|
||||
serviceActionTask {
|
||||
mBinder?.deleteAll()
|
||||
}
|
||||
}
|
||||
// To erase the residues
|
||||
IOActionTask(
|
||||
{
|
||||
cipherDatabaseDao.deleteAll()
|
||||
}
|
||||
).execute()
|
||||
// Unbind
|
||||
removeAllDataAndDetach()
|
||||
}
|
||||
|
||||
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction)
|
||||
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction) {
|
||||
private val TAG = CipherDatabaseAction::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* 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.app.database
|
||||
|
||||
import androidx.room.*
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user