mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
570 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
45da17adb8 | ||
|
|
58d10672ea |
55
CHANGELOG
55
CHANGELOG
@@ -1,3 +1,58 @@
|
|||||||
|
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)
|
KeePassDX(2.9.9)
|
||||||
* Detect file changes and reload database #794
|
* Detect file changes and reload database #794
|
||||||
* Inline suggestions autofill with compatible keyboard (Android R) #827
|
* Inline suggestions autofill with compatible keyboard (Android R) #827
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 30
|
compileSdkVersion 30
|
||||||
buildToolsVersion '30.0.3'
|
buildToolsVersion "30.0.3"
|
||||||
ndkVersion '21.3.6528147'
|
ndkVersion "21.4.7075529"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 14
|
minSdkVersion 15
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode = 53
|
versionCode = 72
|
||||||
versionName = "2.9.9"
|
versionName = "2.9.18"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
testInstrumentationRunner = "android.test.InstrumentationTestRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
|
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
|
||||||
manifestPlaceholders = [ googleAndroidBackupAPIKey:"unused" ]
|
manifestPlaceholders = [ googleAndroidBackupAPIKey:"unused" ]
|
||||||
@@ -30,12 +29,6 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
path "src/main/jni/CMakeLists.txt"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled = false
|
minifyEnabled = false
|
||||||
@@ -51,7 +44,11 @@ android {
|
|||||||
buildConfigField "String", "BUILD_VERSION", "\"libre\""
|
buildConfigField "String", "BUILD_VERSION", "\"libre\""
|
||||||
buildConfigField "boolean", "FULL_VERSION", "true"
|
buildConfigField "boolean", "FULL_VERSION", "true"
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "false"
|
buildConfigField "boolean", "CLOSED_STORE", "false"
|
||||||
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
buildConfigField "String[]", "STYLES_DISABLED",
|
||||||
|
"{\"KeepassDXStyle_Red\"," +
|
||||||
|
"\"KeepassDXStyle_Red_Night\"," +
|
||||||
|
"\"KeepassDXStyle_Purple\"," +
|
||||||
|
"\"KeepassDXStyle_Purple_Dark\"}"
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||||
}
|
}
|
||||||
pro {
|
pro {
|
||||||
@@ -70,7 +67,13 @@ android {
|
|||||||
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
||||||
buildConfigField "boolean", "FULL_VERSION", "false"
|
buildConfigField "boolean", "FULL_VERSION", "false"
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "true"
|
buildConfigField "boolean", "CLOSED_STORE", "true"
|
||||||
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_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", "{}"
|
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||||
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
|
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
|
||||||
}
|
}
|
||||||
@@ -82,6 +85,10 @@ android {
|
|||||||
free.res.srcDir 'src/free/res'
|
free.res.srcDir 'src/free/res'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests.includeAndroidResources = true
|
||||||
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
@@ -100,34 +107,35 @@ dependencies {
|
|||||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'androidx.biometric:biometric:1.1.0-rc01'
|
implementation 'androidx.biometric:biometric:1.1.0'
|
||||||
// Lifecycle - LiveData - ViewModel - Coroutines
|
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||||
implementation "androidx.core:core-ktx:1.3.2"
|
implementation "androidx.core:core-ktx:1.3.2"
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||||
// WARNING: To upgrade with style, bug in edit text
|
// WARNING: Don't upgrade because slowdown https://github.com/Kunzisoft/KeePassDX/issues/923
|
||||||
implementation 'com.google.android.material:material:1.0.0'
|
implementation 'com.google.android.material:material:1.1.0'
|
||||||
// Database
|
// Database
|
||||||
implementation "androidx.room:room-runtime:$room_version"
|
implementation "androidx.room:room-runtime:$room_version"
|
||||||
kapt "androidx.room:room-compiler:$room_version"
|
kapt "androidx.room:room-compiler:$room_version"
|
||||||
// Autofill
|
// Autofill
|
||||||
implementation "androidx.autofill:autofill:1.1.0-rc01"
|
implementation "androidx.autofill:autofill:1.1.0"
|
||||||
// Crypto
|
|
||||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01'
|
|
||||||
// Time
|
// Time
|
||||||
implementation 'joda-time:joda-time:2.10.6'
|
implementation 'joda-time:joda-time:2.10.6'
|
||||||
// Color
|
// Color
|
||||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4'
|
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4'
|
||||||
// Education
|
// Education
|
||||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
|
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
|
||||||
// Apache Commons Collections
|
// Apache Commons
|
||||||
implementation 'commons-collections:commons-collections:3.2.2'
|
implementation 'commons-io:commons-io:2.8.0'
|
||||||
// Apache Commons Codec
|
|
||||||
implementation 'commons-codec:commons-codec:1.15'
|
implementation 'commons-codec:commons-codec:1.15'
|
||||||
|
// Encrypt lib
|
||||||
|
implementation project(path: ':crypto')
|
||||||
// Icon pack
|
// Icon pack
|
||||||
implementation project(path: ':icon-pack-classic')
|
implementation project(path: ':icon-pack-classic')
|
||||||
implementation project(path: ':icon-pack-material')
|
implementation project(path: ':icon-pack-material')
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
androidTestImplementation 'junit:junit:4.13'
|
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||||
|
androidTestImplementation 'androidx.test:rules:1.3.0'
|
||||||
}
|
}
|
||||||
|
|||||||
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,67 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.tests.crypto
|
|
||||||
|
|
||||||
import org.junit.Assert.assertArrayEquals
|
|
||||||
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.Random
|
|
||||||
|
|
||||||
import junit.framework.TestCase
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.finalkey.AndroidAESKeyTransformer
|
|
||||||
import com.kunzisoft.keepass.crypto.finalkey.NativeAESKeyTransformer
|
|
||||||
|
|
||||||
class AESKeyTest : TestCase() {
|
|
||||||
private lateinit var mRand: Random
|
|
||||||
|
|
||||||
@Throws(Exception::class)
|
|
||||||
override fun setUp() {
|
|
||||||
super.setUp()
|
|
||||||
|
|
||||||
mRand = Random()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun testAES() {
|
|
||||||
// Test both an old and an even number to test my flip variable
|
|
||||||
testAESFinalKey(5)
|
|
||||||
testAESFinalKey(6)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun testAESFinalKey(rounds: Long) {
|
|
||||||
val seed = ByteArray(32)
|
|
||||||
val key = ByteArray(32)
|
|
||||||
val nativeKey: ByteArray?
|
|
||||||
val androidKey: ByteArray?
|
|
||||||
|
|
||||||
mRand.nextBytes(seed)
|
|
||||||
mRand.nextBytes(key)
|
|
||||||
|
|
||||||
val androidAESKey = AndroidAESKeyTransformer()
|
|
||||||
androidKey = androidAESKey.transformMasterKey(seed, key, rounds)
|
|
||||||
|
|
||||||
val nativeAESKey = NativeAESKeyTransformer()
|
|
||||||
nativeKey = nativeAESKey.transformMasterKey(seed, key, rounds)
|
|
||||||
|
|
||||||
assertArrayEquals("Does not match", androidKey, nativeKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.tests.crypto
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
|
||||||
|
|
||||||
import junit.framework.TestCase
|
|
||||||
|
|
||||||
import java.security.InvalidAlgorithmParameterException
|
|
||||||
import java.security.InvalidKeyException
|
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.util.Random
|
|
||||||
|
|
||||||
import javax.crypto.BadPaddingException
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.IllegalBlockSizeException
|
|
||||||
import javax.crypto.NoSuchPaddingException
|
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
import org.junit.Assert.assertArrayEquals
|
|
||||||
|
|
||||||
class AESTest : TestCase() {
|
|
||||||
|
|
||||||
private val mRand = Random()
|
|
||||||
|
|
||||||
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, IllegalBlockSizeException::class, BadPaddingException::class, InvalidAlgorithmParameterException::class)
|
|
||||||
fun testEncrypt() {
|
|
||||||
// Test above below and at the blocksize
|
|
||||||
testFinal(15)
|
|
||||||
testFinal(16)
|
|
||||||
testFinal(17)
|
|
||||||
|
|
||||||
// Test random larger sizes
|
|
||||||
val size = mRand.nextInt(494) + 18
|
|
||||||
testFinal(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, IllegalBlockSizeException::class, BadPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
|
||||||
private fun testFinal(dataSize: Int) {
|
|
||||||
|
|
||||||
// Generate some input
|
|
||||||
val input = ByteArray(dataSize)
|
|
||||||
mRand.nextBytes(input)
|
|
||||||
|
|
||||||
// Generate key
|
|
||||||
val keyArray = ByteArray(32)
|
|
||||||
mRand.nextBytes(keyArray)
|
|
||||||
val key = SecretKeySpec(keyArray, "AES")
|
|
||||||
|
|
||||||
// Generate IV
|
|
||||||
val ivArray = ByteArray(16)
|
|
||||||
mRand.nextBytes(ivArray)
|
|
||||||
val iv = IvParameterSpec(ivArray)
|
|
||||||
|
|
||||||
val android = CipherFactory.getInstance("AES/CBC/PKCS5Padding", true)
|
|
||||||
android.init(Cipher.ENCRYPT_MODE, key, iv)
|
|
||||||
val outAndroid = android.doFinal(input, 0, dataSize)
|
|
||||||
|
|
||||||
val nat = CipherFactory.getInstance("AES/CBC/PKCS5Padding")
|
|
||||||
nat.init(Cipher.ENCRYPT_MODE, key, iv)
|
|
||||||
val outNative = nat.doFinal(input, 0, dataSize)
|
|
||||||
|
|
||||||
assertArrayEquals("Arrays differ on size: $dataSize", outAndroid, outNative)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePassDX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
@@ -19,44 +19,32 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests.crypto
|
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.Assert.assertArrayEquals
|
||||||
|
import org.junit.Test
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.IOException
|
import java.util.*
|
||||||
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.Cipher
|
||||||
|
import javax.crypto.CipherInputStream
|
||||||
import javax.crypto.CipherOutputStream
|
import javax.crypto.CipherOutputStream
|
||||||
import javax.crypto.IllegalBlockSizeException
|
|
||||||
import javax.crypto.NoSuchPaddingException
|
|
||||||
|
|
||||||
import junit.framework.TestCase
|
class EncryptionTest {
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
|
||||||
import com.kunzisoft.keepass.crypto.engine.AesEngine
|
|
||||||
import com.kunzisoft.keepass.stream.BetterCipherInputStream
|
|
||||||
import com.kunzisoft.keepass.stream.LittleEndianDataInputStream
|
|
||||||
|
|
||||||
class CipherTest : TestCase() {
|
|
||||||
private val rand = Random()
|
private val rand = Random()
|
||||||
|
|
||||||
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidAlgorithmParameterException::class, IllegalBlockSizeException::class, BadPaddingException::class)
|
@Test
|
||||||
fun testCipherFactory() {
|
fun testCipherFactory() {
|
||||||
val key = ByteArray(32)
|
val key = ByteArray(32)
|
||||||
|
rand.nextBytes(key)
|
||||||
|
|
||||||
val iv = ByteArray(16)
|
val iv = ByteArray(16)
|
||||||
|
rand.nextBytes(iv)
|
||||||
|
|
||||||
val plaintext = ByteArray(1024)
|
val plaintext = ByteArray(1024)
|
||||||
|
|
||||||
rand.nextBytes(key)
|
|
||||||
rand.nextBytes(iv)
|
|
||||||
rand.nextBytes(plaintext)
|
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 encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv)
|
||||||
val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv)
|
val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv)
|
||||||
|
|
||||||
@@ -66,20 +54,20 @@ class CipherTest : TestCase() {
|
|||||||
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext)
|
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() {
|
fun testCipherStreams() {
|
||||||
val MESSAGE_LENGTH = 1024
|
val length = 1024
|
||||||
|
|
||||||
val key = ByteArray(32)
|
val key = ByteArray(32)
|
||||||
val iv = ByteArray(16)
|
|
||||||
|
|
||||||
val plaintext = ByteArray(MESSAGE_LENGTH)
|
|
||||||
|
|
||||||
rand.nextBytes(key)
|
rand.nextBytes(key)
|
||||||
|
|
||||||
|
val iv = ByteArray(16)
|
||||||
rand.nextBytes(iv)
|
rand.nextBytes(iv)
|
||||||
|
|
||||||
|
val plaintext = ByteArray(length)
|
||||||
rand.nextBytes(plaintext)
|
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 encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv)
|
||||||
val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv)
|
val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv)
|
||||||
|
|
||||||
@@ -91,10 +79,9 @@ class CipherTest : TestCase() {
|
|||||||
val secrettext = bos.toByteArray()
|
val secrettext = bos.toByteArray()
|
||||||
|
|
||||||
val bis = ByteArrayInputStream(secrettext)
|
val bis = ByteArrayInputStream(secrettext)
|
||||||
val cis = BetterCipherInputStream(bis, decrypt)
|
val cis = CipherInputStream(bis, decrypt)
|
||||||
val lis = LittleEndianDataInputStream(cis)
|
|
||||||
|
|
||||||
val decrypttext = lis.readBytes(MESSAGE_LENGTH)
|
val decrypttext = cis.readBytesLength(length)
|
||||||
|
|
||||||
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext)
|
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext)
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,9 +20,7 @@
|
|||||||
package com.kunzisoft.keepass.tests.utils
|
package com.kunzisoft.keepass.tests.utils
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.stream.*
|
import com.kunzisoft.keepass.utils.*
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
|
||||||
import com.kunzisoft.keepass.utils.UnsignedLong
|
|
||||||
import junit.framework.TestCase
|
import junit.framework.TestCase
|
||||||
import org.junit.Assert.assertArrayEquals
|
import org.junit.Assert.assertArrayEquals
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
@@ -54,7 +52,7 @@ class ValuesTest : TestCase() {
|
|||||||
val orig = ByteArray(8)
|
val orig = ByteArray(8)
|
||||||
setArray(orig, value, 8)
|
setArray(orig, value, 8)
|
||||||
|
|
||||||
assertArrayEquals(orig, longTo8Bytes(bytes64ToLong(orig)))
|
assertArrayEquals(orig, uLongTo8Bytes(bytes64ToULong(orig)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteIntZero() {
|
fun testReadWriteIntZero() {
|
||||||
@@ -133,7 +131,7 @@ class ValuesTest : TestCase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun testReadWriteByte(value: Byte) {
|
private fun testReadWriteByte(value: Byte) {
|
||||||
val dest: Byte = UnsignedInt(UnsignedInt.fromKotlinByte(value)).toKotlinByte()
|
val dest: Byte = UnsignedInt(value.toInt() and 0xFF).toKotlinByte()
|
||||||
assert(value == dest)
|
assert(value == dest)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,13 +142,11 @@ class ValuesTest : TestCase() {
|
|||||||
expected.set(2008, 1, 2, 3, 4, 5)
|
expected.set(2008, 1, 2, 3, 4, 5)
|
||||||
|
|
||||||
val actual = Calendar.getInstance()
|
val actual = Calendar.getInstance()
|
||||||
dateTo5Bytes(expected.time, cal)?.let { buf ->
|
actual.time = DateInstant(bytes5ToDate(dateTo5Bytes(expected.time, cal), cal)).date
|
||||||
actual.time = bytes5ToDate(buf, cal).date
|
|
||||||
}
|
|
||||||
|
|
||||||
val jDate = DateInstant(System.currentTimeMillis())
|
val jDate = DateInstant(System.currentTimeMillis())
|
||||||
val intermediate = DateInstant(jDate)
|
val intermediate = DateInstant(jDate)
|
||||||
val cDate = bytes5ToDate(dateTo5Bytes(intermediate.date)!!)
|
val cDate = DateInstant(bytes5ToDate(dateTo5Bytes(intermediate.date)))
|
||||||
|
|
||||||
assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR))
|
assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR))
|
||||||
assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH))
|
assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH))
|
||||||
@@ -183,12 +179,10 @@ class ValuesTest : TestCase() {
|
|||||||
ulongBytes[i] = -1
|
ulongBytes[i] = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
val bos = ByteArrayOutputStream()
|
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||||
val leos = LittleEndianDataOutputStream(bos)
|
byteArrayOutputStream.write(UnsignedLong.MAX_BYTES)
|
||||||
leos.writeLong(UnsignedLong.MAX_VALUE)
|
byteArrayOutputStream.close()
|
||||||
leos.close()
|
val uLongMax = byteArrayOutputStream.toByteArray()
|
||||||
|
|
||||||
val uLongMax = bos.toByteArray()
|
|
||||||
|
|
||||||
assertArrayEquals(ulongBytes, uLongMax)
|
assertArrayEquals(ulongBytes, uLongMax)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,9 +129,15 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.EntryActivity"
|
android:name="com.kunzisoft.keepass.activities.EntryActivity"
|
||||||
android:configChanges="keyboardHidden" />
|
android:configChanges="keyboardHidden" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.IconPickerActivity"
|
||||||
|
android:configChanges="keyboardHidden" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.ImageViewerActivity"
|
||||||
|
android:configChanges="keyboardHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
|
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
|
||||||
android:windowSoftInputMode="adjustPan|stateAlwaysHidden" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
<!-- About and Settings -->
|
<!-- About and Settings -->
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
||||||
@@ -175,19 +181,19 @@
|
|||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService"
|
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.AttachmentFileNotificationService"
|
android:name="com.kunzisoft.keepass.services.AttachmentFileNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService"
|
android:name="com.kunzisoft.keepass.services.ClipboardEntryNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService"
|
android:name="com.kunzisoft.keepass.services.AdvancedUnlockNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<!-- Receiver for Autofill -->
|
<!-- Receiver for Autofill -->
|
||||||
@@ -213,7 +219,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService"
|
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
|||||||
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
@@ -39,6 +39,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|||||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
||||||
@@ -47,26 +48,22 @@ import com.kunzisoft.keepass.database.element.Database
|
|||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.StreamDirection
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
|
|
||||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
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.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.*
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
|
||||||
import com.kunzisoft.keepass.utils.createDocument
|
|
||||||
import com.kunzisoft.keepass.utils.onCreateDocumentResult
|
|
||||||
import com.kunzisoft.keepass.view.EntryContentsView
|
import com.kunzisoft.keepass.view.EntryContentsView
|
||||||
import com.kunzisoft.keepass.view.showActionError
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.HashMap
|
import kotlin.collections.HashMap
|
||||||
|
|
||||||
@@ -97,6 +94,8 @@ class EntryActivity : LockingActivity() {
|
|||||||
private var clipboardHelper: ClipboardHelper? = null
|
private var clipboardHelper: ClipboardHelper? = null
|
||||||
private var mFirstLaunchOfActivity: Boolean = false
|
private var mFirstLaunchOfActivity: Boolean = false
|
||||||
|
|
||||||
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var iconColor: Int = 0
|
private var iconColor: Int = 0
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -129,6 +128,7 @@ class EntryActivity : LockingActivity() {
|
|||||||
historyView = findViewById(R.id.history_container)
|
historyView = findViewById(R.id.history_container)
|
||||||
entryContentsView = findViewById(R.id.entry_contents)
|
entryContentsView = findViewById(R.id.entry_contents)
|
||||||
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
||||||
|
entryContentsView?.setAttachmentCipherKey(mDatabase)
|
||||||
entryProgress = findViewById(R.id.entry_progress)
|
entryProgress = findViewById(R.id.entry_progress)
|
||||||
lockView = findViewById(R.id.lock_button)
|
lockView = findViewById(R.id.lock_button)
|
||||||
|
|
||||||
@@ -143,6 +143,9 @@ class EntryActivity : LockingActivity() {
|
|||||||
clipboardHelper = ClipboardHelper(this)
|
clipboardHelper = ClipboardHelper(this)
|
||||||
mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true
|
mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true
|
||||||
|
|
||||||
|
// Init SAF manager
|
||||||
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
|
||||||
// Init attachment service binder manager
|
// Init attachment service binder manager
|
||||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||||
|
|
||||||
@@ -156,10 +159,11 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
ACTION_DATABASE_RELOAD_TASK -> {
|
ACTION_DATABASE_RELOAD_TASK -> {
|
||||||
// Close the current activity
|
// Close the current activity
|
||||||
|
this.showActionErrorIfNeeded(result)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
coordinatorLayout?.showActionError(result)
|
coordinatorLayout?.showActionErrorIfNeeded(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +226,9 @@ class EntryActivity : LockingActivity() {
|
|||||||
registerProgressTask()
|
registerProgressTask()
|
||||||
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
||||||
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
||||||
entryContentsView?.putAttachment(entryAttachmentState)
|
if (entryAttachmentState.streamDirection != StreamDirection.UPLOAD) {
|
||||||
|
entryContentsView?.putAttachment(entryAttachmentState)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,7 +247,9 @@ class EntryActivity : LockingActivity() {
|
|||||||
val entryInfo = entry.getEntryInfo(mDatabase)
|
val entryInfo = entry.getEntryInfo(mDatabase)
|
||||||
|
|
||||||
// Assign title icon
|
// Assign title icon
|
||||||
titleIconView?.assignDatabaseIcon(mDatabase!!.drawFactory, entryInfo.icon, iconColor)
|
titleIconView?.let { iconView ->
|
||||||
|
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, iconColor)
|
||||||
|
}
|
||||||
|
|
||||||
// Assign title text
|
// Assign title text
|
||||||
val entryTitle = entryInfo.title
|
val entryTitle = entryInfo.title
|
||||||
@@ -342,14 +350,14 @@ class EntryActivity : LockingActivity() {
|
|||||||
|
|
||||||
// Manage attachments
|
// Manage attachments
|
||||||
entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
|
entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
|
||||||
createDocument(this, attachmentItem.name)?.let { requestCode ->
|
mExternalFileHelper?.createDocument(attachmentItem.name)?.let { requestCode ->
|
||||||
mAttachmentsToDownload[requestCode] = attachmentItem
|
mAttachmentsToDownload[requestCode] = attachmentItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign dates
|
// Assign dates
|
||||||
entryContentsView?.assignCreationDate(entryInfo.creationTime)
|
entryContentsView?.assignCreationDate(entryInfo.creationTime)
|
||||||
entryContentsView?.assignModificationDate(entryInfo.modificationTime)
|
entryContentsView?.assignModificationDate(entryInfo.lastModificationTime)
|
||||||
entryContentsView?.setExpires(entryInfo.expires, entryInfo.expiryTime)
|
entryContentsView?.setExpires(entryInfo.expires, entryInfo.expiryTime)
|
||||||
|
|
||||||
// Manage history
|
// Manage history
|
||||||
@@ -378,7 +386,7 @@ class EntryActivity : LockingActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
||||||
if (createdFileUri != null) {
|
if (createdFileUri != null) {
|
||||||
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
|
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
|
||||||
mAttachmentFileBinderManager
|
mAttachmentFileBinderManager
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import android.widget.DatePicker
|
|||||||
import android.widget.TimePicker
|
import android.widget.TimePicker
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.widget.Toolbar
|
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.widget.NestedScrollView
|
import androidx.core.widget.NestedScrollView
|
||||||
@@ -44,8 +43,9 @@ import com.google.android.material.snackbar.Snackbar
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.*
|
import com.kunzisoft.keepass.activities.dialogs.*
|
||||||
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE
|
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE
|
||||||
|
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
@@ -57,29 +57,28 @@ import com.kunzisoft.keepass.database.element.node.Node
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||||
import com.kunzisoft.keepass.model.*
|
import com.kunzisoft.keepass.model.*
|
||||||
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
|
|
||||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
|
||||||
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
|
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
||||||
|
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
|
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.view.ToolbarAction
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
import com.kunzisoft.keepass.view.showActionError
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
class EntryEditActivity : LockingActivity(),
|
class EntryEditActivity : LockingActivity(),
|
||||||
IconPickerDialogFragment.IconPickerListener,
|
|
||||||
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
|
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
|
||||||
GeneratePasswordDialogFragment.GeneratePasswordListener,
|
GeneratePasswordDialogFragment.GeneratePasswordListener,
|
||||||
SetOTPDialogFragment.CreateOtpListener,
|
SetOTPDialogFragment.CreateOtpListener,
|
||||||
@@ -99,15 +98,15 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var scrollView: NestedScrollView? = null
|
private var scrollView: NestedScrollView? = null
|
||||||
private var entryEditFragment: EntryEditFragment? = null
|
private var entryEditFragment: EntryEditFragment? = null
|
||||||
private var entryEditAddToolBar: Toolbar? = null
|
private var entryEditAddToolBar: ToolbarAction? = null
|
||||||
private var validateButton: View? = null
|
private var validateButton: View? = null
|
||||||
private var lockView: View? = null
|
private var lockView: View? = null
|
||||||
|
|
||||||
// To manage attachments
|
// To manage attachments
|
||||||
private var mSelectFileHelper: SelectFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||||
private var mAllowMultipleAttachments: Boolean = false
|
private var mAllowMultipleAttachments: Boolean = false
|
||||||
private var mTempAttachments = ArrayList<Attachment>()
|
private var mTempAttachments = ArrayList<EntryAttachmentState>()
|
||||||
|
|
||||||
// Education
|
// Education
|
||||||
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
||||||
@@ -119,11 +118,12 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_entry_edit)
|
setContentView(R.layout.activity_entry_edit)
|
||||||
|
|
||||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
// Bottom Bar
|
||||||
setSupportActionBar(toolbar)
|
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
|
||||||
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp)
|
setSupportActionBar(entryEditAddToolBar)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowTitleEnabled(false)
|
||||||
|
|
||||||
coordinatorLayout = findViewById(R.id.entry_edit_coordinator_layout)
|
coordinatorLayout = findViewById(R.id.entry_edit_coordinator_layout)
|
||||||
|
|
||||||
@@ -172,10 +172,14 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
val parentIcon = mParent?.icon
|
val parentIcon = mParent?.icon
|
||||||
tempEntryInfo = mDatabase?.createEntry()?.getEntryInfo(mDatabase, true)
|
tempEntryInfo = mDatabase?.createEntry()?.getEntryInfo(mDatabase, true)
|
||||||
// Set default icon
|
// Set default icon
|
||||||
if (parentIcon != null
|
if (parentIcon != null) {
|
||||||
&& parentIcon.iconId != IconImage.UNKNOWN_ID
|
if (parentIcon.custom.isUnknown
|
||||||
&& parentIcon.iconId != IconImageStandard.FOLDER) {
|
&& parentIcon.standard.id != IconImageStandard.FOLDER_ID) {
|
||||||
tempEntryInfo?.icon = parentIcon
|
tempEntryInfo?.icon = IconImage(parentIcon.standard)
|
||||||
|
}
|
||||||
|
if (!parentIcon.custom.isUnknown) {
|
||||||
|
tempEntryInfo?.icon = IconImage(parentIcon.custom)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Set default username
|
// Set default username
|
||||||
tempEntryInfo?.username = mDatabase?.defaultUsername ?: ""
|
tempEntryInfo?.username = mDatabase?.defaultUsername ?: ""
|
||||||
@@ -204,8 +208,8 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
.replace(R.id.entry_edit_contents, entryEditFragment!!, ENTRY_EDIT_FRAGMENT_TAG)
|
.replace(R.id.entry_edit_contents, entryEditFragment!!, ENTRY_EDIT_FRAGMENT_TAG)
|
||||||
.commit()
|
.commit()
|
||||||
entryEditFragment?.apply {
|
entryEditFragment?.apply {
|
||||||
drawFactory = mDatabase?.drawFactory
|
drawFactory = mDatabase?.iconDrawableFactory
|
||||||
setOnDateClickListener = View.OnClickListener {
|
setOnDateClickListener = {
|
||||||
expiryTime.date.let { expiresDate ->
|
expiryTime.date.let { expiresDate ->
|
||||||
val dateTime = DateTime(expiresDate)
|
val dateTime = DateTime(expiresDate)
|
||||||
val defaultYear = dateTime.year
|
val defaultYear = dateTime.year
|
||||||
@@ -219,8 +223,8 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
openPasswordGenerator()
|
openPasswordGenerator()
|
||||||
}
|
}
|
||||||
// Add listener to the icon
|
// Add listener to the icon
|
||||||
setOnIconViewClickListener = View.OnClickListener {
|
setOnIconViewClickListener = { iconImage ->
|
||||||
IconPickerDialogFragment.launch(this@EntryEditActivity)
|
IconPickerActivity.launch(this@EntryEditActivity, iconImage)
|
||||||
}
|
}
|
||||||
setOnRemoveAttachment = { attachment ->
|
setOnRemoveAttachment = { attachment ->
|
||||||
mAttachmentFileBinderManager?.removeBinaryAttachment(attachment)
|
mAttachmentFileBinderManager?.removeBinaryAttachment(attachment)
|
||||||
@@ -236,53 +240,8 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
mTempAttachments = savedInstanceState.getParcelableArrayList(TEMP_ATTACHMENTS) ?: mTempAttachments
|
mTempAttachments = savedInstanceState.getParcelableArrayList(TEMP_ATTACHMENTS) ?: mTempAttachments
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign title
|
|
||||||
title = if (mIsNew) getString(R.string.add_entry) else getString(R.string.edit_entry)
|
|
||||||
|
|
||||||
// Bottom Bar
|
|
||||||
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
|
|
||||||
entryEditAddToolBar?.apply {
|
|
||||||
menuInflater.inflate(R.menu.entry_edit, menu)
|
|
||||||
|
|
||||||
menu.findItem(R.id.menu_add_field).apply {
|
|
||||||
val allowCustomField = mDatabase?.allowEntryCustomFields() == true
|
|
||||||
isEnabled = allowCustomField
|
|
||||||
isVisible = allowCustomField
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attachment not compatible below KitKat
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
|
||||||
menu.findItem(R.id.menu_add_attachment).isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.findItem(R.id.menu_add_otp).apply {
|
|
||||||
val allowOTP = mDatabase?.allowOTP == true
|
|
||||||
isEnabled = allowOTP
|
|
||||||
// OTP not compatible below KitKat
|
|
||||||
isVisible = allowOTP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
|
||||||
}
|
|
||||||
|
|
||||||
setOnMenuItemClickListener { item ->
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.menu_add_field -> {
|
|
||||||
addNewCustomField()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.menu_add_attachment -> {
|
|
||||||
addNewAttachment(item)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.menu_add_otp -> {
|
|
||||||
setupOTP()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// To retrieve attachment
|
// To retrieve attachment
|
||||||
mSelectFileHelper = SelectFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||||
|
|
||||||
// Save button
|
// Save button
|
||||||
@@ -338,10 +297,11 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
ACTION_DATABASE_RELOAD_TASK -> {
|
ACTION_DATABASE_RELOAD_TASK -> {
|
||||||
// Close the current activity
|
// Close the current activity
|
||||||
|
this.showActionErrorIfNeeded(result)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
coordinatorLayout?.showActionError(result)
|
coordinatorLayout?.showActionErrorIfNeeded(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,29 +358,27 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
when (entryAttachmentState.downloadState) {
|
when (entryAttachmentState.downloadState) {
|
||||||
AttachmentState.START -> {
|
AttachmentState.START -> {
|
||||||
entryEditFragment?.apply {
|
entryEditFragment?.apply {
|
||||||
// When only one attachment is allowed
|
|
||||||
if (!mAllowMultipleAttachments) {
|
|
||||||
clearAttachments()
|
|
||||||
}
|
|
||||||
putAttachment(entryAttachmentState)
|
putAttachment(entryAttachmentState)
|
||||||
// Scroll to the attachment position
|
// Scroll to the attachment position
|
||||||
getAttachmentViewPosition(entryAttachmentState) {
|
getAttachmentViewPosition(entryAttachmentState) {
|
||||||
scrollView?.smoothScrollTo(0, it.toInt())
|
scrollView?.smoothScrollTo(0, it.toInt())
|
||||||
}
|
}
|
||||||
}
|
} // Add in temp list
|
||||||
|
mTempAttachments.add(entryAttachmentState)
|
||||||
}
|
}
|
||||||
AttachmentState.IN_PROGRESS -> {
|
AttachmentState.IN_PROGRESS -> {
|
||||||
entryEditFragment?.putAttachment(entryAttachmentState)
|
entryEditFragment?.putAttachment(entryAttachmentState)
|
||||||
}
|
}
|
||||||
AttachmentState.COMPLETE -> {
|
AttachmentState.COMPLETE -> {
|
||||||
entryEditFragment?.apply {
|
entryEditFragment?.putAttachment(entryAttachmentState) {
|
||||||
putAttachment(entryAttachmentState)
|
entryEditFragment?.getAttachmentViewPosition(entryAttachmentState) {
|
||||||
// Scroll to the attachment position
|
|
||||||
getAttachmentViewPosition(entryAttachmentState) {
|
|
||||||
scrollView?.smoothScrollTo(0, it.toInt())
|
scrollView?.smoothScrollTo(0, it.toInt())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AttachmentState.CANCELED -> {
|
||||||
|
entryEditFragment?.removeAttachment(entryAttachmentState)
|
||||||
|
}
|
||||||
AttachmentState.ERROR -> {
|
AttachmentState.ERROR -> {
|
||||||
entryEditFragment?.removeAttachment(entryAttachmentState)
|
entryEditFragment?.removeAttachment(entryAttachmentState)
|
||||||
coordinatorLayout?.let {
|
coordinatorLayout?.let {
|
||||||
@@ -500,8 +458,8 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
/**
|
/**
|
||||||
* Add a new attachment
|
* Add a new attachment
|
||||||
*/
|
*/
|
||||||
private fun addNewAttachment(item: MenuItem) {
|
private fun addNewAttachment() {
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.onMenuItemClick(item)
|
mExternalFileHelper?.openDocument()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?) {
|
override fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?) {
|
||||||
@@ -516,16 +474,18 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
|
|
||||||
private fun startUploadAttachment(attachmentToUploadUri: Uri?, attachment: Attachment?) {
|
private fun startUploadAttachment(attachmentToUploadUri: Uri?, attachment: Attachment?) {
|
||||||
if (attachmentToUploadUri != null && attachment != null) {
|
if (attachmentToUploadUri != null && attachment != null) {
|
||||||
|
// When only one attachment is allowed
|
||||||
|
if (!mAllowMultipleAttachments) {
|
||||||
|
entryEditFragment?.clearAttachments()
|
||||||
|
}
|
||||||
// Start uploading in service
|
// Start uploading in service
|
||||||
mAttachmentFileBinderManager?.startUploadAttachment(attachmentToUploadUri, attachment)
|
mAttachmentFileBinderManager?.startUploadAttachment(attachmentToUploadUri, attachment)
|
||||||
// Add in temp list
|
|
||||||
mTempAttachments.add(attachment)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildNewAttachment(attachmentToUploadUri: Uri, fileName: String) {
|
private fun buildNewAttachment(attachmentToUploadUri: Uri, fileName: String) {
|
||||||
val compression = mDatabase?.compressionForNewEntry() ?: false
|
val compression = mDatabase?.compressionForNewEntry() ?: false
|
||||||
mDatabase?.buildNewBinary(UriUtil.getBinaryDir(this), compression)?.let { binaryAttachment ->
|
mDatabase?.buildNewBinaryAttachment(compression)?.let { binaryAttachment ->
|
||||||
val entryAttachment = Attachment(fileName, binaryAttachment)
|
val entryAttachment = Attachment(fileName, binaryAttachment)
|
||||||
// Ask to replace the current attachment
|
// Ask to replace the current attachment
|
||||||
if ((mDatabase?.allowMultipleAttachments != true && entryEditFragment?.containsAttachment() == true) ||
|
if ((mDatabase?.allowMultipleAttachments != true && entryEditFragment?.containsAttachment() == true) ||
|
||||||
@@ -541,9 +501,12 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon ->
|
||||||
|
entryEditFragment?.icon = icon
|
||||||
|
}
|
||||||
|
|
||||||
|
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||||
uri?.let { attachmentToUploadUri ->
|
uri?.let { attachmentToUploadUri ->
|
||||||
// TODO Async to get the name
|
|
||||||
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
|
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
|
||||||
documentFile.name?.let { fileName ->
|
documentFile.name?.let { fileName ->
|
||||||
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
|
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
|
||||||
@@ -572,6 +535,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
* Saves the new entry or update an existing entry in the database
|
* Saves the new entry or update an existing entry in the database
|
||||||
*/
|
*/
|
||||||
private fun saveEntry() {
|
private fun saveEntry() {
|
||||||
|
mAttachmentFileBinderManager?.stopUploadAllAttachments()
|
||||||
// Get the temp entry
|
// Get the temp entry
|
||||||
entryEditFragment?.getEntryInfo()?.let { newEntryInfo ->
|
entryEditFragment?.getEntryInfo()?.let { newEntryInfo ->
|
||||||
|
|
||||||
@@ -583,14 +547,34 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
Entry(mEntry!!)
|
Entry(mEntry!!)
|
||||||
}?.let { newEntry ->
|
}?.let { newEntry ->
|
||||||
|
|
||||||
|
// Do not save entry in upload progression
|
||||||
|
mTempAttachments.forEach { attachmentState ->
|
||||||
|
if (attachmentState.streamDirection == StreamDirection.UPLOAD) {
|
||||||
|
when (attachmentState.downloadState) {
|
||||||
|
AttachmentState.START,
|
||||||
|
AttachmentState.IN_PROGRESS,
|
||||||
|
AttachmentState.CANCELED,
|
||||||
|
AttachmentState.ERROR -> {
|
||||||
|
// Remove attachment not finished from info
|
||||||
|
newEntryInfo.attachments = newEntryInfo.attachments.toMutableList().apply {
|
||||||
|
remove(attachmentState.attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build info
|
// Build info
|
||||||
newEntry.setEntryInfo(mDatabase, newEntryInfo)
|
newEntry.setEntryInfo(mDatabase, newEntryInfo)
|
||||||
|
|
||||||
// Delete temp attachment if not used
|
// Delete temp attachment if not used
|
||||||
mTempAttachments.forEach {
|
mTempAttachments.forEach { tempAttachmentState ->
|
||||||
mDatabase?.binaryPool?.let { binaryPool ->
|
val tempAttachment = tempAttachmentState.attachment
|
||||||
if (!newEntry.getAttachments(binaryPool).contains(it)) {
|
mDatabase?.attachmentPool?.let { binaryPool ->
|
||||||
mDatabase?.removeAttachmentIfNotUsed(it)
|
if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) {
|
||||||
|
mDatabase?.removeAttachmentIfNotUsed(tempAttachment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -619,12 +603,30 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
MenuUtil.contributionMenuInflater(menuInflater, menu)
|
menuInflater.inflate(R.menu.entry_edit, menu)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||||
|
|
||||||
|
menu?.findItem(R.id.menu_add_field)?.apply {
|
||||||
|
val allowCustomField = mDatabase?.allowEntryCustomFields() == true
|
||||||
|
isEnabled = allowCustomField
|
||||||
|
isVisible = allowCustomField
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attachment not compatible below KitKat
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||||
|
menu?.findItem(R.id.menu_add_attachment)?.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
menu?.findItem(R.id.menu_add_otp)?.apply {
|
||||||
|
val allowOTP = mDatabase?.allowOTP == true
|
||||||
|
isEnabled = allowOTP
|
||||||
|
// OTP not compatible below KitKat
|
||||||
|
isVisible = allowOTP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
||||||
|
}
|
||||||
|
|
||||||
entryEditActivityEducation?.let {
|
entryEditActivityEducation?.let {
|
||||||
Handler(Looper.getMainLooper()).post { performedNextEducation(it) }
|
Handler(Looper.getMainLooper()).post { performedNextEducation(it) }
|
||||||
}
|
}
|
||||||
@@ -653,7 +655,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation(
|
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation(
|
||||||
attachmentView,
|
attachmentView,
|
||||||
{
|
{
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(attachmentView)
|
mExternalFileHelper?.openDocument()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(entryEditActivityEducation)
|
performedNextEducation(entryEditActivityEducation)
|
||||||
@@ -676,8 +678,16 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.menu_contribute -> {
|
R.id.menu_add_field -> {
|
||||||
MenuUtil.onContributionItemSelected(this)
|
addNewCustomField()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
R.id.menu_add_attachment -> {
|
||||||
|
addNewAttachment()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
R.id.menu_add_otp -> {
|
||||||
|
setupOTP()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
android.R.id.home -> {
|
android.R.id.home -> {
|
||||||
@@ -708,12 +718,6 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun iconPicked(bundle: Bundle) {
|
|
||||||
IconPickerDialogFragment.getIconStandardFromBundle(bundle)?.let { icon ->
|
|
||||||
entryEditFragment?.icon = icon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) {
|
override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) {
|
||||||
// To fix android 4.4 issue
|
// To fix android 4.4 issue
|
||||||
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
|
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
|
||||||
@@ -787,6 +791,7 @@ class EntryEditActivity : LockingActivity(),
|
|||||||
.setMessage(R.string.discard_changes)
|
.setMessage(R.string.discard_changes)
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.setPositiveButton(R.string.discard) { _, _ ->
|
.setPositiveButton(R.string.discard) { _, _ ->
|
||||||
|
mAttachmentFileBinderManager?.stopUploadAllAttachments()
|
||||||
backPressedAlreadyApproved = true
|
backPressedAlreadyApproved = true
|
||||||
approved.invoke()
|
approved.invoke()
|
||||||
}.create().show()
|
}.create().show()
|
||||||
|
|||||||
@@ -42,8 +42,9 @@ import com.google.android.material.snackbar.Snackbar
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
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.activities.selection.SpecialModeActivity
|
||||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
@@ -52,24 +53,24 @@ import com.kunzisoft.keepass.autofill.AutofillHelper
|
|||||||
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.*
|
import com.kunzisoft.keepass.utils.*
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
||||||
import kotlinx.android.synthetic.main.activity_file_selection.*
|
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
class FileDatabaseSelectActivity : SpecialModeActivity(),
|
class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||||
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||||
private var createDatabaseButtonView: View? = null
|
private var createDatabaseButtonView: View? = null
|
||||||
private var openDatabaseButtonView: View? = null
|
private var openDatabaseButtonView: View? = null
|
||||||
|
|
||||||
@@ -82,7 +83,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
|
|
||||||
private var mDatabaseFileUri: Uri? = null
|
private var mDatabaseFileUri: Uri? = null
|
||||||
|
|
||||||
private var mSelectFileHelper: SelectFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||||
|
|
||||||
@@ -103,14 +104,9 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
createDatabaseButtonView?.setOnClickListener { createNewFile() }
|
createDatabaseButtonView?.setOnClickListener { createNewFile() }
|
||||||
|
|
||||||
// Open database button
|
// Open database button
|
||||||
mSelectFileHelper = SelectFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
||||||
openDatabaseButtonView?.apply {
|
openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
|
||||||
setOnClickListener(it)
|
|
||||||
setOnLongClickListener(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// History list
|
// History list
|
||||||
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
||||||
@@ -162,29 +158,31 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
|
|
||||||
// Observe list of databases
|
// Observe list of databases
|
||||||
databaseFilesViewModel.databaseFilesLoaded.observe(this) { databaseFiles ->
|
databaseFilesViewModel.databaseFilesLoaded.observe(this) { databaseFiles ->
|
||||||
when (databaseFiles.databaseFileAction) {
|
try {
|
||||||
DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
|
when (databaseFiles.databaseFileAction) {
|
||||||
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
|
DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
|
||||||
}
|
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
|
||||||
DatabaseFilesViewModel.DatabaseFileAction.ADD -> {
|
|
||||||
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
|
|
||||||
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
|
|
||||||
}
|
}
|
||||||
GroupActivity.launch(this@FileDatabaseSelectActivity,
|
DatabaseFilesViewModel.DatabaseFileAction.ADD -> {
|
||||||
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity))
|
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
|
||||||
}
|
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
|
||||||
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
|
}
|
||||||
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
|
}
|
||||||
mAdapterDatabaseHistory?.updateDatabaseFileHistory(databaseFileToUpdate)
|
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
|
||||||
}
|
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
|
||||||
}
|
mAdapterDatabaseHistory?.updateDatabaseFileHistory(databaseFileToUpdate)
|
||||||
DatabaseFilesViewModel.DatabaseFileAction.DELETE -> {
|
}
|
||||||
databaseFiles.databaseFileToActivate?.let { databaseFileToDelete ->
|
}
|
||||||
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileToDelete)
|
DatabaseFilesViewModel.DatabaseFileAction.DELETE -> {
|
||||||
|
databaseFiles.databaseFileToActivate?.let { databaseFileToDelete ->
|
||||||
|
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileToDelete)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
databaseFilesViewModel.consumeAction()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to observe database action", e)
|
||||||
}
|
}
|
||||||
databaseFilesViewModel.consumeAction()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Observe default database
|
// Observe default database
|
||||||
@@ -199,9 +197,11 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_CREATE_TASK -> {
|
ACTION_DATABASE_CREATE_TASK -> {
|
||||||
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
|
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
|
||||||
val keyFileUri = result.data?.getParcelable<Uri?>(KEY_FILE_URI_KEY)
|
val mainCredential = result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY) ?: MainCredential()
|
||||||
databaseFilesViewModel.addDatabaseFile(databaseUri, keyFileUri)
|
databaseFilesViewModel.addDatabaseFile(databaseUri, mainCredential.keyFileUri)
|
||||||
}
|
}
|
||||||
|
GroupActivity.launch(this@FileDatabaseSelectActivity,
|
||||||
|
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity))
|
||||||
}
|
}
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
val database = Database.getInstance()
|
val database = Database.getInstance()
|
||||||
@@ -216,7 +216,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
resultError = "$resultError $resultMessage"
|
resultError = "$resultError $resultMessage"
|
||||||
}
|
}
|
||||||
Log.e(TAG, resultError)
|
Log.e(TAG, resultError)
|
||||||
Snackbar.make(activity_file_selection_coordinator_layout,
|
Snackbar.make(coordinatorLayout,
|
||||||
resultError,
|
resultError,
|
||||||
Snackbar.LENGTH_LONG).asError().show()
|
Snackbar.LENGTH_LONG).asError().show()
|
||||||
}
|
}
|
||||||
@@ -230,16 +230,14 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
* Create a new file by calling the content provider
|
* Create a new file by calling the content provider
|
||||||
*/
|
*/
|
||||||
private fun createNewFile() {
|
private fun createNewFile() {
|
||||||
createDocument(this, getString(R.string.database_file_name_default) +
|
mExternalFileHelper?.createDocument( getString(R.string.database_file_name_default) +
|
||||||
getString(R.string.database_file_extension_default), "application/x-keepass")
|
getString(R.string.database_file_extension_default), "application/x-keepass")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fileNoFoundAction(e: FileNotFoundException) {
|
private fun fileNoFoundAction(e: FileNotFoundException) {
|
||||||
val error = getString(R.string.file_not_found_content)
|
val error = getString(R.string.file_not_found_content)
|
||||||
Log.e(TAG, error, e)
|
Log.e(TAG, error, e)
|
||||||
coordinatorLayout?.let {
|
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
||||||
@@ -284,7 +282,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
// Show open and create button or special mode
|
// Show open and create button or special mode
|
||||||
when (mSpecialMode) {
|
when (mSpecialMode) {
|
||||||
SpecialMode.DEFAULT -> {
|
SpecialMode.DEFAULT -> {
|
||||||
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
if (ExternalFileHelper.allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
||||||
// There is an activity which can handle this intent.
|
// There is an activity which can handle this intent.
|
||||||
createDatabaseButtonView?.visibility = View.VISIBLE
|
createDatabaseButtonView?.visibility = View.VISIBLE
|
||||||
} else{
|
} else{
|
||||||
@@ -330,9 +328,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAssignKeyDialogPositiveClick(
|
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
|
||||||
masterPasswordChecked: Boolean, masterPassword: String?,
|
|
||||||
keyFileChecked: Boolean, keyFile: Uri?) {
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mDatabaseFileUri?.let { databaseUri ->
|
mDatabaseFileUri?.let { databaseUri ->
|
||||||
@@ -340,24 +336,17 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
// Create the new database
|
// Create the new database
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseCreate(
|
mProgressDatabaseTaskProvider?.startDatabaseCreate(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
masterPasswordChecked,
|
mainCredential
|
||||||
masterPassword,
|
|
||||||
keyFileChecked,
|
|
||||||
keyFile
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val error = getString(R.string.error_create_database_file)
|
val error = getString(R.string.error_create_database_file)
|
||||||
Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show()
|
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
Log.e(TAG, error, e)
|
Log.e(TAG, error, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAssignKeyDialogNegativeClick(
|
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
|
||||||
masterPasswordChecked: Boolean, masterPassword: String?,
|
|
||||||
keyFileChecked: Boolean, keyFile: Uri?) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
@@ -366,23 +355,21 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
launchPasswordActivityWithPath(uri)
|
launchPasswordActivityWithPath(uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the created URI from the file manager
|
// Retrieve the created URI from the file manager
|
||||||
onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
|
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
|
||||||
mDatabaseFileUri = databaseFileCreatedUri
|
mDatabaseFileUri = databaseFileCreatedUri
|
||||||
if (mDatabaseFileUri != null) {
|
if (mDatabaseFileUri != null) {
|
||||||
AssignMasterKeyDialogFragment.getInstance(true)
|
AssignMasterKeyDialogFragment.getInstance(true)
|
||||||
.show(supportFragmentManager, "passwordDialog")
|
.show(supportFragmentManager, "passwordDialog")
|
||||||
} else {
|
} else {
|
||||||
val error = getString(R.string.error_create_database)
|
val error = getString(R.string.error_create_database)
|
||||||
coordinatorLayout?.let {
|
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
|
|
||||||
}
|
|
||||||
Log.e(TAG, error)
|
Log.e(TAG, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -403,9 +390,10 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
|
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
|
||||||
// If no recent files
|
// If no recent files
|
||||||
val createDatabaseEducationPerformed =
|
val createDatabaseEducationPerformed =
|
||||||
createDatabaseButtonView != null && createDatabaseButtonView!!.visibility == View.VISIBLE
|
createDatabaseButtonView != null
|
||||||
|
&& createDatabaseButtonView!!.visibility == View.VISIBLE
|
||||||
&& mAdapterDatabaseHistory != null
|
&& mAdapterDatabaseHistory != null
|
||||||
&& mAdapterDatabaseHistory!!.itemCount > 0
|
&& mAdapterDatabaseHistory!!.itemCount == 0
|
||||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
||||||
createDatabaseButtonView!!,
|
createDatabaseButtonView!!,
|
||||||
{
|
{
|
||||||
@@ -420,9 +408,9 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
openDatabaseButtonView != null
|
openDatabaseButtonView != null
|
||||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
||||||
openDatabaseButtonView!!,
|
openDatabaseButtonView!!,
|
||||||
{tapTargetView ->
|
{ tapTargetView ->
|
||||||
tapTargetView?.let {
|
tapTargetView?.let {
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
|
mExternalFileHelper?.openDocument()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
|
|||||||
@@ -19,7 +19,9 @@
|
|||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.app.DatePickerDialog
|
||||||
import android.app.SearchManager
|
import android.app.SearchManager
|
||||||
|
import android.app.TimePickerDialog
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -33,9 +35,7 @@ import android.view.Menu
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.*
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
@@ -45,6 +45,7 @@ import androidx.fragment.app.FragmentManager
|
|||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.*
|
import com.kunzisoft.keepass.activities.dialogs.*
|
||||||
|
import com.kunzisoft.keepass.activities.fragments.ListNodesFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
@@ -53,37 +54,35 @@ import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrCha
|
|||||||
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
|
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.*
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
|
||||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.education.GroupActivityEducation
|
import com.kunzisoft.keepass.education.GroupActivityEducation
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.OLD_NODES_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.OLD_NODES_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.view.*
|
import com.kunzisoft.keepass.view.*
|
||||||
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
class GroupActivity : LockingActivity(),
|
class GroupActivity : LockingActivity(),
|
||||||
GroupEditDialogFragment.EditGroupListener,
|
GroupEditDialogFragment.EditGroupListener,
|
||||||
IconPickerDialogFragment.IconPickerListener,
|
DatePickerDialog.OnDateSetListener,
|
||||||
|
TimePickerDialog.OnTimeSetListener,
|
||||||
ListNodesFragment.NodeClickListener,
|
ListNodesFragment.NodeClickListener,
|
||||||
ListNodesFragment.NodesActionMenuListener,
|
ListNodesFragment.NodesActionMenuListener,
|
||||||
DeleteNodesDialogFragment.DeleteNodeListener,
|
DeleteNodesDialogFragment.DeleteNodeListener,
|
||||||
@@ -105,7 +104,6 @@ class GroupActivity : LockingActivity(),
|
|||||||
private var mDatabase: Database? = null
|
private var mDatabase: Database? = null
|
||||||
|
|
||||||
private var mListNodesFragment: ListNodesFragment? = null
|
private var mListNodesFragment: ListNodesFragment? = null
|
||||||
private var mCurrentGroupIsASearch: Boolean = false
|
|
||||||
private var mRequestStartupSearch = true
|
private var mRequestStartupSearch = true
|
||||||
|
|
||||||
private var actionNodeMode: ActionMode? = null
|
private var actionNodeMode: ActionMode? = null
|
||||||
@@ -172,7 +170,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
mCurrentGroup = retrieveCurrentGroup(intent, savedInstanceState)
|
mCurrentGroup = retrieveCurrentGroup(intent, savedInstanceState)
|
||||||
mCurrentGroupIsASearch = Intent.ACTION_SEARCH == intent.action
|
val currentGroupIsASearch = mCurrentGroup?.isVirtual == true
|
||||||
|
|
||||||
Log.i(TAG, "Started creating tree")
|
Log.i(TAG, "Started creating tree")
|
||||||
if (mCurrentGroup == null) {
|
if (mCurrentGroup == null) {
|
||||||
@@ -181,13 +179,13 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
var fragmentTag = LIST_NODES_FRAGMENT_TAG
|
var fragmentTag = LIST_NODES_FRAGMENT_TAG
|
||||||
if (mCurrentGroupIsASearch)
|
if (currentGroupIsASearch)
|
||||||
fragmentTag = SEARCH_FRAGMENT_TAG
|
fragmentTag = SEARCH_FRAGMENT_TAG
|
||||||
|
|
||||||
// Initialize the fragment with the list
|
// Initialize the fragment with the list
|
||||||
mListNodesFragment = supportFragmentManager.findFragmentByTag(fragmentTag) as ListNodesFragment?
|
mListNodesFragment = supportFragmentManager.findFragmentByTag(fragmentTag) as ListNodesFragment?
|
||||||
if (mListNodesFragment == null)
|
if (mListNodesFragment == null)
|
||||||
mListNodesFragment = ListNodesFragment.newInstance(mCurrentGroup, mReadOnly, mCurrentGroupIsASearch)
|
mListNodesFragment = ListNodesFragment.newInstance(mCurrentGroup, mReadOnly, currentGroupIsASearch)
|
||||||
|
|
||||||
// Attach fragment to content view
|
// Attach fragment to content view
|
||||||
supportFragmentManager.beginTransaction().replace(
|
supportFragmentManager.beginTransaction().replace(
|
||||||
@@ -206,9 +204,11 @@ class GroupActivity : LockingActivity(),
|
|||||||
|
|
||||||
// Add listeners to the add buttons
|
// Add listeners to the add buttons
|
||||||
addNodeButtonView?.setAddGroupClickListener {
|
addNodeButtonView?.setAddGroupClickListener {
|
||||||
GroupEditDialogFragment.build()
|
GroupEditDialogFragment.create(GroupInfo().apply {
|
||||||
.show(supportFragmentManager,
|
if (mCurrentGroup?.allowAddNoteInGroup == true) {
|
||||||
GroupEditDialogFragment.TAG_CREATE_GROUP)
|
notes = ""
|
||||||
|
}
|
||||||
|
}).show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP)
|
||||||
}
|
}
|
||||||
addNodeButtonView?.setAddEntryClickListener {
|
addNodeButtonView?.setAddEntryClickListener {
|
||||||
mCurrentGroup?.let { currentGroup ->
|
mCurrentGroup?.let { currentGroup ->
|
||||||
@@ -345,13 +345,16 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
ACTION_DATABASE_RELOAD_TASK -> {
|
ACTION_DATABASE_RELOAD_TASK -> {
|
||||||
// Reload the current activity
|
// Reload the current activity
|
||||||
startActivity(intent)
|
if (result.isSuccess) {
|
||||||
finish()
|
reload()
|
||||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
} else {
|
||||||
|
this.showActionErrorIfNeeded(result)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
coordinatorLayout?.showActionError(result)
|
coordinatorLayout?.showActionErrorIfNeeded(result)
|
||||||
|
|
||||||
finishNodeAction()
|
finishNodeAction()
|
||||||
|
|
||||||
@@ -362,6 +365,14 @@ class GroupActivity : LockingActivity(),
|
|||||||
Log.i(TAG, "Finished creating tree")
|
Log.i(TAG, "Finished creating tree")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun reload() {
|
||||||
|
// Reload the current activity
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||||
|
mDatabase?.wasReloaded = false
|
||||||
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
|
|
||||||
@@ -370,13 +381,10 @@ class GroupActivity : LockingActivity(),
|
|||||||
manageSearchInfoIntent(intentNotNull)
|
manageSearchInfoIntent(intentNotNull)
|
||||||
Log.d(TAG, "setNewIntent: $intentNotNull")
|
Log.d(TAG, "setNewIntent: $intentNotNull")
|
||||||
setIntent(intentNotNull)
|
setIntent(intentNotNull)
|
||||||
mCurrentGroupIsASearch = if (Intent.ACTION_SEARCH == intentNotNull.action) {
|
if (Intent.ACTION_SEARCH == intentNotNull.action) {
|
||||||
// only one instance of search in backstack
|
// only one instance of search in backstack
|
||||||
deletePreviousSearchGroup()
|
deletePreviousSearchGroup()
|
||||||
openGroup(retrieveCurrentGroup(intentNotNull, null), true)
|
openGroup(retrieveCurrentGroup(intentNotNull, null), true)
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -460,12 +468,11 @@ class GroupActivity : LockingActivity(),
|
|||||||
|
|
||||||
private fun refreshSearchGroup() {
|
private fun refreshSearchGroup() {
|
||||||
deletePreviousSearchGroup()
|
deletePreviousSearchGroup()
|
||||||
if (mCurrentGroupIsASearch)
|
if (mCurrentGroup?.isVirtual == true)
|
||||||
openGroup(retrieveCurrentGroup(intent, null), true)
|
openGroup(retrieveCurrentGroup(intent, null), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun retrieveCurrentGroup(intent: Intent, savedInstanceState: Bundle?): Group? {
|
private fun retrieveCurrentGroup(intent: Intent, savedInstanceState: Bundle?): Group? {
|
||||||
|
|
||||||
// Force read only if the database is like that
|
// Force read only if the database is like that
|
||||||
mReadOnly = mDatabase?.isReadOnly == true || mReadOnly
|
mReadOnly = mDatabase?.isReadOnly == true || mReadOnly
|
||||||
|
|
||||||
@@ -513,24 +520,21 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mCurrentGroupIsASearch) {
|
|
||||||
searchTitleView?.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
searchTitleView?.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign icon
|
if (mCurrentGroup?.isVirtual == true) {
|
||||||
if (mCurrentGroupIsASearch) {
|
searchTitleView?.visibility = View.VISIBLE
|
||||||
if (toolbar != null) {
|
if (toolbar != null) {
|
||||||
toolbar?.navigationIcon = null
|
toolbar?.navigationIcon = null
|
||||||
}
|
}
|
||||||
iconView?.visibility = View.GONE
|
iconView?.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
|
searchTitleView?.visibility = View.GONE
|
||||||
// Assign the group icon depending of IconPack or custom icon
|
// Assign the group icon depending of IconPack or custom icon
|
||||||
iconView?.visibility = View.VISIBLE
|
iconView?.visibility = View.VISIBLE
|
||||||
mCurrentGroup?.let {
|
mCurrentGroup?.let { currentGroup ->
|
||||||
if (mDatabase?.drawFactory != null)
|
iconView?.let { imageView ->
|
||||||
iconView?.assignDatabaseIcon(mDatabase?.drawFactory!!, it.icon, mIconColor)
|
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(imageView, currentGroup.icon, mIconColor)
|
||||||
|
}
|
||||||
|
|
||||||
if (toolbar != null) {
|
if (toolbar != null) {
|
||||||
if (mCurrentGroup?.containsParent() == true)
|
if (mCurrentGroup?.containsParent() == true)
|
||||||
@@ -545,20 +549,25 @@ class GroupActivity : LockingActivity(),
|
|||||||
// Assign number of children
|
// Assign number of children
|
||||||
refreshNumberOfChildren()
|
refreshNumberOfChildren()
|
||||||
|
|
||||||
// Show button if allowed
|
// Hide button
|
||||||
addNodeButtonView?.apply {
|
initAddButton()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initAddButton() {
|
||||||
|
addNodeButtonView?.apply {
|
||||||
|
closeButtonIfOpen()
|
||||||
// To enable add button
|
// To enable add button
|
||||||
val addGroupEnabled = !mReadOnly && !mCurrentGroupIsASearch
|
val addGroupEnabled = !mReadOnly && mCurrentGroup?.isVirtual != true
|
||||||
var addEntryEnabled = !mReadOnly && !mCurrentGroupIsASearch
|
var addEntryEnabled = !mReadOnly && mCurrentGroup?.isVirtual != true
|
||||||
mCurrentGroup?.let {
|
mCurrentGroup?.let {
|
||||||
if (!it.allowAddEntryIfIsRoot())
|
if (!it.allowAddEntryIfIsRoot)
|
||||||
addEntryEnabled = it != mRootGroup && addEntryEnabled
|
addEntryEnabled = it != mRootGroup && addEntryEnabled
|
||||||
}
|
}
|
||||||
enableAddGroup(addGroupEnabled)
|
enableAddGroup(addGroupEnabled)
|
||||||
enableAddEntry(addEntryEnabled)
|
enableAddEntry(addEntryEnabled)
|
||||||
|
if (mCurrentGroup?.isVirtual == true)
|
||||||
if (actionNodeMode == null)
|
hideButton()
|
||||||
|
else if (actionNodeMode == null)
|
||||||
showButton()
|
showButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -700,6 +709,39 @@ class GroupActivity : LockingActivity(),
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) {
|
||||||
|
// To fix android 4.4 issue
|
||||||
|
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
|
||||||
|
if (datePicker?.isShown == true) {
|
||||||
|
val groupEditFragment = supportFragmentManager.findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as? GroupEditDialogFragment
|
||||||
|
groupEditFragment?.getExpiryTime()?.date?.let { expiresDate ->
|
||||||
|
groupEditFragment.setExpiryTime(DateInstant(DateTime(expiresDate)
|
||||||
|
.withYear(year)
|
||||||
|
.withMonthOfYear(month + 1)
|
||||||
|
.withDayOfMonth(day)
|
||||||
|
.toDate()))
|
||||||
|
// Launch the time picker
|
||||||
|
val dateTime = DateTime(expiresDate)
|
||||||
|
val defaultHour = dateTime.hourOfDay
|
||||||
|
val defaultMinute = dateTime.minuteOfHour
|
||||||
|
TimePickerFragment.getInstance(defaultHour, defaultMinute)
|
||||||
|
.show(supportFragmentManager, "TimePickerFragment")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTimeSet(view: TimePicker?, hours: Int, minutes: Int) {
|
||||||
|
val groupEditFragment = supportFragmentManager.findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as? GroupEditDialogFragment
|
||||||
|
groupEditFragment?.getExpiryTime()?.date?.let { expiresDate ->
|
||||||
|
// Save the date
|
||||||
|
groupEditFragment.setExpiryTime(
|
||||||
|
DateInstant(DateTime(expiresDate)
|
||||||
|
.withHourOfDay(hours)
|
||||||
|
.withMinuteOfHour(minutes)
|
||||||
|
.toDate()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun finishNodeAction() {
|
private fun finishNodeAction() {
|
||||||
actionNodeMode?.finish()
|
actionNodeMode?.finish()
|
||||||
}
|
}
|
||||||
@@ -745,7 +787,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
when (node.type) {
|
when (node.type) {
|
||||||
Type.GROUP -> {
|
Type.GROUP -> {
|
||||||
mOldGroupToUpdate = node as Group
|
mOldGroupToUpdate = node as Group
|
||||||
GroupEditDialogFragment.build(mOldGroupToUpdate!!)
|
GroupEditDialogFragment.update(mOldGroupToUpdate!!.getGroupInfo())
|
||||||
.show(supportFragmentManager,
|
.show(supportFragmentManager,
|
||||||
GroupEditDialogFragment.TAG_CREATE_GROUP)
|
GroupEditDialogFragment.TAG_CREATE_GROUP)
|
||||||
}
|
}
|
||||||
@@ -770,74 +812,75 @@ class GroupActivity : LockingActivity(),
|
|||||||
|
|
||||||
override fun onPasteMenuClick(pasteMode: ListNodesFragment.PasteMode?,
|
override fun onPasteMenuClick(pasteMode: ListNodesFragment.PasteMode?,
|
||||||
nodes: List<Node>): Boolean {
|
nodes: List<Node>): Boolean {
|
||||||
// Move or copy only if allowed (in root if allowed)
|
when (pasteMode) {
|
||||||
if (mCurrentGroup != mDatabase?.rootGroup
|
ListNodesFragment.PasteMode.PASTE_FROM_COPY -> {
|
||||||
|| mDatabase?.rootCanContainsEntry() == true) {
|
// Copy
|
||||||
|
mCurrentGroup?.let { newParent ->
|
||||||
when (pasteMode) {
|
mProgressDatabaseTaskProvider?.startDatabaseCopyNodes(
|
||||||
ListNodesFragment.PasteMode.PASTE_FROM_COPY -> {
|
nodes,
|
||||||
// Copy
|
newParent,
|
||||||
mCurrentGroup?.let { newParent ->
|
!mReadOnly && mAutoSaveEnable
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseCopyNodes(
|
)
|
||||||
nodes,
|
|
||||||
newParent,
|
|
||||||
!mReadOnly && mAutoSaveEnable
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ListNodesFragment.PasteMode.PASTE_FROM_MOVE -> {
|
|
||||||
// Move
|
|
||||||
mCurrentGroup?.let { newParent ->
|
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseMoveNodes(
|
|
||||||
nodes,
|
|
||||||
newParent,
|
|
||||||
!mReadOnly && mAutoSaveEnable
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
ListNodesFragment.PasteMode.PASTE_FROM_MOVE -> {
|
||||||
coordinatorLayout?.let { coordinatorLayout ->
|
// Move
|
||||||
Snackbar.make(coordinatorLayout,
|
mCurrentGroup?.let { newParent ->
|
||||||
R.string.error_copy_entry_here,
|
mProgressDatabaseTaskProvider?.startDatabaseMoveNodes(
|
||||||
Snackbar.LENGTH_LONG).asError().show()
|
nodes,
|
||||||
|
newParent,
|
||||||
|
!mReadOnly && mAutoSaveEnable
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
finishNodeAction()
|
finishNodeAction()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun eachNodeRecyclable(nodes: List<Node>): Boolean {
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
return nodes.find { node ->
|
||||||
|
var cannotRecycle = true
|
||||||
|
if (node is Entry) {
|
||||||
|
cannotRecycle = !database.canRecycle(node)
|
||||||
|
} else if (node is Group) {
|
||||||
|
cannotRecycle = !database.canRecycle(node)
|
||||||
|
}
|
||||||
|
cannotRecycle
|
||||||
|
} == null
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false): Boolean {
|
private fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false): Boolean {
|
||||||
val database = mDatabase
|
mDatabase?.let { database ->
|
||||||
|
|
||||||
// If recycle bin enabled, ensure it exists
|
// If recycle bin enabled, ensure it exists
|
||||||
if (database != null && database.isRecycleBinEnabled) {
|
if (database.isRecycleBinEnabled) {
|
||||||
database.ensureRecycleBinExists(resources)
|
database.ensureRecycleBinExists(resources)
|
||||||
}
|
|
||||||
|
|
||||||
// If recycle bin enabled and not in recycle bin, move in recycle bin
|
|
||||||
if (database != null
|
|
||||||
&& database.isRecycleBinEnabled
|
|
||||||
&& database.recycleBin != mCurrentGroup) {
|
|
||||||
|
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes(
|
|
||||||
nodes,
|
|
||||||
!mReadOnly && mAutoSaveEnable
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// else open the dialog to confirm deletion
|
|
||||||
else {
|
|
||||||
val deleteNodesDialogFragment: DeleteNodesDialogFragment =
|
|
||||||
if (recycleBin) {
|
|
||||||
EmptyRecycleBinDialogFragment.getInstance(nodes)
|
|
||||||
} else {
|
|
||||||
DeleteNodesDialogFragment.getInstance(nodes)
|
|
||||||
}
|
}
|
||||||
deleteNodesDialogFragment.show(supportFragmentManager, "deleteNodesDialogFragment")
|
|
||||||
|
// If recycle bin enabled and not in recycle bin, move in recycle bin
|
||||||
|
if (eachNodeRecyclable(nodes)) {
|
||||||
|
mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes(
|
||||||
|
nodes,
|
||||||
|
!mReadOnly && mAutoSaveEnable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// else open the dialog to confirm deletion
|
||||||
|
else {
|
||||||
|
val deleteNodesDialogFragment: DeleteNodesDialogFragment =
|
||||||
|
if (recycleBin) {
|
||||||
|
EmptyRecycleBinDialogFragment.getInstance(nodes)
|
||||||
|
} else {
|
||||||
|
DeleteNodesDialogFragment.getInstance(nodes)
|
||||||
|
}
|
||||||
|
deleteNodesDialogFragment.show(supportFragmentManager, "deleteNodesDialogFragment")
|
||||||
|
}
|
||||||
|
finishNodeAction()
|
||||||
}
|
}
|
||||||
finishNodeAction()
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -855,6 +898,9 @@ class GroupActivity : LockingActivity(),
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
|
if (mDatabase?.wasReloaded == true) {
|
||||||
|
reload()
|
||||||
|
}
|
||||||
// Show the lock button
|
// Show the lock button
|
||||||
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
|
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
|
||||||
View.VISIBLE
|
View.VISIBLE
|
||||||
@@ -1031,19 +1077,27 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?,
|
override fun isValidGroupName(name: String): GroupEditDialogFragment.Error {
|
||||||
name: String?,
|
if (name.isEmpty()) {
|
||||||
icon: IconImage?) {
|
return GroupEditDialogFragment.Error(true, R.string.error_no_name)
|
||||||
|
}
|
||||||
|
if (mDatabase?.groupNamesNotAllowed?.find { it.equals(name, ignoreCase = true) } != null) {
|
||||||
|
return GroupEditDialogFragment.Error(true, R.string.error_word_reserved)
|
||||||
|
}
|
||||||
|
return GroupEditDialogFragment.Error(false, null)
|
||||||
|
}
|
||||||
|
|
||||||
if (name != null && name.isNotEmpty() && icon != null) {
|
override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction,
|
||||||
|
groupInfo: GroupInfo) {
|
||||||
|
|
||||||
|
if (groupInfo.title.isNotEmpty()) {
|
||||||
when (action) {
|
when (action) {
|
||||||
GroupEditDialogFragment.EditGroupDialogAction.CREATION -> {
|
GroupEditDialogFragment.EditGroupDialogAction.CREATION -> {
|
||||||
// If group creation
|
// If group creation
|
||||||
mCurrentGroup?.let { currentGroup ->
|
mCurrentGroup?.let { currentGroup ->
|
||||||
// Build the group
|
// Build the group
|
||||||
mDatabase?.createGroup()?.let { newGroup ->
|
mDatabase?.createGroup()?.let { newGroup ->
|
||||||
newGroup.title = name
|
newGroup.setGroupInfo(groupInfo)
|
||||||
newGroup.icon = icon
|
|
||||||
// Not really needed here because added in runnable but safe
|
// Not really needed here because added in runnable but safe
|
||||||
newGroup.parent = currentGroup
|
newGroup.parent = currentGroup
|
||||||
|
|
||||||
@@ -1063,9 +1117,7 @@ class GroupActivity : LockingActivity(),
|
|||||||
// WARNING remove parent and children to keep memory
|
// WARNING remove parent and children to keep memory
|
||||||
removeParent()
|
removeParent()
|
||||||
removeChildren()
|
removeChildren()
|
||||||
|
this.setGroupInfo(groupInfo)
|
||||||
title = name
|
|
||||||
this.icon = icon // TODO custom icon #96
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If group updated save it in the database
|
// If group updated save it in the database
|
||||||
@@ -1081,19 +1133,11 @@ class GroupActivity : LockingActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cancelEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?,
|
override fun cancelEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction,
|
||||||
name: String?,
|
groupInfo: GroupInfo) {
|
||||||
icon: IconImage?) {
|
|
||||||
// Do nothing here
|
// Do nothing here
|
||||||
}
|
}
|
||||||
|
|
||||||
override// For icon in create tree dialog
|
|
||||||
fun iconPicked(bundle: Bundle) {
|
|
||||||
(supportFragmentManager
|
|
||||||
.findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as GroupEditDialogFragment)
|
|
||||||
.iconPicked(bundle)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
override fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
||||||
mListNodesFragment?.onSortSelected(sortNodeEnum, sortNodeParameters)
|
mListNodesFragment?.onSortSelected(sortNodeEnum, sortNodeParameters)
|
||||||
}
|
}
|
||||||
@@ -1132,6 +1176,13 @@ class GroupActivity : LockingActivity(),
|
|||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
|
// To create tree dialog for icon
|
||||||
|
IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon ->
|
||||||
|
(supportFragmentManager
|
||||||
|
.findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as GroupEditDialogFragment)
|
||||||
|
.setIcon(icon)
|
||||||
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
@@ -1156,7 +1207,6 @@ class GroupActivity : LockingActivity(),
|
|||||||
mCurrentGroup = mListNodesFragment?.mainGroup
|
mCurrentGroup = mListNodesFragment?.mainGroup
|
||||||
// Remove search in intent
|
// Remove search in intent
|
||||||
deletePreviousSearchGroup()
|
deletePreviousSearchGroup()
|
||||||
mCurrentGroupIsASearch = false
|
|
||||||
if (Intent.ACTION_SEARCH == intent.action) {
|
if (Intent.ACTION_SEARCH == intent.action) {
|
||||||
intent.action = Intent.ACTION_DEFAULT
|
intent.action = Intent.ACTION_DEFAULT
|
||||||
intent.removeExtra(SearchManager.QUERY)
|
intent.removeExtra(SearchManager.QUERY)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,15 +36,13 @@ import android.widget.*
|
|||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.*
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
@@ -56,14 +54,14 @@ import com.kunzisoft.keepass.database.element.Database
|
|||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.MASTER_PASSWORD_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
@@ -71,7 +69,6 @@ import com.kunzisoft.keepass.utils.UriUtil
|
|||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||||
import kotlinx.android.synthetic.main.activity_password.*
|
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
||||||
@@ -84,8 +81,9 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
private var confirmButtonView: Button? = null
|
private var confirmButtonView: Button? = null
|
||||||
private var checkboxPasswordView: CompoundButton? = null
|
private var checkboxPasswordView: CompoundButton? = null
|
||||||
private var checkboxKeyFileView: CompoundButton? = null
|
private var checkboxKeyFileView: CompoundButton? = null
|
||||||
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
|
|
||||||
private var infoContainerView: ViewGroup? = null
|
private var infoContainerView: ViewGroup? = null
|
||||||
|
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||||
|
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
|
||||||
|
|
||||||
private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
|
private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||||
|
|
||||||
@@ -94,7 +92,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
private var mDatabaseKeyFileUri: Uri? = null
|
private var mDatabaseKeyFileUri: Uri? = null
|
||||||
|
|
||||||
private var mRememberKeyFile: Boolean = false
|
private var mRememberKeyFile: Boolean = false
|
||||||
private var mSelectFileHelper: SelectFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var mPermissionAsked = false
|
private var mPermissionAsked = false
|
||||||
private var readOnly: Boolean = false
|
private var readOnly: Boolean = false
|
||||||
@@ -131,18 +129,14 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
||||||
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
||||||
infoContainerView = findViewById(R.id.activity_password_info_container)
|
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||||
|
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
|
||||||
|
|
||||||
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
||||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
||||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||||
|
|
||||||
mSelectFileHelper = SelectFileHelper(this@PasswordActivity)
|
mExternalFileHelper = ExternalFileHelper(this@PasswordActivity)
|
||||||
keyFileSelectionView?.apply {
|
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
|
||||||
setOnClickListener(it)
|
|
||||||
setOnLongClickListener(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
||||||
passwordView?.addTextChangedListener(object : TextWatcher {
|
passwordView?.addTextChangedListener(object : TextWatcher {
|
||||||
@@ -236,15 +230,13 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
showLoadDatabaseDuplicateUuidMessage {
|
showLoadDatabaseDuplicateUuidMessage {
|
||||||
|
|
||||||
var databaseUri: Uri? = null
|
var databaseUri: Uri? = null
|
||||||
var masterPassword: String? = null
|
var mainCredential: MainCredential = MainCredential()
|
||||||
var keyFileUri: Uri? = null
|
|
||||||
var readOnly = true
|
var readOnly = true
|
||||||
var cipherEntity: CipherDatabaseEntity? = null
|
var cipherEntity: CipherDatabaseEntity? = null
|
||||||
|
|
||||||
result.data?.let { resultData ->
|
result.data?.let { resultData ->
|
||||||
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
||||||
masterPassword = resultData.getString(MASTER_PASSWORD_KEY)
|
mainCredential = resultData.getParcelable(MAIN_CREDENTIAL_KEY) ?: mainCredential
|
||||||
keyFileUri = resultData.getParcelable(KEY_FILE_URI_KEY)
|
|
||||||
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
||||||
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
|
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
|
||||||
}
|
}
|
||||||
@@ -252,8 +244,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
databaseUri?.let { databaseFileUri ->
|
databaseUri?.let { databaseFileUri ->
|
||||||
showProgressDialogAndLoadDatabase(
|
showProgressDialogAndLoadDatabase(
|
||||||
databaseFileUri,
|
databaseFileUri,
|
||||||
masterPassword,
|
mainCredential,
|
||||||
keyFileUri,
|
|
||||||
readOnly,
|
readOnly,
|
||||||
cipherEntity,
|
cipherEntity,
|
||||||
true)
|
true)
|
||||||
@@ -274,7 +265,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
resultError = "$resultError $resultMessage"
|
resultError = "$resultError $resultMessage"
|
||||||
}
|
}
|
||||||
Log.e(TAG, resultError)
|
Log.e(TAG, resultError)
|
||||||
Snackbar.make(activity_password_coordinator_layout,
|
Snackbar.make(coordinatorLayout,
|
||||||
resultError,
|
resultError,
|
||||||
Snackbar.LENGTH_LONG).asError().show()
|
Snackbar.LENGTH_LONG).asError().show()
|
||||||
}
|
}
|
||||||
@@ -526,7 +517,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
|| mSpecialMode == SpecialMode.REGISTRATION)
|
|| mSpecialMode == SpecialMode.REGISTRATION)
|
||||||
) {
|
) {
|
||||||
Log.e(TAG, getString(R.string.autofill_read_only_save))
|
Log.e(TAG, getString(R.string.autofill_read_only_save))
|
||||||
Snackbar.make(activity_password_coordinator_layout,
|
Snackbar.make(coordinatorLayout,
|
||||||
R.string.autofill_read_only_save,
|
R.string.autofill_read_only_save,
|
||||||
Snackbar.LENGTH_LONG).asError().show()
|
Snackbar.LENGTH_LONG).asError().show()
|
||||||
} else {
|
} else {
|
||||||
@@ -534,8 +525,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
// Show the progress dialog and load the database
|
// Show the progress dialog and load the database
|
||||||
showProgressDialogAndLoadDatabase(
|
showProgressDialogAndLoadDatabase(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
password,
|
MainCredential(password, keyFileUri),
|
||||||
keyFileUri,
|
|
||||||
readOnly,
|
readOnly,
|
||||||
cipherDatabaseEntity,
|
cipherDatabaseEntity,
|
||||||
false)
|
false)
|
||||||
@@ -544,15 +534,13 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
|
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
|
||||||
password: String?,
|
mainCredential: MainCredential,
|
||||||
keyFile: Uri?,
|
|
||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
cipherDatabaseEntity: CipherDatabaseEntity?,
|
cipherDatabaseEntity: CipherDatabaseEntity?,
|
||||||
fixDuplicateUUID: Boolean) {
|
fixDuplicateUUID: Boolean) {
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseLoad(
|
mProgressDatabaseTaskProvider?.startDatabaseLoad(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
password,
|
mainCredential,
|
||||||
keyFile,
|
|
||||||
readOnly,
|
readOnly,
|
||||||
cipherDatabaseEntity,
|
cipherDatabaseEntity,
|
||||||
fixDuplicateUUID
|
fixDuplicateUUID
|
||||||
@@ -706,8 +694,8 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil
|
|||||||
}
|
}
|
||||||
|
|
||||||
var keyFileResult = false
|
var keyFileResult = false
|
||||||
mSelectFileHelper?.let {
|
mExternalFileHelper?.let {
|
||||||
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
keyFileResult = it.onOpenDocumentResult(requestCode, resultCode, data
|
||||||
) { uri ->
|
) { uri ->
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
mDatabaseKeyFileUri = uri
|
mDatabaseKeyFileUri = uri
|
||||||
|
|||||||
@@ -30,13 +30,14 @@ import android.text.SpannableStringBuilder
|
|||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
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.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
|
|
||||||
@@ -59,7 +60,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
private var mListener: AssignPasswordDialogListener? = null
|
private var mListener: AssignPasswordDialogListener? = null
|
||||||
|
|
||||||
private var mSelectFileHelper: SelectFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
|
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
|
||||||
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
||||||
@@ -76,10 +77,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface AssignPasswordDialogListener {
|
interface AssignPasswordDialogListener {
|
||||||
fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?,
|
fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential)
|
||||||
keyFileChecked: Boolean, keyFile: Uri?)
|
fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential)
|
||||||
fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean, masterPassword: String?,
|
|
||||||
keyFileChecked: Boolean, keyFile: Uri?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttach(activity: Context) {
|
override fun onAttach(activity: Context) {
|
||||||
@@ -121,8 +120,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
|
||||||
val credentialsInfo: ImageView? = rootView?.findViewById(R.id.credentials_information)
|
rootView?.findViewById<View>(R.id.credentials_information)?.setOnClickListener {
|
||||||
credentialsInfo?.setOnClickListener {
|
|
||||||
UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
|
UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,11 +133,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
||||||
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
||||||
|
|
||||||
mSelectFileHelper = SelectFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
keyFileSelectionView?.apply {
|
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
setOnClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
|
||||||
setOnLongClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
val dialog = builder.create()
|
val dialog = builder.create()
|
||||||
|
|
||||||
@@ -161,17 +156,13 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!error) {
|
if (!error) {
|
||||||
mListener?.onAssignKeyDialogPositiveClick(
|
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
|
||||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
|
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
|
||||||
negativeButton.setOnClickListener {
|
negativeButton.setOnClickListener {
|
||||||
mListener?.onAssignKeyDialogNegativeClick(
|
mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
|
||||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
|
||||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,6 +174,12 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
return super.onCreateDialog(savedInstanceState)
|
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() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
@@ -242,9 +239,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
builder.setMessage(R.string.warning_empty_password)
|
builder.setMessage(R.string.warning_empty_password)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
if (!verifyKeyFile()) {
|
if (!verifyKeyFile()) {
|
||||||
mListener?.onAssignKeyDialogPositiveClick(
|
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
|
||||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
|
||||||
this@AssignMasterKeyDialogFragment.dismiss()
|
this@AssignMasterKeyDialogFragment.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,9 +254,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
val builder = AlertDialog.Builder(it)
|
val builder = AlertDialog.Builder(it)
|
||||||
builder.setMessage(R.string.warning_no_encryption_key)
|
builder.setMessage(R.string.warning_no_encryption_key)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
mListener?.onAssignKeyDialogPositiveClick(
|
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
|
||||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
|
||||||
this@AssignMasterKeyDialogFragment.dismiss()
|
this@AssignMasterKeyDialogFragment.dismiss()
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
@@ -293,7 +286,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
||||||
uri?.let { pathUri ->
|
uri?.let { pathUri ->
|
||||||
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
||||||
keyFileSelectionView?.error = null
|
keyFileSelectionView?.error = null
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ import androidx.fragment.app.DialogFragment
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
||||||
|
|
||||||
open class DeleteNodesDialogFragment : DialogFragment() {
|
open class DeleteNodesDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ class DuplicateUuidDialog : DialogFragment() {
|
|||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
// Use the Builder class for convenient dialog construction
|
// Use the Builder class for convenient dialog construction
|
||||||
val builder = androidx.appcompat.app.AlertDialog.Builder(activity).apply {
|
val builder = AlertDialog.Builder(activity).apply {
|
||||||
val message = getString(R.string.contains_duplicate_uuid) +
|
val message = getString(R.string.contains_duplicate_uuid) +
|
||||||
"\n\n" + getString(R.string.contains_duplicate_uuid_procedure)
|
"\n\n" + getString(R.string.contains_duplicate_uuid_procedure)
|
||||||
setMessage(message)
|
setMessage(message)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||||
|
|
||||||
class EmptyRecycleBinDialogFragment : DeleteNodesDialogFragment() {
|
class EmptyRecycleBinDialogFragment : DeleteNodesDialogFragment() {
|
||||||
|
|
||||||
|
|||||||
@@ -23,34 +23,40 @@ import android.app.Dialog
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import android.view.View
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
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.CREATION
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
|
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
|
import com.kunzisoft.keepass.view.ExpirationView
|
||||||
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconPickerListener {
|
class GroupEditDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
private var mDatabase: Database? = null
|
private var mDatabase: Database? = null
|
||||||
|
|
||||||
private var editGroupListener: EditGroupListener? = null
|
private var mEditGroupListener: EditGroupListener? = null
|
||||||
|
|
||||||
private var editGroupDialogAction: EditGroupDialogAction? = null
|
private var mEditGroupDialogAction = EditGroupDialogAction.NONE
|
||||||
private var nameGroup: String? = null
|
private var mGroupInfo = GroupInfo()
|
||||||
private var iconGroup: IconImage? = null
|
|
||||||
|
|
||||||
private var nameTextLayoutView: TextInputLayout? = null
|
private lateinit var iconButtonView: ImageView
|
||||||
private var nameTextView: TextView? = null
|
|
||||||
private var iconButtonView: ImageView? = null
|
|
||||||
private var iconColor: Int = 0
|
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 {
|
enum class EditGroupDialogAction {
|
||||||
CREATION, UPDATE, NONE;
|
CREATION, UPDATE, NONE;
|
||||||
@@ -67,7 +73,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
// Verify that the host activity implements the callback interface
|
// Verify that the host activity implements the callback interface
|
||||||
try {
|
try {
|
||||||
// Instantiate the NoticeDialogListener so we can send events to the host
|
// Instantiate the NoticeDialogListener so we can send events to the host
|
||||||
editGroupListener = context as EditGroupListener
|
mEditGroupListener = context as EditGroupListener
|
||||||
} catch (e: ClassCastException) {
|
} catch (e: ClassCastException) {
|
||||||
// The activity doesn't implement the interface, throw exception
|
// The activity doesn't implement the interface, throw exception
|
||||||
throw ClassCastException(context.toString()
|
throw ClassCastException(context.toString()
|
||||||
@@ -76,16 +82,19 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetach() {
|
override fun onDetach() {
|
||||||
editGroupListener = null
|
mEditGroupListener = null
|
||||||
super.onDetach()
|
super.onDetach()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val root = activity.layoutInflater.inflate(R.layout.fragment_group_edit, null)
|
val root = activity.layoutInflater.inflate(R.layout.fragment_group_edit, null)
|
||||||
nameTextLayoutView = root?.findViewById(R.id.group_edit_name_container)
|
iconButtonView = root.findViewById(R.id.group_edit_icon_button)
|
||||||
nameTextView = root?.findViewById(R.id.group_edit_name)
|
nameTextLayoutView = root.findViewById(R.id.group_edit_name_container)
|
||||||
iconButtonView = root?.findViewById(R.id.group_edit_icon_button)
|
nameTextView = root.findViewById(R.id.group_edit_name)
|
||||||
|
notesTextLayoutView = root.findViewById(R.id.group_edit_note_container)
|
||||||
|
notesTextView = root.findViewById(R.id.group_edit_note)
|
||||||
|
expirationView = root.findViewById(R.id.group_edit_expiration)
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
// Retrieve the textColor to tint the icon
|
||||||
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
@@ -94,47 +103,47 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
|
|
||||||
// Init elements
|
// Init elements
|
||||||
mDatabase = Database.getInstance()
|
mDatabase = Database.getInstance()
|
||||||
editGroupDialogAction = EditGroupDialogAction.NONE
|
|
||||||
nameGroup = ""
|
|
||||||
iconGroup = mDatabase?.iconFactory?.folderIcon
|
|
||||||
|
|
||||||
if (savedInstanceState != null
|
if (savedInstanceState != null
|
||||||
&& savedInstanceState.containsKey(KEY_ACTION_ID)
|
&& savedInstanceState.containsKey(KEY_ACTION_ID)
|
||||||
&& savedInstanceState.containsKey(KEY_NAME)
|
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
|
||||||
&& savedInstanceState.containsKey(KEY_ICON)) {
|
mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID))
|
||||||
editGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID))
|
mGroupInfo = savedInstanceState.getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
||||||
nameGroup = savedInstanceState.getString(KEY_NAME)
|
|
||||||
iconGroup = savedInstanceState.getParcelable(KEY_ICON)
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
if (containsKey(KEY_ACTION_ID))
|
if (containsKey(KEY_ACTION_ID))
|
||||||
editGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID))
|
mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID))
|
||||||
|
if (containsKey(KEY_GROUP_INFO)) {
|
||||||
if (containsKey(KEY_NAME) && containsKey(KEY_ICON)) {
|
mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
||||||
nameGroup = getString(KEY_NAME)
|
|
||||||
iconGroup = getParcelable(KEY_ICON)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// populate the name
|
// populate info in views
|
||||||
nameTextView?.text = nameGroup
|
populateInfoToViews()
|
||||||
// populate the icon
|
expirationView.setOnDateClickListener = {
|
||||||
assignIconView()
|
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)
|
val builder = AlertDialog.Builder(activity)
|
||||||
builder.setView(root)
|
builder.setView(root)
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
editGroupListener?.cancelEditGroup(
|
retrieveGroupInfoFromViews()
|
||||||
editGroupDialogAction,
|
mEditGroupListener?.cancelEditGroup(
|
||||||
nameTextView?.text?.toString(),
|
mEditGroupDialogAction,
|
||||||
iconGroup)
|
mGroupInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
iconButtonView?.setOnClickListener { _ ->
|
iconButtonView.setOnClickListener { _ ->
|
||||||
IconPickerDialogFragment().show(parentFragmentManager, "IconPickerDialogFragment")
|
IconPickerActivity.launch(activity, mGroupInfo.icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.create()
|
return builder.create()
|
||||||
@@ -150,69 +159,104 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
if (d != null) {
|
if (d != null) {
|
||||||
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
|
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
|
||||||
positiveButton.setOnClickListener {
|
positiveButton.setOnClickListener {
|
||||||
|
retrieveGroupInfoFromViews()
|
||||||
if (isValid()) {
|
if (isValid()) {
|
||||||
editGroupListener?.approveEditGroup(
|
mEditGroupListener?.approveEditGroup(
|
||||||
editGroupDialogAction,
|
mEditGroupDialogAction,
|
||||||
nameTextView?.text?.toString(),
|
mGroupInfo)
|
||||||
iconGroup)
|
|
||||||
d.dismiss()
|
d.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assignIconView() {
|
fun getExpiryTime(): DateInstant {
|
||||||
if (mDatabase?.drawFactory != null && iconGroup != null) {
|
retrieveGroupInfoFromViews()
|
||||||
iconButtonView?.assignDatabaseIcon(mDatabase?.drawFactory!!, iconGroup!!, iconColor)
|
return mGroupInfo.expiryTime
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun iconPicked(bundle: Bundle) {
|
fun setExpiryTime(expiryTime: DateInstant) {
|
||||||
iconGroup = IconPickerDialogFragment.getIconStandardFromBundle(bundle)
|
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()
|
assignIconView()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
outState.putInt(KEY_ACTION_ID, editGroupDialogAction!!.ordinal)
|
retrieveGroupInfoFromViews()
|
||||||
outState.putString(KEY_NAME, nameGroup)
|
outState.putInt(KEY_ACTION_ID, mEditGroupDialogAction.ordinal)
|
||||||
outState.putParcelable(KEY_ICON, iconGroup)
|
outState.putParcelable(KEY_GROUP_INFO, mGroupInfo)
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isValid(): Boolean {
|
private fun isValid(): Boolean {
|
||||||
if (nameTextView?.text?.toString()?.isNotEmpty() != true) {
|
val error = mEditGroupListener?.isValidGroupName(nameTextView.text.toString()) ?: Error(false, null)
|
||||||
nameTextLayoutView?.error = getString(R.string.error_no_name)
|
error.messageId?.let { messageId ->
|
||||||
return false
|
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 {
|
interface EditGroupListener {
|
||||||
fun approveEditGroup(action: EditGroupDialogAction?, name: String?, icon: IconImage?)
|
fun isValidGroupName(name: String): Error
|
||||||
fun cancelEditGroup(action: EditGroupDialogAction?, name: String?, icon: IconImage?)
|
fun approveEditGroup(action: EditGroupDialogAction,
|
||||||
|
groupInfo: GroupInfo)
|
||||||
|
fun cancelEditGroup(action: EditGroupDialogAction,
|
||||||
|
groupInfo: GroupInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
|
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_ACTION_ID = "KEY_ACTION_ID"
|
||||||
|
const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
||||||
|
|
||||||
fun build(): GroupEditDialogFragment {
|
fun create(groupInfo: GroupInfo): GroupEditDialogFragment {
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
bundle.putInt(KEY_ACTION_ID, CREATION.ordinal)
|
bundle.putInt(KEY_ACTION_ID, CREATION.ordinal)
|
||||||
|
bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
|
||||||
val fragment = GroupEditDialogFragment()
|
val fragment = GroupEditDialogFragment()
|
||||||
fragment.arguments = bundle
|
fragment.arguments = bundle
|
||||||
return fragment
|
return fragment
|
||||||
}
|
}
|
||||||
|
|
||||||
fun build(group: Group): GroupEditDialogFragment {
|
fun update(groupInfo: GroupInfo): GroupEditDialogFragment {
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
bundle.putString(KEY_NAME, group.title)
|
|
||||||
bundle.putParcelable(KEY_ICON, group.icon)
|
|
||||||
bundle.putInt(KEY_ACTION_ID, UPDATE.ordinal)
|
bundle.putInt(KEY_ACTION_ID, UPDATE.ordinal)
|
||||||
|
bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
|
||||||
val fragment = GroupEditDialogFragment()
|
val fragment = GroupEditDialogFragment()
|
||||||
fragment.arguments = bundle
|
fragment.arguments = bundle
|
||||||
return fragment
|
return fragment
|
||||||
|
|||||||
@@ -1,147 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.res.ColorStateList
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.BaseAdapter
|
|
||||||
import android.widget.GridView
|
|
||||||
import android.widget.ImageView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.widget.ImageViewCompat
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
|
||||||
import com.kunzisoft.keepass.icons.IconPack
|
|
||||||
import com.kunzisoft.keepass.icons.IconPackChooser
|
|
||||||
|
|
||||||
|
|
||||||
class IconPickerDialogFragment : DialogFragment() {
|
|
||||||
|
|
||||||
private var iconPickerListener: IconPickerListener? = null
|
|
||||||
private var iconPack: IconPack? = null
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
try {
|
|
||||||
iconPickerListener = context as IconPickerListener
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
// The activity doesn't implement the interface, throw exception
|
|
||||||
throw ClassCastException(context.toString()
|
|
||||||
+ " must implement " + IconPickerListener::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetach() {
|
|
||||||
iconPickerListener = null
|
|
||||||
super.onDetach()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
activity?.let { activity ->
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
|
||||||
|
|
||||||
iconPack = IconPackChooser.getSelectedIconPack(requireContext())
|
|
||||||
|
|
||||||
// Inflate and set the layout for the dialog
|
|
||||||
// Pass null as the parent view because its going in the dialog layout
|
|
||||||
val root = activity.layoutInflater.inflate(R.layout.fragment_icon_picker, null)
|
|
||||||
builder.setView(root)
|
|
||||||
|
|
||||||
val currIconGridView = root.findViewById<GridView>(R.id.IconGridView)
|
|
||||||
currIconGridView.adapter = ImageAdapter(activity)
|
|
||||||
|
|
||||||
currIconGridView.setOnItemClickListener { _, _, position, _ ->
|
|
||||||
val bundle = Bundle()
|
|
||||||
bundle.putParcelable(KEY_ICON_STANDARD, IconImageStandard(position))
|
|
||||||
iconPickerListener?.iconPicked(bundle)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() }
|
|
||||||
|
|
||||||
return builder.create()
|
|
||||||
}
|
|
||||||
return super.onCreateDialog(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ImageAdapter internal constructor(private val context: Context) : BaseAdapter() {
|
|
||||||
|
|
||||||
override fun getCount(): Int {
|
|
||||||
return iconPack?.numberOfIcons() ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItem(position: Int): Any? {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
|
||||||
val currentView: View = convertView
|
|
||||||
?: (context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)
|
|
||||||
.inflate(R.layout.item_icon, parent, false)
|
|
||||||
|
|
||||||
iconPack?.let { iconPack ->
|
|
||||||
val iconImageView = currentView.findViewById<ImageView>(R.id.icon_image)
|
|
||||||
iconImageView.setImageResource(iconPack.iconToResId(position))
|
|
||||||
|
|
||||||
// Assign color if icons are tintable
|
|
||||||
if (iconPack.tintable()) {
|
|
||||||
// Retrieve the textColor to tint the icon
|
|
||||||
val ta = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
|
||||||
ImageViewCompat.setImageTintList(iconImageView, ColorStateList.valueOf(ta.getColor(0, Color.BLACK)))
|
|
||||||
ta.recycle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentView
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IconPickerListener {
|
|
||||||
fun iconPicked(bundle: Bundle)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val KEY_ICON_STANDARD = "KEY_ICON_STANDARD"
|
|
||||||
|
|
||||||
fun getIconStandardFromBundle(bundle: Bundle): IconImageStandard? {
|
|
||||||
return bundle.getParcelable(KEY_ICON_STANDARD)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun launch(activity: FragmentActivity) {
|
|
||||||
// Create an instance of the dialog fragment and show it
|
|
||||||
val dialog = IconPickerDialogFragment()
|
|
||||||
dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -26,6 +26,7 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
|
|
||||||
class PasswordEncodingDialogFragment : DialogFragment() {
|
class PasswordEncodingDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
@@ -49,10 +50,7 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
|||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|
||||||
val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY)
|
val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY)
|
||||||
val masterPasswordChecked: Boolean = savedInstanceState?.getBoolean(MASTER_PASSWORD_CHECKED_KEY) ?: false
|
val mainCredential: MainCredential = savedInstanceState?.getParcelable(MAIN_CREDENTIAL) ?: MainCredential()
|
||||||
val masterPassword: String? = savedInstanceState?.getString(MASTER_PASSWORD_KEY)
|
|
||||||
val keyFileChecked: Boolean = savedInstanceState?.getBoolean(KEY_FILE_CHECKED_KEY) ?: false
|
|
||||||
val keyFile: Uri? = savedInstanceState?.getParcelable(KEY_FILE_URI_KEY)
|
|
||||||
|
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
@@ -60,10 +58,7 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
|||||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
mListener?.onPasswordEncodingValidateListener(
|
mListener?.onPasswordEncodingValidateListener(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
masterPasswordChecked,
|
mainCredential
|
||||||
masterPassword,
|
|
||||||
keyFileChecked,
|
|
||||||
keyFile
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
|
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
|
||||||
@@ -75,32 +70,20 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
fun onPasswordEncodingValidateListener(databaseUri: Uri?,
|
fun onPasswordEncodingValidateListener(databaseUri: Uri?,
|
||||||
masterPasswordChecked: Boolean,
|
mainCredential: MainCredential)
|
||||||
masterPassword: String?,
|
|
||||||
keyFileChecked: Boolean,
|
|
||||||
keyFile: Uri?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
|
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
|
||||||
private const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY"
|
private const val MAIN_CREDENTIAL = "MAIN_CREDENTIAL"
|
||||||
private const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY"
|
|
||||||
private const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY"
|
|
||||||
private const val KEY_FILE_URI_KEY = "KEY_FILE_URI_KEY"
|
|
||||||
|
|
||||||
fun getInstance(databaseUri: Uri,
|
fun getInstance(databaseUri: Uri,
|
||||||
masterPasswordChecked: Boolean,
|
mainCredential: MainCredential): SortDialogFragment {
|
||||||
masterPassword: String?,
|
|
||||||
keyFileChecked: Boolean,
|
|
||||||
keyFile: Uri?): SortDialogFragment {
|
|
||||||
val fragment = SortDialogFragment()
|
val fragment = SortDialogFragment()
|
||||||
fragment.arguments = Bundle().apply {
|
fragment.arguments = Bundle().apply {
|
||||||
putParcelable(DATABASE_URI_KEY, databaseUri)
|
putParcelable(DATABASE_URI_KEY, databaseUri)
|
||||||
putBoolean(MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
putParcelable(MAIN_CREDENTIAL, mainCredential)
|
||||||
putString(MASTER_PASSWORD_KEY, masterPassword)
|
|
||||||
putBoolean(KEY_FILE_CHECKED_KEY, keyFileChecked)
|
|
||||||
putParcelable(KEY_FILE_URI_KEY, keyFile)
|
|
||||||
}
|
}
|
||||||
return fragment
|
return fragment
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,10 +29,7 @@ import android.view.MotionEvent
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.AdapterView
|
import android.widget.*
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.Spinner
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
@@ -49,6 +46,7 @@ import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
|
|||||||
import com.kunzisoft.keepass.otp.OtpTokenType
|
import com.kunzisoft.keepass.otp.OtpTokenType
|
||||||
import com.kunzisoft.keepass.otp.OtpType
|
import com.kunzisoft.keepass.otp.OtpType
|
||||||
import com.kunzisoft.keepass.otp.TokenCalculator
|
import com.kunzisoft.keepass.otp.TokenCalculator
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class SetOTPDialogFragment : DialogFragment() {
|
class SetOTPDialogFragment : DialogFragment() {
|
||||||
@@ -57,6 +55,7 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
private var mOtpElement: OtpElement = OtpElement()
|
private var mOtpElement: OtpElement = OtpElement()
|
||||||
|
|
||||||
|
private var otpTypeMessage: TextView? = null
|
||||||
private var otpTypeSpinner: Spinner? = null
|
private var otpTypeSpinner: Spinner? = null
|
||||||
private var otpTokenTypeSpinner: Spinner? = null
|
private var otpTokenTypeSpinner: Spinner? = null
|
||||||
private var otpSecretContainer: TextInputLayout? = null
|
private var otpSecretContainer: TextInputLayout? = null
|
||||||
@@ -74,6 +73,8 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
private var totpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
private var totpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
||||||
private var hotpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
private var hotpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
||||||
private var otpAlgorithmAdapter: ArrayAdapter<TokenCalculator.HashAlgorithm>? = null
|
private var otpAlgorithmAdapter: ArrayAdapter<TokenCalculator.HashAlgorithm>? = null
|
||||||
|
private var mHotpTokenTypeArray: Array<OtpTokenType>? = null
|
||||||
|
private var mTotpTokenTypeArray: Array<OtpTokenType>? = null
|
||||||
|
|
||||||
private var mManualEvent = false
|
private var mManualEvent = false
|
||||||
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
|
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
|
||||||
@@ -134,6 +135,7 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val root = activity.layoutInflater.inflate(R.layout.fragment_set_otp, null) as ViewGroup?
|
val root = activity.layoutInflater.inflate(R.layout.fragment_set_otp, null) as ViewGroup?
|
||||||
|
otpTypeMessage = root?.findViewById(R.id.setup_otp_type_message)
|
||||||
otpTypeSpinner = root?.findViewById(R.id.setup_otp_type)
|
otpTypeSpinner = root?.findViewById(R.id.setup_otp_type)
|
||||||
otpTokenTypeSpinner = root?.findViewById(R.id.setup_otp_token_type)
|
otpTokenTypeSpinner = root?.findViewById(R.id.setup_otp_token_type)
|
||||||
otpSecretContainer = root?.findViewById(R.id.setup_otp_secret_label)
|
otpSecretContainer = root?.findViewById(R.id.setup_otp_secret_label)
|
||||||
@@ -183,23 +185,23 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
// HOTP / TOTP Type selection
|
// HOTP / TOTP Type selection
|
||||||
val otpTypeArray = OtpType.values()
|
val otpTypeArray = OtpType.values()
|
||||||
otpTypeAdapter = ArrayAdapter<OtpType>(activity,
|
otpTypeAdapter = ArrayAdapter(activity,
|
||||||
android.R.layout.simple_spinner_item, otpTypeArray).apply {
|
android.R.layout.simple_spinner_item, otpTypeArray).apply {
|
||||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
}
|
}
|
||||||
otpTypeSpinner?.adapter = otpTypeAdapter
|
otpTypeSpinner?.adapter = otpTypeAdapter
|
||||||
|
|
||||||
// Otp Token type selection
|
// Otp Token type selection
|
||||||
val hotpTokenTypeArray = OtpTokenType.getHotpTokenTypeValues()
|
mHotpTokenTypeArray = OtpTokenType.getHotpTokenTypeValues()
|
||||||
hotpTokenTypeAdapter = ArrayAdapter(activity,
|
hotpTokenTypeAdapter = ArrayAdapter(activity,
|
||||||
android.R.layout.simple_spinner_item, hotpTokenTypeArray).apply {
|
android.R.layout.simple_spinner_item, mHotpTokenTypeArray!!).apply {
|
||||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
}
|
}
|
||||||
// Proprietary only on closed and full version
|
// Proprietary only on closed and full version
|
||||||
val totpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
|
mTotpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
|
||||||
BuildConfig.CLOSED_STORE && BuildConfig.FULL_VERSION)
|
BuildConfig.CLOSED_STORE && BuildConfig.FULL_VERSION)
|
||||||
totpTokenTypeAdapter = ArrayAdapter(activity,
|
totpTokenTypeAdapter = ArrayAdapter(activity,
|
||||||
android.R.layout.simple_spinner_item, totpTokenTypeArray).apply {
|
android.R.layout.simple_spinner_item, mTotpTokenTypeArray!!).apply {
|
||||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
}
|
}
|
||||||
otpTokenTypeAdapter = hotpTokenTypeAdapter
|
otpTokenTypeAdapter = hotpTokenTypeAdapter
|
||||||
@@ -207,7 +209,7 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
// OTP Algorithm
|
// OTP Algorithm
|
||||||
val otpAlgorithmArray = TokenCalculator.HashAlgorithm.values()
|
val otpAlgorithmArray = TokenCalculator.HashAlgorithm.values()
|
||||||
otpAlgorithmAdapter = ArrayAdapter<TokenCalculator.HashAlgorithm>(activity,
|
otpAlgorithmAdapter = ArrayAdapter(activity,
|
||||||
android.R.layout.simple_spinner_item, otpAlgorithmArray).apply {
|
android.R.layout.simple_spinner_item, otpAlgorithmArray).apply {
|
||||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
}
|
}
|
||||||
@@ -222,13 +224,16 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
builder.apply {
|
builder.apply {
|
||||||
setTitle(R.string.entry_setup_otp)
|
|
||||||
setView(root)
|
setView(root)
|
||||||
.setPositiveButton(android.R.string.ok) {_, _ -> }
|
.setPositiveButton(android.R.string.ok) {_, _ -> }
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
.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 builder.create()
|
||||||
}
|
}
|
||||||
return super.onCreateDialog(savedInstanceState)
|
return super.onCreateDialog(savedInstanceState)
|
||||||
@@ -372,24 +377,40 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun upgradeTokenType() {
|
private fun upgradeTokenType() {
|
||||||
|
val tokenType = mOtpElement.tokenType
|
||||||
when (mOtpElement.type) {
|
when (mOtpElement.type) {
|
||||||
OtpType.HOTP -> {
|
OtpType.HOTP -> {
|
||||||
otpPeriodContainer?.visibility = View.GONE
|
otpPeriodContainer?.visibility = View.GONE
|
||||||
otpCounterContainer?.visibility = View.VISIBLE
|
otpCounterContainer?.visibility = View.VISIBLE
|
||||||
otpTokenTypeSpinner?.adapter = hotpTokenTypeAdapter
|
otpTokenTypeSpinner?.adapter = hotpTokenTypeAdapter
|
||||||
otpTokenTypeSpinner?.setSelection(OtpTokenType
|
mHotpTokenTypeArray?.let { otpTokenTypeArray ->
|
||||||
.getHotpTokenTypeValues().indexOf(mOtpElement.tokenType))
|
defineOtpTokenTypeSpinner(otpTokenTypeArray, tokenType, OtpTokenType.RFC4226)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
OtpType.TOTP -> {
|
OtpType.TOTP -> {
|
||||||
otpPeriodContainer?.visibility = View.VISIBLE
|
otpPeriodContainer?.visibility = View.VISIBLE
|
||||||
otpCounterContainer?.visibility = View.GONE
|
otpCounterContainer?.visibility = View.GONE
|
||||||
otpTokenTypeSpinner?.adapter = totpTokenTypeAdapter
|
otpTokenTypeSpinner?.adapter = totpTokenTypeAdapter
|
||||||
otpTokenTypeSpinner?.setSelection(OtpTokenType
|
mTotpTokenTypeArray?.let { otpTokenTypeArray ->
|
||||||
.getTotpTokenTypeValues().indexOf(mOtpElement.tokenType))
|
defineOtpTokenTypeSpinner(otpTokenTypeArray, tokenType, OtpTokenType.RFC6238)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun defineOtpTokenTypeSpinner(otpTokenTypeArray: Array<OtpTokenType>,
|
||||||
|
tokenType: OtpTokenType,
|
||||||
|
defaultTokenType: OtpTokenType) {
|
||||||
|
val formTokenType = if (otpTokenTypeArray.contains(tokenType)) {
|
||||||
|
otpTypeMessage?.visibility = View.GONE
|
||||||
|
tokenType
|
||||||
|
} else {
|
||||||
|
otpTypeMessage?.visibility = View.VISIBLE
|
||||||
|
defaultTokenType
|
||||||
|
}
|
||||||
|
otpTokenTypeSpinner?.setSelection(otpTokenTypeArray.indexOf(formTokenType))
|
||||||
|
}
|
||||||
|
|
||||||
private fun upgradeParameters() {
|
private fun upgradeParameters() {
|
||||||
otpAlgorithmSpinner?.setSelection(TokenCalculator.HashAlgorithm.values()
|
otpAlgorithmSpinner?.setSelection(TokenCalculator.HashAlgorithm.values()
|
||||||
.indexOf(mOtpElement.algorithm))
|
.indexOf(mOtpElement.algorithm))
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
@@ -26,29 +26,29 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.CompoundButton
|
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.EntryEditActivity
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
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.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
|
||||||
import com.kunzisoft.keepass.model.*
|
import com.kunzisoft.keepass.model.*
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.view.ExpirationView
|
||||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||||
import com.kunzisoft.keepass.view.collapse
|
import com.kunzisoft.keepass.view.collapse
|
||||||
import com.kunzisoft.keepass.view.expand
|
import com.kunzisoft.keepass.view.expand
|
||||||
@@ -63,8 +63,7 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
private lateinit var entryPasswordLayoutView: TextInputLayout
|
private lateinit var entryPasswordLayoutView: TextInputLayout
|
||||||
private lateinit var entryPasswordView: EditText
|
private lateinit var entryPasswordView: EditText
|
||||||
private lateinit var entryPasswordGeneratorView: View
|
private lateinit var entryPasswordGeneratorView: View
|
||||||
private lateinit var entryExpiresCheckBox: CompoundButton
|
private lateinit var entryExpirationView: ExpirationView
|
||||||
private lateinit var entryExpiresTextView: TextView
|
|
||||||
private lateinit var entryNotesView: EditText
|
private lateinit var entryNotesView: EditText
|
||||||
private lateinit var extraFieldsContainerView: View
|
private lateinit var extraFieldsContainerView: View
|
||||||
private lateinit var extraFieldsListView: ViewGroup
|
private lateinit var extraFieldsListView: ViewGroup
|
||||||
@@ -75,12 +74,11 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
|
|
||||||
private var fontInVisibility: Boolean = false
|
private var fontInVisibility: Boolean = false
|
||||||
private var iconColor: Int = 0
|
private var iconColor: Int = 0
|
||||||
private var expiresInstant: DateInstant = DateInstant.IN_ONE_MONTH
|
|
||||||
|
|
||||||
var drawFactory: IconDrawableFactory? = null
|
var drawFactory: IconDrawableFactory? = null
|
||||||
var setOnDateClickListener: View.OnClickListener? = null
|
var setOnDateClickListener: (() -> Unit)? = null
|
||||||
var setOnPasswordGeneratorClickListener: View.OnClickListener? = null
|
var setOnPasswordGeneratorClickListener: View.OnClickListener? = null
|
||||||
var setOnIconViewClickListener: View.OnClickListener? = null
|
var setOnIconViewClickListener: ((IconImage) -> Unit)? = null
|
||||||
var setOnEditCustomField: ((Field) -> Unit)? = null
|
var setOnEditCustomField: ((Field) -> Unit)? = null
|
||||||
var setOnRemoveAttachment: ((Attachment) -> Unit)? = null
|
var setOnRemoveAttachment: ((Attachment) -> Unit)? = null
|
||||||
|
|
||||||
@@ -101,7 +99,7 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
entryTitleView = rootView.findViewById(R.id.entry_edit_title)
|
entryTitleView = rootView.findViewById(R.id.entry_edit_title)
|
||||||
entryIconView = rootView.findViewById(R.id.entry_edit_icon_button)
|
entryIconView = rootView.findViewById(R.id.entry_edit_icon_button)
|
||||||
entryIconView.setOnClickListener {
|
entryIconView.setOnClickListener {
|
||||||
setOnIconViewClickListener?.onClick(it)
|
setOnIconViewClickListener?.invoke(mEntryInfo.icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
entryUserNameView = rootView.findViewById(R.id.entry_edit_user_name)
|
entryUserNameView = rootView.findViewById(R.id.entry_edit_user_name)
|
||||||
@@ -112,12 +110,8 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
entryPasswordGeneratorView.setOnClickListener {
|
entryPasswordGeneratorView.setOnClickListener {
|
||||||
setOnPasswordGeneratorClickListener?.onClick(it)
|
setOnPasswordGeneratorClickListener?.onClick(it)
|
||||||
}
|
}
|
||||||
entryExpiresCheckBox = rootView.findViewById(R.id.entry_edit_expires_checkbox)
|
entryExpirationView = rootView.findViewById(R.id.entry_edit_expiration)
|
||||||
entryExpiresTextView = rootView.findViewById(R.id.entry_edit_expires_text)
|
entryExpirationView.setOnDateClickListener = setOnDateClickListener
|
||||||
entryExpiresTextView.setOnClickListener {
|
|
||||||
if (entryExpiresCheckBox.isChecked)
|
|
||||||
setOnDateClickListener?.onClick(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
entryNotesView = rootView.findViewById(R.id.entry_edit_notes)
|
entryNotesView = rootView.findViewById(R.id.entry_edit_notes)
|
||||||
|
|
||||||
@@ -127,6 +121,9 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
|
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
|
||||||
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
|
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
|
||||||
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
|
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 ->
|
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
|
||||||
if (previousSize > 0 && newSize == 0) {
|
if (previousSize > 0 && newSize == 0) {
|
||||||
attachmentsContainerView.collapse(true)
|
attachmentsContainerView.collapse(true)
|
||||||
@@ -140,10 +137,6 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
}
|
}
|
||||||
|
|
||||||
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
|
|
||||||
assignExpiresDateText()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
// Retrieve the textColor to tint the icon
|
||||||
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
|
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
|
||||||
@@ -178,7 +171,7 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
setOnEditCustomField = null
|
setOnEditCustomField = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEntryInfo(): EntryInfo? {
|
fun getEntryInfo(): EntryInfo {
|
||||||
populateEntryWithViews()
|
populateEntryWithViews()
|
||||||
return mEntryInfo
|
return mEntryInfo
|
||||||
}
|
}
|
||||||
@@ -247,9 +240,7 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
mEntryInfo.icon = value
|
mEntryInfo.icon = value
|
||||||
drawFactory?.let { drawFactory ->
|
drawFactory?.assignDatabaseIcon(entryIconView, value, iconColor)
|
||||||
entryIconView.assignDatabaseIcon(drawFactory, value, iconColor)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var username: String
|
var username: String
|
||||||
@@ -283,41 +274,20 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assignExpiresDateText() {
|
|
||||||
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
|
|
||||||
entryExpiresTextView.setOnClickListener(setOnDateClickListener)
|
|
||||||
expiresInstant.getDateTimeString(resources)
|
|
||||||
} else {
|
|
||||||
entryExpiresTextView.setOnClickListener(null)
|
|
||||||
resources.getString(R.string.never)
|
|
||||||
}
|
|
||||||
if (fontInVisibility)
|
|
||||||
entryExpiresTextView.applyFontVisibility()
|
|
||||||
}
|
|
||||||
|
|
||||||
var expires: Boolean
|
var expires: Boolean
|
||||||
get() {
|
get() {
|
||||||
return entryExpiresCheckBox.isChecked
|
return entryExpirationView.expires
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
if (!value) {
|
entryExpirationView.expires = value
|
||||||
expiresInstant = DateInstant.IN_ONE_MONTH
|
|
||||||
}
|
|
||||||
entryExpiresCheckBox.isChecked = value
|
|
||||||
assignExpiresDateText()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var expiryTime: DateInstant
|
var expiryTime: DateInstant
|
||||||
get() {
|
get() {
|
||||||
return if (expires)
|
return entryExpirationView.expiryTime
|
||||||
expiresInstant
|
|
||||||
else
|
|
||||||
DateInstant.NEVER_EXPIRE
|
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
if (expires)
|
entryExpirationView.expiryTime = value
|
||||||
expiresInstant = value
|
|
||||||
assignExpiresDateText()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var notes: String
|
var notes: String
|
||||||
@@ -344,7 +314,8 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
itemView?.id = View.NO_ID
|
itemView?.id = View.NO_ID
|
||||||
|
|
||||||
val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container)
|
val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container)
|
||||||
extraFieldValueContainer?.isPasswordVisibilityToggleEnabled = extraField.protectedValue.isProtected
|
extraFieldValueContainer?.endIconMode = if (extraField.protectedValue.isProtected)
|
||||||
|
TextInputLayout.END_ICON_PASSWORD_TOGGLE else TextInputLayout.END_ICON_NONE
|
||||||
extraFieldValueContainer?.hint = extraField.name
|
extraFieldValueContainer?.hint = extraField.name
|
||||||
extraFieldValueContainer?.id = View.NO_ID
|
extraFieldValueContainer?.id = View.NO_ID
|
||||||
|
|
||||||
@@ -502,9 +473,13 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
return attachmentsAdapter.contains(attachment)
|
return attachmentsAdapter.contains(attachment)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun putAttachment(attachment: EntryAttachmentState) {
|
fun putAttachment(attachment: EntryAttachmentState,
|
||||||
|
onPreviewLoaded: (()-> Unit)? = null) {
|
||||||
attachmentsContainerView.visibility = View.VISIBLE
|
attachmentsContainerView.visibility = View.VISIBLE
|
||||||
attachmentsAdapter.putItem(attachment)
|
attachmentsAdapter.putItem(attachment)
|
||||||
|
attachmentsAdapter.onBinaryPreviewLoaded = {
|
||||||
|
onPreviewLoaded?.invoke()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAttachment(attachment: EntryAttachmentState) {
|
fun removeAttachment(attachment: EntryAttachmentState) {
|
||||||
@@ -535,12 +510,16 @@ class EntryEditFragment: StylishFragment() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
|
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"
|
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
|
||||||
|
|
||||||
fun getInstance(entryInfo: EntryInfo?): EntryEditFragment {
|
fun getInstance(entryInfo: EntryInfo?): EntryEditFragment {
|
||||||
|
//database: Database?): EntryEditFragment {
|
||||||
return EntryEditFragment().apply {
|
return EntryEditFragment().apply {
|
||||||
arguments = Bundle().apply {
|
arguments = Bundle().apply {
|
||||||
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePassDX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
@@ -17,35 +17,27 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.stream
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
import java.io.IOException
|
import com.kunzisoft.keepass.R
|
||||||
import java.io.OutputStream
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
|
|
||||||
class NullOutputStream : OutputStream() {
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
class IconStandardFragment : IconFragment<IconImageStandard>() {
|
||||||
override fun close() {
|
|
||||||
super.close()
|
override fun retrieveMainLayoutId(): Int {
|
||||||
|
return R.layout.fragment_icon_grid
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
override fun defineIconList() {
|
||||||
override fun flush() {
|
mDatabase?.doForEachStandardIcons { standardIcon ->
|
||||||
super.flush()
|
iconPickerAdapter.addIcon(standardIcon, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
override fun onIconClickListener(icon: IconImageStandard) {
|
||||||
override fun write(buffer: ByteArray, offset: Int, count: Int) {
|
iconPickerViewModel.pickStandardIcon(icon)
|
||||||
super.write(buffer, offset, count)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
override fun onIconLongClickListener(icon: IconImageStandard) {}
|
||||||
override fun write(buffer: ByteArray) {
|
}
|
||||||
super.write(buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun write(oneByte: Int) {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -28,6 +28,7 @@ import androidx.appcompat.view.ActionMode
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.EntryEditActivity
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
@@ -310,13 +311,17 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
menu?.removeItem(R.id.menu_edit)
|
menu?.removeItem(R.id.menu_edit)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy and Move (not for groups)
|
// Move
|
||||||
|
if (readOnly
|
||||||
|
|| isASearchResult) {
|
||||||
|
menu?.removeItem(R.id.menu_move)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy (not allowed for group)
|
||||||
if (readOnly
|
if (readOnly
|
||||||
|| isASearchResult
|
|| isASearchResult
|
||||||
|| nodes.any { it.type == Type.GROUP }) {
|
|| nodes.any { it.type == Type.GROUP }) {
|
||||||
// TODO Copy For Group
|
|
||||||
menu?.removeItem(R.id.menu_copy)
|
menu?.removeItem(R.id.menu_copy)
|
||||||
menu?.removeItem(R.id.menu_move)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deletion
|
// Deletion
|
||||||
@@ -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,244 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.activities.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.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
|
||||||
|
|
||||||
class SelectFileHelper {
|
|
||||||
|
|
||||||
private var activity: Activity? = null
|
|
||||||
private var fragment: Fragment? = null
|
|
||||||
|
|
||||||
val selectFileOnClickViewListener: SelectFileOnClickViewListener
|
|
||||||
get() = SelectFileOnClickViewListener()
|
|
||||||
|
|
||||||
constructor(context: Activity) {
|
|
||||||
this.activity = context
|
|
||||||
this.fragment = null
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context: Fragment) {
|
|
||||||
this.activity = context.activity
|
|
||||||
this.fragment = context
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class SelectFileOnClickViewListener :
|
|
||||||
View.OnClickListener,
|
|
||||||
View.OnLongClickListener,
|
|
||||||
MenuItem.OnMenuItemClickListener {
|
|
||||||
|
|
||||||
private fun onAbstractClick(longClick: Boolean = false) {
|
|
||||||
try {
|
|
||||||
if (longClick) {
|
|
||||||
try {
|
|
||||||
openActivityWithActionGetContent()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
openActivityWithActionOpenDocument()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
|
||||||
onAbstractClick()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLongClick(v: View?): Boolean {
|
|
||||||
onAbstractClick(true)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
|
||||||
onAbstractClick()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
private fun openActivityWithActionOpenDocument() {
|
|
||||||
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
type = "*/*"
|
|
||||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
private fun openActivityWithActionGetContent() {
|
|
||||||
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
type = "*/*"
|
|
||||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 fileManagerDialogFragment = FileManagerDialogFragment()
|
|
||||||
fragment?.let {
|
|
||||||
fileManagerDialogFragment.show(it.parentFragmentManager, "browserDialog")
|
|
||||||
} ?: fileManagerDialogFragment.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"
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -60,6 +60,9 @@ abstract class LockingActivity : SpecialModeActivity() {
|
|||||||
private set
|
private set
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
||||||
|
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this)
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
if (savedInstanceState != null
|
if (savedInstanceState != null
|
||||||
@@ -84,8 +87,6 @@ abstract class LockingActivity : SpecialModeActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mExitLock = false
|
mExitLock = false
|
||||||
|
|
||||||
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
|||||||
@@ -20,11 +20,11 @@
|
|||||||
package com.kunzisoft.keepass.activities.stylish
|
package com.kunzisoft.keepass.activities.stylish
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.StyleRes
|
import android.content.res.Configuration
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.annotation.StyleRes
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that provides functions to retrieve and assign a theme to a module
|
* 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
|
* Initialize the class with a theme preference
|
||||||
* @param context Context to retrieve the theme preference
|
* @param context Context to retrieve the theme preference
|
||||||
*/
|
*/
|
||||||
fun init(context: Context) {
|
fun load(context: Context) {
|
||||||
val stylishPrefKey = context.getString(R.string.setting_style_key)
|
|
||||||
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
|
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
|
* Assign the style to the class attribute
|
||||||
* @param styleString Style id String
|
* @param styleString Style id String
|
||||||
*/
|
*/
|
||||||
fun assignStyle(styleString: String) {
|
fun assignStyle(context: Context, styleString: String) {
|
||||||
themeString = styleString
|
PreferencesUtil.setStyle(context, styleString)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,14 +103,18 @@ object Stylish {
|
|||||||
*/
|
*/
|
||||||
@StyleRes
|
@StyleRes
|
||||||
fun getThemeId(context: Context): Int {
|
fun getThemeId(context: Context): Int {
|
||||||
|
return when (retrieveEquivalentSystemStyle(context, themeString ?: context.getString(R.string.list_style_name_light))) {
|
||||||
return when (themeString) {
|
|
||||||
context.getString(R.string.list_style_name_night) -> R.style.KeepassDXStyle_Night
|
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_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_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) -> 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) -> 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) -> R.style.KeepassDXStyle_Purple
|
||||||
|
context.getString(R.string.list_style_name_purple_dark) -> R.style.KeepassDXStyle_Purple_Dark
|
||||||
else -> R.style.KeepassDXStyle_Light
|
else -> R.style.KeepassDXStyle_Light
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ abstract class StylishFragment : Fragment() {
|
|||||||
contextThemed = ContextThemeWrapper(context, themeId)
|
contextThemed = ContextThemeWrapper(context, themeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
// To fix status bar color
|
// To fix status bar color
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
@@ -53,14 +54,21 @@ abstract class StylishFragment : Fragment() {
|
|||||||
window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor
|
window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor
|
||||||
taStatusBarColor?.recycle()
|
taStatusBarColor?.recycle()
|
||||||
} catch (e: Exception) {}
|
} 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 {
|
try {
|
||||||
val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor))
|
val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor))
|
||||||
window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor
|
window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor
|
||||||
taNavigationBarColor?.recycle()
|
taNavigationBarColor?.recycle()
|
||||||
} catch (e: Exception) {}
|
} catch (e: Exception) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onCreateView(inflater, container, savedInstanceState)
|
return super.onCreateView(inflater, container, savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,9 @@ abstract class AnimatedItemsAdapter<Item, T: RecyclerView.ViewHolder>(val contex
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
itemsList.clear()
|
if (itemsList.size > 0) {
|
||||||
notifyDataSetChanged()
|
itemsList.clear()
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,17 +31,29 @@ import android.widget.ProgressBar
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.kunzisoft.keepass.R
|
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.database.element.database.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.model.AttachmentState
|
import com.kunzisoft.keepass.model.AttachmentState
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.StreamDirection
|
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)
|
class EntryAttachmentsItemsAdapter(context: Context)
|
||||||
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
|
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
|
||||||
|
|
||||||
|
var database: Database? = null
|
||||||
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = 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
|
private var mTitleColor: Int
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -62,24 +74,62 @@ class EntryAttachmentsItemsAdapter(context: Context)
|
|||||||
val entryAttachmentState = itemsList[position]
|
val entryAttachmentState = itemsList[position]
|
||||||
|
|
||||||
holder.itemView.visibility = View.VISIBLE
|
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 {
|
holder.binaryFileBroken.apply {
|
||||||
setColorFilter(Color.RED)
|
setColorFilter(Color.RED)
|
||||||
visibility = if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) {
|
visibility = if (entryAttachmentState.attachment.binaryData.isCorrupted) {
|
||||||
View.VISIBLE
|
View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
View.GONE
|
View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
holder.binaryFileTitle.text = entryAttachmentState.attachment.name
|
holder.binaryFileTitle.text = entryAttachmentState.attachment.name
|
||||||
if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) {
|
if (entryAttachmentState.attachment.binaryData.isCorrupted) {
|
||||||
holder.binaryFileTitle.setTextColor(Color.RED)
|
holder.binaryFileTitle.setTextColor(Color.RED)
|
||||||
} else {
|
} else {
|
||||||
holder.binaryFileTitle.setTextColor(mTitleColor)
|
holder.binaryFileTitle.setTextColor(mTitleColor)
|
||||||
}
|
}
|
||||||
holder.binaryFileSize.text = Formatter.formatFileSize(context,
|
|
||||||
entryAttachmentState.attachment.binaryAttachment.length())
|
val size = entryAttachmentState.attachment.binaryData.getSize()
|
||||||
|
holder.binaryFileSize.text = Formatter.formatFileSize(context, size)
|
||||||
holder.binaryFileCompression.apply {
|
holder.binaryFileCompression.apply {
|
||||||
if (entryAttachmentState.attachment.binaryAttachment.isCompressed) {
|
if (entryAttachmentState.attachment.binaryData.isCompressed) {
|
||||||
text = CompressionAlgorithm.GZip.getName(context.resources)
|
text = CompressionAlgorithm.GZip.getName(context.resources)
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
@@ -105,6 +155,7 @@ class EntryAttachmentsItemsAdapter(context: Context)
|
|||||||
}
|
}
|
||||||
AttachmentState.NULL,
|
AttachmentState.NULL,
|
||||||
AttachmentState.ERROR,
|
AttachmentState.ERROR,
|
||||||
|
AttachmentState.CANCELED,
|
||||||
AttachmentState.COMPLETE -> {
|
AttachmentState.COMPLETE -> {
|
||||||
holder.binaryFileProgressContainer.visibility = View.GONE
|
holder.binaryFileProgressContainer.visibility = View.GONE
|
||||||
holder.binaryFileProgress.visibility = View.GONE
|
holder.binaryFileProgress.visibility = View.GONE
|
||||||
@@ -114,7 +165,7 @@ class EntryAttachmentsItemsAdapter(context: Context)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
holder.itemView.setOnClickListener(null)
|
holder.binaryFileInfo.setOnClickListener(null)
|
||||||
}
|
}
|
||||||
StreamDirection.DOWNLOAD -> {
|
StreamDirection.DOWNLOAD -> {
|
||||||
holder.binaryFileProgressIcon.isActivated = false
|
holder.binaryFileProgressIcon.isActivated = false
|
||||||
@@ -122,12 +173,17 @@ class EntryAttachmentsItemsAdapter(context: Context)
|
|||||||
holder.binaryFileDeleteButton.visibility = View.GONE
|
holder.binaryFileDeleteButton.visibility = View.GONE
|
||||||
holder.binaryFileProgress.apply {
|
holder.binaryFileProgress.apply {
|
||||||
visibility = when (entryAttachmentState.downloadState) {
|
visibility = when (entryAttachmentState.downloadState) {
|
||||||
AttachmentState.NULL, AttachmentState.COMPLETE, AttachmentState.ERROR -> View.GONE
|
AttachmentState.NULL,
|
||||||
AttachmentState.START, AttachmentState.IN_PROGRESS -> View.VISIBLE
|
AttachmentState.COMPLETE,
|
||||||
|
AttachmentState.CANCELED,
|
||||||
|
AttachmentState.ERROR -> View.GONE
|
||||||
|
|
||||||
|
AttachmentState.START,
|
||||||
|
AttachmentState.IN_PROGRESS -> View.VISIBLE
|
||||||
}
|
}
|
||||||
progress = entryAttachmentState.downloadProgression
|
progress = entryAttachmentState.downloadProgression
|
||||||
}
|
}
|
||||||
holder.itemView.setOnClickListener {
|
holder.binaryFileInfo.setOnClickListener {
|
||||||
onItemClickListener?.invoke(entryAttachmentState)
|
onItemClickListener?.invoke(entryAttachmentState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,6 +192,8 @@ class EntryAttachmentsItemsAdapter(context: Context)
|
|||||||
|
|
||||||
class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
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 binaryFileBroken: ImageView = itemView.findViewById(R.id.item_attachment_broken)
|
||||||
var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title)
|
var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title)
|
||||||
var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size)
|
var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size)
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
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 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)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import android.view.ViewGroup
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SortedList
|
import androidx.recyclerview.widget.SortedList
|
||||||
import androidx.recyclerview.widget.SortedListAdapterCallback
|
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||||
@@ -39,7 +40,6 @@ import com.kunzisoft.keepass.database.element.SortNodeEnum
|
|||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.setTextSize
|
import com.kunzisoft.keepass.view.setTextSize
|
||||||
import com.kunzisoft.keepass.view.strikeOut
|
import com.kunzisoft.keepass.view.strikeOut
|
||||||
@@ -57,7 +57,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
private val mNodeSortedList: SortedList<Node>
|
private val mNodeSortedList: SortedList<Node>
|
||||||
private val mInflater: LayoutInflater = LayoutInflater.from(context)
|
private val mInflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
|
|
||||||
private var mCalculateViewTypeTextSize = Array(2) { true} // number of view type
|
private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type
|
||||||
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
||||||
private var mPrefSizeMultiplier: Float = 0F
|
private var mPrefSizeMultiplier: Float = 0F
|
||||||
private var mSubtextDefaultDimension: Float = 0F
|
private var mSubtextDefaultDimension: Float = 0F
|
||||||
@@ -100,9 +100,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
this.mDatabase = Database.getInstance()
|
this.mDatabase = Database.getInstance()
|
||||||
|
|
||||||
// Color of content selection
|
// Color of content selection
|
||||||
val taContentSelectionColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white)
|
||||||
this.mContentSelectionColor = taContentSelectionColor.getColor(0, Color.WHITE)
|
|
||||||
taContentSelectionColor.recycle()
|
|
||||||
// Retrieve the color to tint the icon
|
// Retrieve the color to tint the icon
|
||||||
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
||||||
this.mIconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
|
this.mIconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
|
||||||
@@ -305,7 +303,7 @@ class NodeAdapter (private val context: Context)
|
|||||||
}
|
}
|
||||||
holder.imageIdentifier?.setColorFilter(iconColor)
|
holder.imageIdentifier?.setColorFilter(iconColor)
|
||||||
holder.icon.apply {
|
holder.icon.apply {
|
||||||
assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
|
mDatabase.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor)
|
||||||
// Relative size of the icon
|
// Relative size of the icon
|
||||||
layoutParams?.apply {
|
layoutParams?.apply {
|
||||||
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
|
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import com.kunzisoft.keepass.database.element.Group
|
|||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.strikeOut
|
import com.kunzisoft.keepass.view.strikeOut
|
||||||
|
|
||||||
@@ -81,10 +80,9 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
val viewHolder = view.tag as ViewHolder
|
val viewHolder = view.tag as ViewHolder
|
||||||
|
|
||||||
// Assign image
|
// Assign image
|
||||||
viewHolder.imageViewIcon?.assignDatabaseIcon(
|
viewHolder.imageViewIcon?.let { iconView ->
|
||||||
database.drawFactory,
|
database.iconDrawableFactory.assignDatabaseIcon(iconView, currentEntry.icon, iconColor)
|
||||||
currentEntry.icon,
|
}
|
||||||
iconColor)
|
|
||||||
|
|
||||||
// Assign title
|
// Assign title
|
||||||
viewHolder.textViewTitle?.apply {
|
viewHolder.textViewTitle?.apply {
|
||||||
@@ -110,10 +108,24 @@ class SearchEntryCursorAdapter(private val context: Context,
|
|||||||
return database.createEntry()?.apply {
|
return database.createEntry()?.apply {
|
||||||
database.startManageEntry(this)
|
database.startManageEntry(this)
|
||||||
entryKDB?.let { entryKDB ->
|
entryKDB?.let { entryKDB ->
|
||||||
(cursor as EntryCursorKDB).populateEntry(entryKDB, database.iconFactory)
|
(cursor as EntryCursorKDB).populateEntry(entryKDB,
|
||||||
|
{ standardIconId ->
|
||||||
|
database.getStandardIcon(standardIconId)
|
||||||
|
},
|
||||||
|
{ customIconId ->
|
||||||
|
database.getCustomIcon(customIconId)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
entryKDBX?.let { entryKDBX ->
|
entryKDBX?.let { entryKDBX ->
|
||||||
(cursor as EntryCursorKDBX).populateEntry(entryKDBX, database.iconFactory)
|
(cursor as EntryCursorKDBX).populateEntry(entryKDBX,
|
||||||
|
{ standardIconId ->
|
||||||
|
database.getStandardIcon(standardIconId)
|
||||||
|
},
|
||||||
|
{ customIconId ->
|
||||||
|
database.getCustomIcon(customIconId)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
database.stopManageEntry(this)
|
database.stopManageEntry(this)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class App : MultiDexApplication() {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
Stylish.init(this)
|
Stylish.load(this)
|
||||||
PRNGFixes.apply()
|
PRNGFixes.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ import android.content.Intent
|
|||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService
|
import android.util.Log
|
||||||
|
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -76,7 +77,11 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
mServiceConnection!!,
|
mServiceConnection!!,
|
||||||
Context.BIND_ABOVE_CLIENT)
|
Context.BIND_ABOVE_CLIENT)
|
||||||
if (mBinder == null) {
|
if (mBinder == null) {
|
||||||
applicationContext.startService(mIntentAdvancedUnlockService)
|
try {
|
||||||
|
applicationContext.startService(mIntentAdvancedUnlockService)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to start cipher action", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,5 +178,7 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction)
|
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction) {
|
||||||
|
private val TAG = CipherDatabaseAction::class.java.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,12 @@ class IOActionTask<T>(
|
|||||||
mainScope.launch {
|
mainScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val asyncResult: Deferred<T?> = async {
|
val asyncResult: Deferred<T?> = async {
|
||||||
action.invoke()
|
try {
|
||||||
|
action.invoke()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
afterActionDatabaseListener?.invoke(asyncResult.await())
|
afterActionDatabaseListener?.invoke(asyncResult.await())
|
||||||
|
|||||||
@@ -46,8 +46,6 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
|
||||||
import com.kunzisoft.keepass.icons.createIconFromDatabaseIcon
|
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||||
@@ -64,8 +62,12 @@ object AutofillHelper {
|
|||||||
|
|
||||||
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
|
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
|
||||||
intent?.getParcelableExtra<AssistStructure?>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
|
intent?.getParcelableExtra<AssistStructure?>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
|
||||||
return AutofillComponent(assistStructure,
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
intent.getParcelableExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST))
|
AutofillComponent(assistStructure,
|
||||||
|
intent.getParcelableExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST))
|
||||||
|
} else {
|
||||||
|
AutofillComponent(assistStructure, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -82,6 +84,24 @@ object AutofillHelper {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun newRemoteViews(context: Context,
|
||||||
|
remoteViewsText: String,
|
||||||
|
remoteViewsIcon: IconImage? = null): RemoteViews {
|
||||||
|
val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry)
|
||||||
|
presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
|
||||||
|
if (remoteViewsIcon != null) {
|
||||||
|
try {
|
||||||
|
Database.getInstance().iconDrawableFactory.getBitmapFromIcon(context,
|
||||||
|
remoteViewsIcon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
||||||
|
presentation.setImageViewBitmap(R.id.autofill_entry_icon, bitmap)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return presentation
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildDataset(context: Context,
|
private fun buildDataset(context: Context,
|
||||||
entryInfo: EntryInfo,
|
entryInfo: EntryInfo,
|
||||||
struct: StructureParser.Result,
|
struct: StructureParser.Result,
|
||||||
@@ -112,6 +132,21 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to assign a drawable to a new icon from a database icon
|
||||||
|
*/
|
||||||
|
private fun buildIconFromEntry(context: Context, entryInfo: EntryInfo): Icon? {
|
||||||
|
try {
|
||||||
|
Database.getInstance().iconDrawableFactory.getBitmapFromIcon(context,
|
||||||
|
entryInfo.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
||||||
|
return Icon.createWithBitmap(bitmap)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
private fun buildInlinePresentationForEntry(context: Context,
|
private fun buildInlinePresentationForEntry(context: Context,
|
||||||
@@ -205,10 +240,14 @@ object AutofillHelper {
|
|||||||
activity.intent?.getParcelableExtra<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { structure ->
|
activity.intent?.getParcelableExtra<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { structure ->
|
||||||
StructureParser(structure).parse()?.let { result ->
|
StructureParser(structure).parse()?.let { result ->
|
||||||
// New Response
|
// New Response
|
||||||
val inlineSuggestionsRequest = activity.intent?.getParcelableExtra<InlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
val response = buildResponse(activity, entriesInfo, result, inlineSuggestionsRequest)
|
val inlineSuggestionsRequest = activity.intent?.getParcelableExtra<InlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
||||||
if (inlineSuggestionsRequest != null) {
|
if (inlineSuggestionsRequest != null) {
|
||||||
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
buildResponse(activity, entriesInfo, result, inlineSuggestionsRequest)
|
||||||
|
} else {
|
||||||
|
buildResponse(activity, entriesInfo, result, null)
|
||||||
}
|
}
|
||||||
val mReplyIntent = Intent()
|
val mReplyIntent = Intent()
|
||||||
Log.d(activity.javaClass.name, "Successed Autofill auth.")
|
Log.d(activity.javaClass.name, "Successed Autofill auth.")
|
||||||
@@ -259,26 +298,4 @@ object AutofillHelper {
|
|||||||
activity.finish()
|
activity.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun newRemoteViews(context: Context,
|
|
||||||
remoteViewsText: String,
|
|
||||||
remoteViewsIcon: IconImage? = null): RemoteViews {
|
|
||||||
val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry)
|
|
||||||
presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
|
|
||||||
if (remoteViewsIcon != null) {
|
|
||||||
presentation.assignDatabaseIcon(context,
|
|
||||||
R.id.autofill_entry_icon,
|
|
||||||
Database.getInstance().drawFactory,
|
|
||||||
remoteViewsIcon,
|
|
||||||
ContextCompat.getColor(context, R.color.green))
|
|
||||||
}
|
|
||||||
return presentation
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildIconFromEntry(context: Context, entryInfo: EntryInfo): Icon? {
|
|
||||||
return createIconFromDatabaseIcon(context,
|
|
||||||
Database.getInstance().drawFactory,
|
|
||||||
entryInfo.icon,
|
|
||||||
ContextCompat.getColor(context, R.color.green))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.autofill
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.BlendMode
|
import android.graphics.BlendMode
|
||||||
@@ -130,6 +131,7 @@ class KeeAutofillService : AutofillService() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
||||||
searchInfo: SearchInfo,
|
searchInfo: SearchInfo,
|
||||||
inlineSuggestionsRequest: InlineSuggestionsRequest?,
|
inlineSuggestionsRequest: InlineSuggestionsRequest?,
|
||||||
@@ -174,9 +176,9 @@ class KeeAutofillService : AutofillService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build inline presentation
|
// Build inline presentation
|
||||||
var inlinePresentation: InlinePresentation? = null
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
&& autofillInlineSuggestionsEnabled) {
|
&& autofillInlineSuggestionsEnabled) {
|
||||||
|
var inlinePresentation: InlinePresentation? = null
|
||||||
inlineSuggestionsRequest?.let {
|
inlineSuggestionsRequest?.let {
|
||||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||||
if (inlineSuggestionsRequest.maxSuggestionCount > 0
|
if (inlineSuggestionsRequest.maxSuggestionCount > 0
|
||||||
@@ -203,14 +205,10 @@ class KeeAutofillService : AutofillService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
// Build response
|
||||||
|
|
||||||
// Build response
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
|
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
|
||||||
} else {
|
|
||||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
|
|
||||||
}
|
}
|
||||||
|
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
|
||||||
callback.onSuccess(responseBuilder.build())
|
callback.onSuccess(responseBuilder.build())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,9 +223,22 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
usernameValueCandidate = node.autofillValue
|
usernameValueCandidate = node.autofillValue
|
||||||
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
|
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
|
||||||
}
|
}
|
||||||
|
inputIsVariationType(inputType,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {
|
||||||
|
// Some forms used visible password as username
|
||||||
|
if (usernameCandidate == null && usernameValueCandidate == null) {
|
||||||
|
usernameCandidate = autofillId
|
||||||
|
usernameValueCandidate = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill visible password android text type (as username): ${showHexInputType(inputType)}")
|
||||||
|
} else if (result?.passwordId == null && result?.passwordValue == null) {
|
||||||
|
result?.passwordId = autofillId
|
||||||
|
result?.passwordValue = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill visible password android text type (as password): ${showHexInputType(inputType)}")
|
||||||
|
usernameNeeded = false
|
||||||
|
}
|
||||||
|
}
|
||||||
inputIsVariationType(inputType,
|
inputIsVariationType(inputType,
|
||||||
InputType.TYPE_TEXT_VARIATION_PASSWORD,
|
InputType.TYPE_TEXT_VARIATION_PASSWORD,
|
||||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
|
|
||||||
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) -> {
|
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) -> {
|
||||||
result?.passwordId = autofillId
|
result?.passwordId = autofillId
|
||||||
result?.passwordValue = node.autofillValue
|
result?.passwordValue = node.autofillValue
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
|||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||||
import com.kunzisoft.keepass.database.exception.IODatabaseException
|
import com.kunzisoft.keepass.database.exception.IODatabaseException
|
||||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||||
import com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService
|
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||||
|
|
||||||
@@ -162,8 +162,8 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
disconnect()
|
disconnect()
|
||||||
}
|
}
|
||||||
} ?: run {
|
} ?: run {
|
||||||
connect(databaseUri)
|
|
||||||
this.mAutoOpenPrompt = autoOpenPrompt
|
this.mAutoOpenPrompt = autoOpenPrompt
|
||||||
|
connect(databaseUri)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
disconnect()
|
disconnect()
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.crypto
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import com.kunzisoft.keepass.crypto.engine.AesEngine
|
|
||||||
import com.kunzisoft.keepass.crypto.engine.ChaCha20Engine
|
|
||||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
|
||||||
import com.kunzisoft.keepass.crypto.engine.TwofishEngine
|
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.security.Security
|
|
||||||
import java.util.*
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.NoSuchPaddingException
|
|
||||||
|
|
||||||
object CipherFactory {
|
|
||||||
|
|
||||||
private var blacklistInit = false
|
|
||||||
private var blacklisted: Boolean = false
|
|
||||||
|
|
||||||
init {
|
|
||||||
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
|
|
||||||
Security.addProvider(BouncyCastleProvider())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deviceBlacklisted(): Boolean {
|
|
||||||
if (!blacklistInit) {
|
|
||||||
blacklistInit = true
|
|
||||||
// The Acer Iconia A500 is special and seems to always crash in the native crypto libraries
|
|
||||||
blacklisted = Build.MODEL == "A500"
|
|
||||||
}
|
|
||||||
return blacklisted
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hasNativeImplementation(transformation: String): Boolean {
|
|
||||||
return transformation == "AES/CBC/PKCS5Padding"
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class)
|
|
||||||
fun getInstance(transformation: String, androidOverride: Boolean = false): Cipher {
|
|
||||||
// Return the native AES if it is possible
|
|
||||||
return if (!deviceBlacklisted() && !androidOverride && hasNativeImplementation(transformation) && NativeLib.loaded()) {
|
|
||||||
Cipher.getInstance(transformation, AESProvider())
|
|
||||||
} else {
|
|
||||||
Cipher.getInstance(transformation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate appropriate cipher based on KeePass 2.x UUID's
|
|
||||||
*/
|
|
||||||
@Throws(NoSuchAlgorithmException::class)
|
|
||||||
fun getInstance(uuid: UUID): CipherEngine {
|
|
||||||
return when (uuid) {
|
|
||||||
AesEngine.CIPHER_UUID -> AesEngine()
|
|
||||||
TwofishEngine.CIPHER_UUID -> TwofishEngine()
|
|
||||||
ChaCha20Engine.CIPHER_UUID -> ChaCha20Engine()
|
|
||||||
else -> throw NoSuchAlgorithmException("UUID unrecognized.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.crypto
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.stream.NullOutputStream
|
|
||||||
import com.kunzisoft.keepass.stream.longTo8Bytes
|
|
||||||
import java.io.IOException
|
|
||||||
import java.security.DigestOutputStream
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.util.*
|
|
||||||
import javax.crypto.Mac
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
object CryptoUtil {
|
|
||||||
|
|
||||||
fun resizeKey(inBytes: ByteArray, inOffset: Int, cbIn: Int, cbOut: Int): ByteArray {
|
|
||||||
if (cbOut == 0) return ByteArray(0)
|
|
||||||
|
|
||||||
val hash: ByteArray = if (cbOut <= 32) {
|
|
||||||
hashSha256(inBytes, inOffset, cbIn)
|
|
||||||
} else {
|
|
||||||
hashSha512(inBytes, inOffset, cbIn)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cbOut == hash.size) {
|
|
||||||
return hash
|
|
||||||
}
|
|
||||||
|
|
||||||
val ret = ByteArray(cbOut)
|
|
||||||
if (cbOut < hash.size) {
|
|
||||||
System.arraycopy(hash, 0, ret, 0, cbOut)
|
|
||||||
} else {
|
|
||||||
var pos = 0
|
|
||||||
var r: Long = 0
|
|
||||||
while (pos < cbOut) {
|
|
||||||
val hmac: Mac
|
|
||||||
try {
|
|
||||||
hmac = Mac.getInstance("HmacSHA256")
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
val pbR = longTo8Bytes(r)
|
|
||||||
val part = hmac.doFinal(pbR)
|
|
||||||
|
|
||||||
val copy = min(cbOut - pos, part.size)
|
|
||||||
System.arraycopy(part, 0, ret, pos, copy)
|
|
||||||
pos += copy
|
|
||||||
r++
|
|
||||||
|
|
||||||
Arrays.fill(part, 0.toByte())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Arrays.fill(hash, 0.toByte())
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hashSha256(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
|
|
||||||
return hashGen("SHA-256", data, offset, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hashSha512(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
|
|
||||||
return hashGen("SHA-512", data, offset, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hashGen(transform: String, data: ByteArray, offset: Int, count: Int): ByteArray {
|
|
||||||
val hash: MessageDigest
|
|
||||||
try {
|
|
||||||
hash = MessageDigest.getInstance(transform)
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
val nos = NullOutputStream()
|
|
||||||
val dos = DigestOutputStream(nos, hash)
|
|
||||||
|
|
||||||
try {
|
|
||||||
dos.write(data, offset, count)
|
|
||||||
dos.close()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
return hash.digest()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.crypto
|
|
||||||
|
|
||||||
import org.bouncycastle.crypto.StreamCipher
|
|
||||||
import org.bouncycastle.crypto.engines.ChaCha7539Engine
|
|
||||||
import org.bouncycastle.crypto.engines.Salsa20Engine
|
|
||||||
import org.bouncycastle.crypto.params.KeyParameter
|
|
||||||
import org.bouncycastle.crypto.params.ParametersWithIV
|
|
||||||
|
|
||||||
object StreamCipherFactory {
|
|
||||||
|
|
||||||
private val SALSA_IV = byteArrayOf(0xE8.toByte(), 0x30, 0x09, 0x4B, 0x97.toByte(), 0x20, 0x5D, 0x2A)
|
|
||||||
|
|
||||||
@Throws(Exception::class)
|
|
||||||
fun getInstance(alg: CrsAlgorithm?, key: ByteArray): StreamCipher {
|
|
||||||
return when {
|
|
||||||
alg === CrsAlgorithm.Salsa20 -> getSalsa20(key)
|
|
||||||
alg === CrsAlgorithm.ChaCha20 -> getChaCha20(key)
|
|
||||||
else -> throw Exception("Invalid random cipher")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSalsa20(key: ByteArray): StreamCipher {
|
|
||||||
// Build stream cipher key
|
|
||||||
val key32 = CryptoUtil.hashSha256(key)
|
|
||||||
|
|
||||||
val keyParam = KeyParameter(key32)
|
|
||||||
val ivParam = ParametersWithIV(keyParam, SALSA_IV)
|
|
||||||
|
|
||||||
val cipher = Salsa20Engine()
|
|
||||||
cipher.init(true, ivParam)
|
|
||||||
|
|
||||||
return cipher
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getChaCha20(key: ByteArray): StreamCipher {
|
|
||||||
// Build stream cipher key
|
|
||||||
val hash = CryptoUtil.hashSha512(key)
|
|
||||||
val key32 = ByteArray(32)
|
|
||||||
val iv = ByteArray(12)
|
|
||||||
|
|
||||||
System.arraycopy(hash, 0, key32, 0, 32)
|
|
||||||
System.arraycopy(hash, 32, iv, 0, 12)
|
|
||||||
|
|
||||||
val keyParam = KeyParameter(key32)
|
|
||||||
val ivParam = ParametersWithIV(keyParam, iv)
|
|
||||||
|
|
||||||
val cipher = ChaCha7539Engine()
|
|
||||||
cipher.init(true, ivParam)
|
|
||||||
|
|
||||||
return cipher
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.crypto.engine
|
|
||||||
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
|
||||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
|
||||||
import java.security.InvalidAlgorithmParameterException
|
|
||||||
import java.security.InvalidKeyException
|
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.util.*
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.NoSuchPaddingException
|
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
class AesEngine : CipherEngine() {
|
|
||||||
|
|
||||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
|
||||||
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray, androidOverride: Boolean): Cipher {
|
|
||||||
val cipher = CipherFactory.getInstance("AES/CBC/PKCS5Padding", androidOverride)
|
|
||||||
cipher.init(opmode, SecretKeySpec(key, "AES"), IvParameterSpec(IV))
|
|
||||||
return cipher
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPwEncryptionAlgorithm(): EncryptionAlgorithm {
|
|
||||||
return EncryptionAlgorithm.AESRijndael
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
val CIPHER_UUID: UUID = bytes16ToUuid(
|
|
||||||
byteArrayOf(0x31.toByte(),
|
|
||||||
0xC1.toByte(),
|
|
||||||
0xF2.toByte(),
|
|
||||||
0xE6.toByte(),
|
|
||||||
0xBF.toByte(),
|
|
||||||
0x71.toByte(),
|
|
||||||
0x43.toByte(),
|
|
||||||
0x50.toByte(),
|
|
||||||
0xBE.toByte(),
|
|
||||||
0x58.toByte(),
|
|
||||||
0x05.toByte(),
|
|
||||||
0x21.toByte(),
|
|
||||||
0x6A.toByte(),
|
|
||||||
0xFC.toByte(),
|
|
||||||
0x5A.toByte(),
|
|
||||||
0xFF.toByte()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.crypto.engine
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
|
||||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
|
||||||
import java.security.InvalidAlgorithmParameterException
|
|
||||||
import java.security.InvalidKeyException
|
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.util.*
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.NoSuchPaddingException
|
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
class TwofishEngine : CipherEngine() {
|
|
||||||
|
|
||||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
|
||||||
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray, androidOverride: Boolean): Cipher {
|
|
||||||
val cipher: Cipher = if (opmode == Cipher.ENCRYPT_MODE) {
|
|
||||||
CipherFactory.getInstance("Twofish/CBC/ZeroBytePadding", androidOverride)
|
|
||||||
} else {
|
|
||||||
CipherFactory.getInstance("Twofish/CBC/NoPadding", androidOverride)
|
|
||||||
}
|
|
||||||
cipher.init(opmode, SecretKeySpec(key, "AES"), IvParameterSpec(IV))
|
|
||||||
|
|
||||||
return cipher
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPwEncryptionAlgorithm(): EncryptionAlgorithm {
|
|
||||||
return EncryptionAlgorithm.Twofish
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
val CIPHER_UUID: UUID = bytes16ToUuid(
|
|
||||||
byteArrayOf(0xAD.toByte(),
|
|
||||||
0x68.toByte(),
|
|
||||||
0xF2.toByte(),
|
|
||||||
0x9F.toByte(),
|
|
||||||
0x57.toByte(),
|
|
||||||
0x6F.toByte(),
|
|
||||||
0x4B.toByte(),
|
|
||||||
0xB9.toByte(),
|
|
||||||
0xA3.toByte(),
|
|
||||||
0x6A.toByte(),
|
|
||||||
0xD4.toByte(),
|
|
||||||
0x7A.toByte(),
|
|
||||||
0xF9.toByte(),
|
|
||||||
0x65.toByte(),
|
|
||||||
0x34.toByte(),
|
|
||||||
0x6C.toByte()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.crypto.finalkey
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory.deviceBlacklisted
|
|
||||||
|
|
||||||
object AESKeyTransformerFactory : KeyTransformer() {
|
|
||||||
override fun transformMasterKey(seed: ByteArray?, key: ByteArray?, rounds: Long?): ByteArray? {
|
|
||||||
// Prefer the native final key implementation
|
|
||||||
val keyTransformer = if (!deviceBlacklisted()
|
|
||||||
&& NativeAESKeyTransformer.available()) {
|
|
||||||
NativeAESKeyTransformer()
|
|
||||||
} else {
|
|
||||||
// Fall back on the android crypto implementation
|
|
||||||
AndroidAESKeyTransformer()
|
|
||||||
}
|
|
||||||
return keyTransformer.transformMasterKey(seed, key, rounds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.crypto.keyDerivation;
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.NativeLib;
|
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public class Argon2Native {
|
|
||||||
|
|
||||||
enum CType {
|
|
||||||
ARGON2_D(0),
|
|
||||||
ARGON2_I(1),
|
|
||||||
ARGON2_ID(2);
|
|
||||||
|
|
||||||
int cValue = 0;
|
|
||||||
|
|
||||||
CType(int i) {
|
|
||||||
cValue = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[] transformKey(Argon2Kdf.Type type, byte[] password, byte[] salt, UnsignedInt parallelism,
|
|
||||||
UnsignedInt memory, UnsignedInt iterations, byte[] secretKey,
|
|
||||||
byte[] associatedData, UnsignedInt version) throws IOException {
|
|
||||||
NativeLib.INSTANCE.init();
|
|
||||||
|
|
||||||
CType cType = CType.ARGON2_D;
|
|
||||||
if (type.equals(Argon2Kdf.Type.ARGON2_ID))
|
|
||||||
cType = CType.ARGON2_ID;
|
|
||||||
|
|
||||||
return nTransformMasterKey(
|
|
||||||
cType.cValue,
|
|
||||||
password,
|
|
||||||
salt,
|
|
||||||
parallelism.toKotlinInt(),
|
|
||||||
memory.toKotlinInt(),
|
|
||||||
iterations.toKotlinInt(),
|
|
||||||
secretKey,
|
|
||||||
associatedData,
|
|
||||||
version.toKotlinInt());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static native byte[] nTransformMasterKey(int type, byte[] password, byte[] salt, int parallelism,
|
|
||||||
int memory, int iterations, byte[] secretKey,
|
|
||||||
byte[] associatedData, int version) throws IOException;
|
|
||||||
}
|
|
||||||
@@ -24,39 +24,26 @@ import android.net.Uri
|
|||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
open class AssignPasswordInDatabaseRunnable (
|
open class AssignPasswordInDatabaseRunnable (
|
||||||
context: Context,
|
context: Context,
|
||||||
database: Database,
|
database: Database,
|
||||||
protected val mDatabaseUri: Uri,
|
protected val mDatabaseUri: Uri,
|
||||||
withMasterPassword: Boolean,
|
protected val mMainCredential: MainCredential)
|
||||||
masterPassword: String?,
|
|
||||||
withKeyFile: Boolean,
|
|
||||||
keyFile: Uri?)
|
|
||||||
: SaveDatabaseRunnable(context, database, true) {
|
: SaveDatabaseRunnable(context, database, true) {
|
||||||
|
|
||||||
private var mMasterPassword: String? = null
|
|
||||||
protected var mKeyFileUri: Uri? = null
|
|
||||||
|
|
||||||
private var mBackupKey: ByteArray? = null
|
private var mBackupKey: ByteArray? = null
|
||||||
|
|
||||||
init {
|
|
||||||
if (withMasterPassword)
|
|
||||||
this.mMasterPassword = masterPassword
|
|
||||||
if (withKeyFile)
|
|
||||||
this.mKeyFileUri = keyFile
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
// Set key
|
// Set key
|
||||||
try {
|
try {
|
||||||
// TODO move master key methods
|
|
||||||
mBackupKey = ByteArray(database.masterKey.size)
|
mBackupKey = ByteArray(database.masterKey.size)
|
||||||
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
|
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
|
||||||
|
|
||||||
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mKeyFileUri)
|
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri)
|
||||||
database.retrieveMasterKey(mMasterPassword, uriInputStream)
|
database.retrieveMasterKey(mMainCredential.masterPassword, uriInputStream)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
erase(mBackupKey)
|
erase(mBackupKey)
|
||||||
setError(e)
|
setError(e)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import android.net.Uri
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
@@ -32,12 +33,9 @@ class CreateDatabaseRunnable(context: Context,
|
|||||||
databaseUri: Uri,
|
databaseUri: Uri,
|
||||||
private val databaseName: String,
|
private val databaseName: String,
|
||||||
private val rootName: String,
|
private val rootName: String,
|
||||||
withMasterPassword: Boolean,
|
mainCredential: MainCredential,
|
||||||
masterPassword: String?,
|
|
||||||
withKeyFile: Boolean,
|
|
||||||
keyFile: Uri?,
|
|
||||||
private val createDatabaseResult: ((Result) -> Unit)?)
|
private val createDatabaseResult: ((Result) -> Unit)?)
|
||||||
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, withMasterPassword, masterPassword, withKeyFile, keyFile) {
|
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) {
|
||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
try {
|
try {
|
||||||
@@ -61,7 +59,7 @@ class CreateDatabaseRunnable(context: Context,
|
|||||||
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
||||||
FileDatabaseHistoryAction.getInstance(context.applicationContext)
|
FileDatabaseHistoryAction.getInstance(context.applicationContext)
|
||||||
.addOrUpdateDatabaseUri(mDatabaseUri,
|
.addOrUpdateDatabaseUri(mDatabaseUri,
|
||||||
if (PreferencesUtil.rememberKeyFileLocations(context)) mKeyFileUri else null)
|
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the current time to init the lock timer
|
// Register the current time to init the lock timer
|
||||||
|
|||||||
@@ -25,8 +25,10 @@ import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
|||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
@@ -35,8 +37,7 @@ import com.kunzisoft.keepass.utils.UriUtil
|
|||||||
class LoadDatabaseRunnable(private val context: Context,
|
class LoadDatabaseRunnable(private val context: Context,
|
||||||
private val mDatabase: Database,
|
private val mDatabase: Database,
|
||||||
private val mUri: Uri,
|
private val mUri: Uri,
|
||||||
private val mPass: String?,
|
private val mMainCredential: MainCredential,
|
||||||
private val mKey: Uri?,
|
|
||||||
private val mReadonly: Boolean,
|
private val mReadonly: Boolean,
|
||||||
private val mCipherEntity: CipherDatabaseEntity?,
|
private val mCipherEntity: CipherDatabaseEntity?,
|
||||||
private val mFixDuplicateUUID: Boolean,
|
private val mFixDuplicateUUID: Boolean,
|
||||||
@@ -51,10 +52,15 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
|
|
||||||
override fun onActionRun() {
|
override fun onActionRun() {
|
||||||
try {
|
try {
|
||||||
mDatabase.loadData(mUri, mPass, mKey,
|
mDatabase.loadData(mUri,
|
||||||
|
mMainCredential,
|
||||||
mReadonly,
|
mReadonly,
|
||||||
context.contentResolver,
|
context.contentResolver,
|
||||||
UriUtil.getBinaryDir(context),
|
UriUtil.getBinaryDir(context),
|
||||||
|
{ memoryWanted ->
|
||||||
|
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||||
|
},
|
||||||
|
LoadedKey.generateNewCipherKey(),
|
||||||
mFixDuplicateUUID,
|
mFixDuplicateUUID,
|
||||||
progressTaskUpdater)
|
progressTaskUpdater)
|
||||||
}
|
}
|
||||||
@@ -67,7 +73,7 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
||||||
FileDatabaseHistoryAction.getInstance(context)
|
FileDatabaseHistoryAction.getInstance(context)
|
||||||
.addOrUpdateDatabaseUri(mUri,
|
.addOrUpdateDatabaseUri(mUri,
|
||||||
if (PreferencesUtil.rememberKeyFileLocations(context)) mKey else null)
|
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the biometric
|
// Register the biometric
|
||||||
|
|||||||
@@ -25,48 +25,52 @@ import android.content.Context.BIND_NOT_FOREGROUND
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
|
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_SAVE
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COLOR_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COMPRESSION_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_COMPRESSION_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DESCRIPTION_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_DESCRIPTION_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENCRYPTION_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENCRYPTION_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ITERATIONS_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ITERATIONS_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_NAME_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_PARALLELISM_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
|
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
|
||||||
@@ -250,11 +254,16 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun start(bundle: Bundle? = null, actionTask: String) {
|
private fun start(bundle: Bundle? = null, actionTask: String) {
|
||||||
activity.stopService(intentDatabaseTask)
|
try {
|
||||||
if (bundle != null)
|
activity.stopService(intentDatabaseTask)
|
||||||
intentDatabaseTask.putExtras(bundle)
|
if (bundle != null)
|
||||||
intentDatabaseTask.action = actionTask
|
intentDatabaseTask.putExtras(bundle)
|
||||||
activity.startService(intentDatabaseTask)
|
intentDatabaseTask.action = actionTask
|
||||||
|
activity.startService(intentDatabaseTask)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to perform database action", e)
|
||||||
|
Toast.makeText(activity, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -264,30 +273,22 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
fun startDatabaseCreate(databaseUri: Uri,
|
fun startDatabaseCreate(databaseUri: Uri,
|
||||||
masterPasswordChecked: Boolean,
|
mainCredential: MainCredential) {
|
||||||
masterPassword: String?,
|
|
||||||
keyFileChecked: Boolean,
|
|
||||||
keyFile: Uri?) {
|
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||||
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||||
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
|
||||||
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
|
|
||||||
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
|
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_CREATE_TASK)
|
, ACTION_DATABASE_CREATE_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseLoad(databaseUri: Uri,
|
fun startDatabaseLoad(databaseUri: Uri,
|
||||||
masterPassword: String?,
|
mainCredential: MainCredential,
|
||||||
keyFile: Uri?,
|
|
||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
cipherEntity: CipherDatabaseEntity?,
|
cipherEntity: CipherDatabaseEntity?,
|
||||||
fixDuplicateUuid: Boolean) {
|
fixDuplicateUuid: Boolean) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||||
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||||
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
|
|
||||||
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
|
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
|
||||||
putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity)
|
putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity)
|
||||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||||
@@ -303,17 +304,11 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseAssignPassword(databaseUri: Uri,
|
fun startDatabaseAssignPassword(databaseUri: Uri,
|
||||||
masterPasswordChecked: Boolean,
|
mainCredential: MainCredential) {
|
||||||
masterPassword: String?,
|
|
||||||
keyFileChecked: Boolean,
|
|
||||||
keyFile: Uri?) {
|
|
||||||
|
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||||
putBoolean(DatabaseTaskNotificationService.MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||||
putString(DatabaseTaskNotificationService.MASTER_PASSWORD_KEY, masterPassword)
|
|
||||||
putBoolean(DatabaseTaskNotificationService.KEY_FILE_CHECKED_KEY, keyFileChecked)
|
|
||||||
putParcelable(DatabaseTaskNotificationService.KEY_FILE_URI_KEY, keyFile)
|
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_ASSIGN_PASSWORD_TASK)
|
, ACTION_DATABASE_ASSIGN_PASSWORD_TASK)
|
||||||
}
|
}
|
||||||
@@ -604,4 +599,8 @@ class ProgressDatabaseTaskProvider(private val activity: FragmentActivity) {
|
|||||||
}
|
}
|
||||||
, ACTION_DATABASE_SAVE)
|
, ACTION_DATABASE_SAVE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = ProgressDatabaseTaskProvider::class.java.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,8 @@ package com.kunzisoft.keepass.database.action
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
@@ -34,18 +35,25 @@ class ReloadDatabaseRunnable(private val context: Context,
|
|||||||
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||||
: ActionRunnable() {
|
: ActionRunnable() {
|
||||||
|
|
||||||
|
private var tempCipherKey: LoadedKey? = null
|
||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
|
tempCipherKey = mDatabase.binaryCache.loadedCipherKey
|
||||||
// Clear before we load
|
// Clear before we load
|
||||||
mDatabase.clear(UriUtil.getBinaryDir(context))
|
mDatabase.clear(UriUtil.getBinaryDir(context))
|
||||||
|
mDatabase.wasReloaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionRun() {
|
override fun onActionRun() {
|
||||||
try {
|
try {
|
||||||
mDatabase.reloadData(context.contentResolver,
|
mDatabase.reloadData(context.contentResolver,
|
||||||
UriUtil.getBinaryDir(context),
|
UriUtil.getBinaryDir(context),
|
||||||
|
{ memoryWanted ->
|
||||||
|
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||||
|
},
|
||||||
|
tempCipherKey ?: LoadedKey.generateNewCipherKey(),
|
||||||
progressTaskUpdater)
|
progressTaskUpdater)
|
||||||
}
|
} catch (e: LoadDatabaseException) {
|
||||||
catch (e: LoadDatabaseException) {
|
|
||||||
setError(e)
|
setError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +61,7 @@ class ReloadDatabaseRunnable(private val context: Context,
|
|||||||
// Register the current time to init the lock timer
|
// Register the current time to init the lock timer
|
||||||
PreferencesUtil.saveCurrentTime(context)
|
PreferencesUtil.saveCurrentTime(context)
|
||||||
} else {
|
} else {
|
||||||
|
tempCipherKey = null
|
||||||
mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
|
mDatabase.clearAndClose(UriUtil.getBinaryDir(context))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,16 +52,9 @@ class CopyNodesRunnable constructor(
|
|||||||
if (mNewParent != database.rootGroup || database.rootCanContainsEntry()) {
|
if (mNewParent != database.rootGroup || database.rootCanContainsEntry()) {
|
||||||
// Update entry with new values
|
// Update entry with new values
|
||||||
mNewParent.touch(modified = false, touchParents = true)
|
mNewParent.touch(modified = false, touchParents = true)
|
||||||
|
|
||||||
val entryCopied = database.copyEntryTo(currentNode as Entry, mNewParent)
|
val entryCopied = database.copyEntryTo(currentNode as Entry, mNewParent)
|
||||||
if (entryCopied != null) {
|
entryCopied.touch(modified = true, touchParents = true)
|
||||||
entryCopied.touch(modified = true, touchParents = true)
|
mEntriesCopied.add(entryCopied)
|
||||||
mEntriesCopied.add(entryCopied)
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "Unable to create a copy of the entry")
|
|
||||||
setError(CopyEntryDatabaseException())
|
|
||||||
break@foreachNode
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Only finish thread
|
// Only finish thread
|
||||||
setError(CopyEntryDatabaseException())
|
setError(CopyEntryDatabaseException())
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class DeleteNodesRunnable(context: Context,
|
|||||||
database.deleteEntry(currentNode)
|
database.deleteEntry(currentNode)
|
||||||
}
|
}
|
||||||
// Remove the oldest attachments
|
// Remove the oldest attachments
|
||||||
currentNode.getAttachments(database.binaryPool).forEach {
|
currentNode.getAttachments(database.attachmentPool).forEach {
|
||||||
database.removeAttachmentIfNotUsed(it)
|
database.removeAttachmentIfNotUsed(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import android.util.Log
|
|||||||
import com.kunzisoft.keepass.database.element.*
|
import com.kunzisoft.keepass.database.element.*
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.exception.EntryDatabaseException
|
import com.kunzisoft.keepass.database.exception.MoveEntryDatabaseException
|
||||||
import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException
|
import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException
|
||||||
|
|
||||||
class MoveNodesRunnable constructor(
|
class MoveNodesRunnable constructor(
|
||||||
@@ -47,8 +47,10 @@ class MoveNodesRunnable constructor(
|
|||||||
when (nodeToMove.type) {
|
when (nodeToMove.type) {
|
||||||
Type.GROUP -> {
|
Type.GROUP -> {
|
||||||
val groupToMove = nodeToMove as Group
|
val groupToMove = nodeToMove as Group
|
||||||
// Move group in new parent if not in the current group
|
// Move group if the parent change
|
||||||
if (groupToMove != mNewParent
|
if (mOldParent != mNewParent
|
||||||
|
// and if not in the current group
|
||||||
|
&& groupToMove != mNewParent
|
||||||
&& !mNewParent.isContainedIn(groupToMove)) {
|
&& !mNewParent.isContainedIn(groupToMove)) {
|
||||||
nodeToMove.touch(modified = true, touchParents = true)
|
nodeToMove.touch(modified = true, touchParents = true)
|
||||||
database.moveGroupTo(groupToMove, mNewParent)
|
database.moveGroupTo(groupToMove, mNewParent)
|
||||||
@@ -68,7 +70,7 @@ class MoveNodesRunnable constructor(
|
|||||||
database.moveEntryTo(entryToMove, mNewParent)
|
database.moveEntryTo(entryToMove, mNewParent)
|
||||||
} else {
|
} else {
|
||||||
// Only finish thread
|
// Only finish thread
|
||||||
setError(EntryDatabaseException())
|
setError(MoveEntryDatabaseException())
|
||||||
break@foreachNode
|
break@foreachNode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,14 +42,14 @@ class UpdateEntryRunnable constructor(
|
|||||||
mNewEntry.addParentFrom(mOldEntry)
|
mNewEntry.addParentFrom(mOldEntry)
|
||||||
|
|
||||||
// Build oldest attachments
|
// Build oldest attachments
|
||||||
val oldEntryAttachments = mOldEntry.getAttachments(database.binaryPool, true)
|
val oldEntryAttachments = mOldEntry.getAttachments(database.attachmentPool, true)
|
||||||
val newEntryAttachments = mNewEntry.getAttachments(database.binaryPool, true)
|
val newEntryAttachments = mNewEntry.getAttachments(database.attachmentPool, true)
|
||||||
val attachmentsToRemove = ArrayList<Attachment>(oldEntryAttachments)
|
val attachmentsToRemove = ArrayList<Attachment>(oldEntryAttachments)
|
||||||
// Not use equals because only check name
|
// Not use equals because only check name
|
||||||
newEntryAttachments.forEach { newAttachment ->
|
newEntryAttachments.forEach { newAttachment ->
|
||||||
oldEntryAttachments.forEach { oldAttachment ->
|
oldEntryAttachments.forEach { oldAttachment ->
|
||||||
if (oldAttachment.name == newAttachment.name
|
if (oldAttachment.name == newAttachment.name
|
||||||
&& oldAttachment.binaryAttachment == newAttachment.binaryAttachment)
|
&& oldAttachment.binaryData == newAttachment.binaryData)
|
||||||
attachmentsToRemove.remove(oldAttachment)
|
attachmentsToRemove.remove(oldAttachment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ class UpdateEntryRunnable constructor(
|
|||||||
|
|
||||||
// Create an entry history (an entry history don't have history)
|
// Create an entry history (an entry history don't have history)
|
||||||
mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false))
|
mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false))
|
||||||
database.removeOldestEntryHistory(mOldEntry, database.binaryPool)
|
database.removeOldestEntryHistory(mOldEntry, database.attachmentPool)
|
||||||
|
|
||||||
// Only change data in index
|
// Only change data in index
|
||||||
database.updateEntry(mOldEntry)
|
database.updateEntry(mOldEntry)
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.database.crypto
|
||||||
|
|
||||||
|
|
||||||
|
import com.kunzisoft.encrypt.CipherFactory
|
||||||
|
import java.security.InvalidAlgorithmParameterException
|
||||||
|
import java.security.InvalidKeyException
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.NoSuchPaddingException
|
||||||
|
|
||||||
|
class AesEngine : CipherEngine() {
|
||||||
|
|
||||||
|
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
||||||
|
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
|
||||||
|
return CipherFactory.getAES(opmode, key, IV)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getEncryptionAlgorithm(): EncryptionAlgorithm {
|
||||||
|
return EncryptionAlgorithm.AESRijndael
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,19 +17,14 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto.engine
|
package com.kunzisoft.keepass.database.crypto
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
import com.kunzisoft.encrypt.CipherFactory
|
||||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
|
||||||
import java.security.InvalidAlgorithmParameterException
|
import java.security.InvalidAlgorithmParameterException
|
||||||
import java.security.InvalidKeyException
|
import java.security.InvalidKeyException
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
import java.util.*
|
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.NoSuchPaddingException
|
import javax.crypto.NoSuchPaddingException
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
class ChaCha20Engine : CipherEngine() {
|
class ChaCha20Engine : CipherEngine() {
|
||||||
|
|
||||||
@@ -38,34 +33,11 @@ class ChaCha20Engine : CipherEngine() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
||||||
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray, androidOverride: Boolean): Cipher {
|
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
|
||||||
val cipher = Cipher.getInstance("Chacha7539", BouncyCastleProvider())
|
return CipherFactory.getChacha20(opmode, key, IV)
|
||||||
cipher.init(opmode, SecretKeySpec(key, "ChaCha7539"), IvParameterSpec(IV))
|
|
||||||
return cipher
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPwEncryptionAlgorithm(): EncryptionAlgorithm {
|
override fun getEncryptionAlgorithm(): EncryptionAlgorithm {
|
||||||
return EncryptionAlgorithm.ChaCha20
|
return EncryptionAlgorithm.ChaCha20
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
val CIPHER_UUID: UUID = bytes16ToUuid(
|
|
||||||
byteArrayOf(0xD6.toByte(),
|
|
||||||
0x03.toByte(),
|
|
||||||
0x8A.toByte(),
|
|
||||||
0x2B.toByte(),
|
|
||||||
0x8B.toByte(),
|
|
||||||
0x6F.toByte(),
|
|
||||||
0x4C.toByte(),
|
|
||||||
0xB5.toByte(),
|
|
||||||
0xA5.toByte(),
|
|
||||||
0x24.toByte(),
|
|
||||||
0x33.toByte(),
|
|
||||||
0x9A.toByte(),
|
|
||||||
0x31.toByte(),
|
|
||||||
0xDB.toByte(),
|
|
||||||
0xB5.toByte(),
|
|
||||||
0x9A.toByte()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -17,9 +17,7 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto.engine
|
package com.kunzisoft.keepass.database.crypto
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
|
||||||
|
|
||||||
import java.security.InvalidAlgorithmParameterException
|
import java.security.InvalidAlgorithmParameterException
|
||||||
import java.security.InvalidKeyException
|
import java.security.InvalidKeyException
|
||||||
@@ -38,14 +36,12 @@ abstract class CipherEngine {
|
|||||||
return 16
|
return 16
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
// Used only with padding workaround
|
||||||
abstract fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray, androidOverride: Boolean): Cipher
|
var forcePaddingCompatibility = false
|
||||||
|
|
||||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
||||||
fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
|
abstract fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher
|
||||||
return getCipher(opmode, key, IV, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract fun getPwEncryptionAlgorithm(): EncryptionAlgorithm
|
abstract fun getEncryptionAlgorithm(): EncryptionAlgorithm
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -17,11 +17,13 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto
|
package com.kunzisoft.keepass.database.crypto
|
||||||
|
|
||||||
|
import com.kunzisoft.encrypt.HashManager
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
|
import com.kunzisoft.encrypt.StreamCipher
|
||||||
|
|
||||||
enum class CrsAlgorithm constructor(val id: UnsignedInt) {
|
enum class CrsAlgorithm(val id: UnsignedInt) {
|
||||||
|
|
||||||
Null(UnsignedInt(0)),
|
Null(UnsignedInt(0)),
|
||||||
ArcFourVariant(UnsignedInt(1)),
|
ArcFourVariant(UnsignedInt(1)),
|
||||||
@@ -30,6 +32,15 @@ enum class CrsAlgorithm constructor(val id: UnsignedInt) {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun getCipher(algorithm: CrsAlgorithm?, key: ByteArray): StreamCipher {
|
||||||
|
return when (algorithm) {
|
||||||
|
Salsa20 -> HashManager.getSalsa20(key)
|
||||||
|
ChaCha20 -> HashManager.getChaCha20(key)
|
||||||
|
else -> throw Exception("Invalid random cipher")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun fromId(num: UnsignedInt): CrsAlgorithm? {
|
fun fromId(num: UnsignedInt): CrsAlgorithm? {
|
||||||
for (e in values()) {
|
for (e in values()) {
|
||||||
if (e.id == num) {
|
if (e.id == num) {
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.database.crypto
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.utils.bytes16ToUuid
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
enum class EncryptionAlgorithm {
|
||||||
|
|
||||||
|
AESRijndael,
|
||||||
|
Twofish,
|
||||||
|
ChaCha20;
|
||||||
|
|
||||||
|
val cipherEngine: CipherEngine
|
||||||
|
get() {
|
||||||
|
return when (this) {
|
||||||
|
AESRijndael -> AesEngine()
|
||||||
|
Twofish -> TwofishEngine()
|
||||||
|
ChaCha20 -> ChaCha20Engine()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val uuid: UUID
|
||||||
|
get() {
|
||||||
|
return when (this) {
|
||||||
|
AESRijndael -> AES_UUID
|
||||||
|
Twofish -> TWOFISH_UUID
|
||||||
|
ChaCha20 -> CHACHA20_UUID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return when (this) {
|
||||||
|
AESRijndael -> "Rijndael (AES)"
|
||||||
|
Twofish -> "Twofish"
|
||||||
|
ChaCha20 -> "ChaCha20"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate appropriate cipher based on KeePass 2.x UUID's
|
||||||
|
*/
|
||||||
|
@Throws(NoSuchAlgorithmException::class)
|
||||||
|
fun getFrom(uuid: UUID): EncryptionAlgorithm {
|
||||||
|
return when (uuid) {
|
||||||
|
AES_UUID -> AESRijndael
|
||||||
|
TWOFISH_UUID -> Twofish
|
||||||
|
CHACHA20_UUID -> ChaCha20
|
||||||
|
else -> throw NoSuchAlgorithmException("UUID unrecognized.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val AES_UUID: UUID by lazy {
|
||||||
|
bytes16ToUuid(
|
||||||
|
byteArrayOf(0x31.toByte(),
|
||||||
|
0xC1.toByte(),
|
||||||
|
0xF2.toByte(),
|
||||||
|
0xE6.toByte(),
|
||||||
|
0xBF.toByte(),
|
||||||
|
0x71.toByte(),
|
||||||
|
0x43.toByte(),
|
||||||
|
0x50.toByte(),
|
||||||
|
0xBE.toByte(),
|
||||||
|
0x58.toByte(),
|
||||||
|
0x05.toByte(),
|
||||||
|
0x21.toByte(),
|
||||||
|
0x6A.toByte(),
|
||||||
|
0xFC.toByte(),
|
||||||
|
0x5A.toByte(),
|
||||||
|
0xFF.toByte()))
|
||||||
|
}
|
||||||
|
|
||||||
|
private val TWOFISH_UUID: UUID by lazy {
|
||||||
|
bytes16ToUuid(
|
||||||
|
byteArrayOf(0xAD.toByte(),
|
||||||
|
0x68.toByte(),
|
||||||
|
0xF2.toByte(),
|
||||||
|
0x9F.toByte(),
|
||||||
|
0x57.toByte(),
|
||||||
|
0x6F.toByte(),
|
||||||
|
0x4B.toByte(),
|
||||||
|
0xB9.toByte(),
|
||||||
|
0xA3.toByte(),
|
||||||
|
0x6A.toByte(),
|
||||||
|
0xD4.toByte(),
|
||||||
|
0x7A.toByte(),
|
||||||
|
0xF9.toByte(),
|
||||||
|
0x65.toByte(),
|
||||||
|
0x34.toByte(),
|
||||||
|
0x6C.toByte()))
|
||||||
|
}
|
||||||
|
|
||||||
|
private val CHACHA20_UUID: UUID by lazy {
|
||||||
|
bytes16ToUuid(
|
||||||
|
byteArrayOf(0xD6.toByte(),
|
||||||
|
0x03.toByte(),
|
||||||
|
0x8A.toByte(),
|
||||||
|
0x2B.toByte(),
|
||||||
|
0x8B.toByte(),
|
||||||
|
0x6F.toByte(),
|
||||||
|
0x4C.toByte(),
|
||||||
|
0xB5.toByte(),
|
||||||
|
0xA5.toByte(),
|
||||||
|
0x24.toByte(),
|
||||||
|
0x33.toByte(),
|
||||||
|
0x9A.toByte(),
|
||||||
|
0x31.toByte(),
|
||||||
|
0xDB.toByte(),
|
||||||
|
0xB5.toByte(),
|
||||||
|
0x9A.toByte()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,35 +17,40 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.stream
|
package com.kunzisoft.keepass.database.crypto
|
||||||
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.security.DigestOutputStream
|
import java.security.InvalidKeyException
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
object HmacBlockStream {
|
object HmacBlock {
|
||||||
fun getHmacKey64(key: ByteArray, blockIndex: Long): ByteArray {
|
|
||||||
|
fun getHmacSha256(blockKey: ByteArray): Mac {
|
||||||
|
val hmac: Mac
|
||||||
|
try {
|
||||||
|
hmac = Mac.getInstance("HmacSHA256")
|
||||||
|
val signingKey = SecretKeySpec(blockKey, "HmacSHA256")
|
||||||
|
hmac.init(signingKey)
|
||||||
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
|
throw IOException("No HmacAlogirthm")
|
||||||
|
} catch (e: InvalidKeyException) {
|
||||||
|
throw IOException("Invalid Hmac Key")
|
||||||
|
}
|
||||||
|
return hmac
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getHmacKey64(key: ByteArray, blockIndex: ByteArray): ByteArray {
|
||||||
val hash: MessageDigest
|
val hash: MessageDigest
|
||||||
try {
|
try {
|
||||||
hash = MessageDigest.getInstance("SHA-512")
|
hash = MessageDigest.getInstance("SHA-512")
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
throw RuntimeException(e)
|
throw RuntimeException(e)
|
||||||
}
|
}
|
||||||
|
hash.update(blockIndex)
|
||||||
val nos = NullOutputStream()
|
hash.update(key)
|
||||||
val dos = DigestOutputStream(nos, hash)
|
|
||||||
val leos = LittleEndianDataOutputStream(dos)
|
|
||||||
|
|
||||||
try {
|
|
||||||
leos.writeLong(blockIndex)
|
|
||||||
leos.write(key)
|
|
||||||
leos.close()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
//assert(hashKey.length == 64);
|
|
||||||
return hash.digest()
|
return hash.digest()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.database.crypto
|
||||||
|
|
||||||
|
import com.kunzisoft.encrypt.CipherFactory
|
||||||
|
import java.security.InvalidAlgorithmParameterException
|
||||||
|
import java.security.InvalidKeyException
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.NoSuchPaddingException
|
||||||
|
|
||||||
|
class TwofishEngine : CipherEngine() {
|
||||||
|
|
||||||
|
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
||||||
|
override fun getCipher(opmode: Int, key: ByteArray, IV: ByteArray): Cipher {
|
||||||
|
return CipherFactory.getTwofish(opmode, key, IV, forcePaddingCompatibility)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getEncryptionAlgorithm(): EncryptionAlgorithm {
|
||||||
|
return EncryptionAlgorithm.Twofish
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,13 +17,10 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.utils
|
package com.kunzisoft.keepass.database.crypto
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
|
import com.kunzisoft.keepass.utils.*
|
||||||
import com.kunzisoft.keepass.stream.*
|
import java.io.*
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.IOException
|
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -55,12 +52,12 @@ open class VariantDictionary {
|
|||||||
return dict[name]?.value as UnsignedInt?
|
return dict[name]?.value as UnsignedInt?
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setUInt64(name: String, value: Long) {
|
fun setUInt64(name: String, value: UnsignedLong) {
|
||||||
putType(VdType.UInt64, name, value)
|
putType(VdType.UInt64, name, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getUInt64(name: String): Long? {
|
fun getUInt64(name: String): UnsignedLong? {
|
||||||
return dict[name]?.value as Long?
|
return dict[name]?.value as UnsignedLong?
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setBool(name: String, value: Boolean) {
|
fun setBool(name: String, value: Boolean) {
|
||||||
@@ -115,22 +112,21 @@ open class VariantDictionary {
|
|||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun deserialize(data: ByteArray): VariantDictionary {
|
fun deserialize(data: ByteArray): VariantDictionary {
|
||||||
val inputStream = LittleEndianDataInputStream(ByteArrayInputStream(data))
|
val inputStream = ByteArrayInputStream(data)
|
||||||
return deserialize(inputStream)
|
return deserialize(inputStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun serialize(kdfParameters: KdfParameters): ByteArray {
|
fun serialize(variantDictionary: VariantDictionary): ByteArray {
|
||||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||||
val outputStream = LittleEndianDataOutputStream(byteArrayOutputStream)
|
serialize(variantDictionary, byteArrayOutputStream)
|
||||||
serialize(kdfParameters, outputStream)
|
|
||||||
return byteArrayOutputStream.toByteArray()
|
return byteArrayOutputStream.toByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun deserialize(inputStream: LittleEndianDataInputStream): VariantDictionary {
|
fun deserialize(inputStream: InputStream): VariantDictionary {
|
||||||
val dictionary = VariantDictionary()
|
val dictionary = VariantDictionary()
|
||||||
val version = inputStream.readUShort()
|
val version = inputStream.readBytes2ToUShort()
|
||||||
if (version and VdmCritical > VdVersion and VdmCritical) {
|
if (version and VdmCritical > VdVersion and VdmCritical) {
|
||||||
throw IOException("Invalid format")
|
throw IOException("Invalid format")
|
||||||
}
|
}
|
||||||
@@ -143,14 +139,14 @@ open class VariantDictionary {
|
|||||||
if (bType == VdType.None) {
|
if (bType == VdType.None) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
val nameLen = inputStream.readUInt().toKotlinInt()
|
val nameLen = inputStream.readBytes4ToUInt().toKotlinInt()
|
||||||
val nameBuf = inputStream.readBytes(nameLen)
|
val nameBuf = inputStream.readBytesLength(nameLen)
|
||||||
if (nameLen != nameBuf.size) {
|
if (nameLen != nameBuf.size) {
|
||||||
throw IOException("Invalid format")
|
throw IOException("Invalid format")
|
||||||
}
|
}
|
||||||
val name = String(nameBuf, UTF8Charset)
|
val name = String(nameBuf, UTF8Charset)
|
||||||
val valueLen = inputStream.readUInt().toKotlinInt()
|
val valueLen = inputStream.readBytes4ToUInt().toKotlinInt()
|
||||||
val valueBuf = inputStream.readBytes(valueLen)
|
val valueBuf = inputStream.readBytesLength(valueLen)
|
||||||
if (valueLen != valueBuf.size) {
|
if (valueLen != valueBuf.size) {
|
||||||
throw IOException("Invalid format")
|
throw IOException("Invalid format")
|
||||||
}
|
}
|
||||||
@@ -159,7 +155,7 @@ open class VariantDictionary {
|
|||||||
dictionary.setUInt32(name, bytes4ToUInt(valueBuf))
|
dictionary.setUInt32(name, bytes4ToUInt(valueBuf))
|
||||||
}
|
}
|
||||||
VdType.UInt64 -> if (valueLen == 8) {
|
VdType.UInt64 -> if (valueLen == 8) {
|
||||||
dictionary.setUInt64(name, bytes64ToLong(valueBuf))
|
dictionary.setUInt64(name, bytes64ToULong(valueBuf))
|
||||||
}
|
}
|
||||||
VdType.Bool -> if (valueLen == 1) {
|
VdType.Bool -> if (valueLen == 1) {
|
||||||
dictionary.setBool(name, valueBuf[0] != 0.toByte())
|
dictionary.setBool(name, valueBuf[0] != 0.toByte())
|
||||||
@@ -181,48 +177,47 @@ open class VariantDictionary {
|
|||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun serialize(variantDictionary: VariantDictionary,
|
fun serialize(variantDictionary: VariantDictionary,
|
||||||
outputStream: LittleEndianDataOutputStream?) {
|
outputStream: OutputStream?) {
|
||||||
if (outputStream == null) {
|
if (outputStream == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
outputStream.writeUShort(VdVersion)
|
outputStream.write2BytesUShort(VdVersion)
|
||||||
for ((name, vd) in variantDictionary.dict) {
|
for ((name, vd) in variantDictionary.dict) {
|
||||||
val nameBuf = name.toByteArray(UTF8Charset)
|
val nameBuf = name.toByteArray(UTF8Charset)
|
||||||
outputStream.write(vd.type.toInt())
|
outputStream.writeByte(vd.type)
|
||||||
outputStream.writeInt(nameBuf.size)
|
outputStream.write4BytesUInt(UnsignedInt(nameBuf.size))
|
||||||
outputStream.write(nameBuf)
|
outputStream.write(nameBuf)
|
||||||
var buf: ByteArray
|
var buf: ByteArray
|
||||||
when (vd.type) {
|
when (vd.type) {
|
||||||
VdType.UInt32 -> {
|
VdType.UInt32 -> {
|
||||||
outputStream.writeInt(4)
|
outputStream.write4BytesUInt(UnsignedInt(4))
|
||||||
outputStream.writeUInt((vd.value as UnsignedInt))
|
outputStream.write4BytesUInt(vd.value as UnsignedInt)
|
||||||
}
|
}
|
||||||
VdType.UInt64 -> {
|
VdType.UInt64 -> {
|
||||||
outputStream.writeInt(8)
|
outputStream.write4BytesUInt(UnsignedInt(8))
|
||||||
outputStream.writeLong(vd.value as Long)
|
outputStream.write8BytesLong(vd.value as UnsignedLong)
|
||||||
}
|
}
|
||||||
VdType.Bool -> {
|
VdType.Bool -> {
|
||||||
outputStream.writeInt(1)
|
outputStream.write4BytesUInt(UnsignedInt(1))
|
||||||
val bool = if (vd.value as Boolean) 1.toByte() else 0.toByte()
|
outputStream.writeBooleanByte(vd.value as Boolean)
|
||||||
outputStream.write(bool.toInt())
|
|
||||||
}
|
}
|
||||||
VdType.Int32 -> {
|
VdType.Int32 -> {
|
||||||
outputStream.writeInt(4)
|
outputStream.write4BytesUInt(UnsignedInt(4))
|
||||||
outputStream.writeInt(vd.value as Int)
|
outputStream.write4BytesUInt(UnsignedInt(vd.value as Int))
|
||||||
}
|
}
|
||||||
VdType.Int64 -> {
|
VdType.Int64 -> {
|
||||||
outputStream.writeInt(8)
|
outputStream.write4BytesUInt(UnsignedInt(8))
|
||||||
outputStream.writeLong(vd.value as Long)
|
outputStream.write8BytesLong(vd.value as Long)
|
||||||
}
|
}
|
||||||
VdType.String -> {
|
VdType.String -> {
|
||||||
val value = vd.value as String
|
val value = vd.value as String
|
||||||
buf = value.toByteArray(UTF8Charset)
|
buf = value.toByteArray(UTF8Charset)
|
||||||
outputStream.writeInt(buf.size)
|
outputStream.write4BytesUInt(UnsignedInt(buf.size))
|
||||||
outputStream.write(buf)
|
outputStream.write(buf)
|
||||||
}
|
}
|
||||||
VdType.ByteArray -> {
|
VdType.ByteArray -> {
|
||||||
buf = vd.value as ByteArray
|
buf = vd.value as ByteArray
|
||||||
outputStream.writeInt(buf.size)
|
outputStream.write4BytesUInt(UnsignedInt(buf.size))
|
||||||
outputStream.write(buf)
|
outputStream.write(buf)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@@ -17,13 +17,12 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto.keyDerivation
|
package com.kunzisoft.keepass.database.crypto.kdf
|
||||||
|
|
||||||
import android.content.res.Resources
|
import com.kunzisoft.encrypt.HashManager
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.utils.UnsignedLong
|
||||||
import com.kunzisoft.keepass.crypto.CryptoUtil
|
import com.kunzisoft.encrypt.aes.AESTransformer
|
||||||
import com.kunzisoft.keepass.crypto.finalkey.AESKeyTransformerFactory
|
import com.kunzisoft.keepass.utils.bytes16ToUuid
|
||||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -38,32 +37,28 @@ class AesKdf : KdfEngine() {
|
|||||||
get() {
|
get() {
|
||||||
return KdfParameters(uuid!!).apply {
|
return KdfParameters(uuid!!).apply {
|
||||||
setParamUUID()
|
setParamUUID()
|
||||||
setUInt64(PARAM_ROUNDS, defaultKeyRounds)
|
setUInt64(PARAM_ROUNDS, UnsignedLong(defaultKeyRounds))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override val defaultKeyRounds: Long = 500000L
|
override val defaultKeyRounds = 500000L
|
||||||
|
|
||||||
override fun getName(resources: Resources): String {
|
|
||||||
return resources.getString(R.string.kdf_AES)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun transform(masterKey: ByteArray, kdfParameters: KdfParameters): ByteArray {
|
override fun transform(masterKey: ByteArray, kdfParameters: KdfParameters): ByteArray {
|
||||||
|
|
||||||
var seed = kdfParameters.getByteArray(PARAM_SEED)
|
var seed = kdfParameters.getByteArray(PARAM_SEED)
|
||||||
if (seed != null && seed.size != 32) {
|
if (seed != null && seed.size != 32) {
|
||||||
seed = CryptoUtil.hashSha256(seed)
|
seed = HashManager.hashSha256(seed)
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentMasterKey = masterKey
|
var currentMasterKey = masterKey
|
||||||
if (currentMasterKey.size != 32) {
|
if (currentMasterKey.size != 32) {
|
||||||
currentMasterKey = CryptoUtil.hashSha256(currentMasterKey)
|
currentMasterKey = HashManager.hashSha256(currentMasterKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
val rounds = kdfParameters.getUInt64(PARAM_ROUNDS)
|
val rounds = kdfParameters.getUInt64(PARAM_ROUNDS)?.toKotlinLong()
|
||||||
|
|
||||||
return AESKeyTransformerFactory.transformMasterKey(seed, currentMasterKey, rounds) ?: ByteArray(0)
|
return AESTransformer.transformKey(seed, currentMasterKey, rounds) ?: ByteArray(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun randomize(kdfParameters: KdfParameters) {
|
override fun randomize(kdfParameters: KdfParameters) {
|
||||||
@@ -76,11 +71,15 @@ class AesKdf : KdfEngine() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getKeyRounds(kdfParameters: KdfParameters): Long {
|
override fun getKeyRounds(kdfParameters: KdfParameters): Long {
|
||||||
return kdfParameters.getUInt64(PARAM_ROUNDS) ?: defaultKeyRounds
|
return kdfParameters.getUInt64(PARAM_ROUNDS)?.toKotlinLong() ?: defaultKeyRounds
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setKeyRounds(kdfParameters: KdfParameters, keyRounds: Long) {
|
override fun setKeyRounds(kdfParameters: KdfParameters, keyRounds: Long) {
|
||||||
kdfParameters.setUInt64(PARAM_ROUNDS, keyRounds)
|
kdfParameters.setUInt64(PARAM_ROUNDS, UnsignedLong(keyRounds))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "AES"
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -17,13 +17,13 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto.keyDerivation
|
package com.kunzisoft.keepass.database.crypto.kdf
|
||||||
|
|
||||||
import android.content.res.Resources
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
|
import com.kunzisoft.keepass.utils.UnsignedLong
|
||||||
|
import com.kunzisoft.encrypt.argon2.Argon2Transformer
|
||||||
|
import com.kunzisoft.encrypt.argon2.Argon2Type
|
||||||
|
import com.kunzisoft.keepass.utils.bytes16ToUuid
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -48,40 +48,30 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override val defaultKeyRounds: Long
|
override val defaultKeyRounds: Long
|
||||||
get() = DEFAULT_ITERATIONS
|
get() = DEFAULT_ITERATIONS.toKotlinLong()
|
||||||
|
|
||||||
override fun getName(resources: Resources): String {
|
|
||||||
return resources.getString(type.nameId)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun transform(masterKey: ByteArray, kdfParameters: KdfParameters): ByteArray {
|
override fun transform(masterKey: ByteArray, kdfParameters: KdfParameters): ByteArray {
|
||||||
|
|
||||||
val salt = kdfParameters.getByteArray(PARAM_SALT)
|
val salt = kdfParameters.getByteArray(PARAM_SALT) ?: ByteArray(0)
|
||||||
val parallelism = kdfParameters.getUInt32(PARAM_PARALLELISM)?.let {
|
val parallelism = kdfParameters.getUInt32(PARAM_PARALLELISM)?.toKotlinLong() ?: DEFAULT_PARALLELISM.toKotlinLong()
|
||||||
UnsignedInt(it)
|
val memory = kdfParameters.getUInt64(PARAM_MEMORY)?.toKotlinLong()?.div(MEMORY_BLOCK_SIZE) ?: DEFAULT_MEMORY.toKotlinLong()
|
||||||
}
|
val iterations = kdfParameters.getUInt64(PARAM_ITERATIONS)?.toKotlinLong() ?: DEFAULT_ITERATIONS.toKotlinLong()
|
||||||
val memory = kdfParameters.getUInt64(PARAM_MEMORY)?.div(MEMORY_BLOCK_SIZE)?.let {
|
val version = kdfParameters.getUInt32(PARAM_VERSION)?.toKotlinInt() ?: MAX_VERSION.toKotlinInt()
|
||||||
UnsignedInt.fromKotlinLong(it)
|
|
||||||
}
|
|
||||||
val iterations = kdfParameters.getUInt64(PARAM_ITERATIONS)?.let {
|
|
||||||
UnsignedInt.fromKotlinLong(it)
|
|
||||||
}
|
|
||||||
val version = kdfParameters.getUInt32(PARAM_VERSION)?.let {
|
|
||||||
UnsignedInt(it)
|
|
||||||
}
|
|
||||||
val secretKey = kdfParameters.getByteArray(PARAM_SECRET_KEY)
|
|
||||||
val assocData = kdfParameters.getByteArray(PARAM_ASSOC_DATA)
|
|
||||||
|
|
||||||
return Argon2Native.transformKey(
|
// Not used
|
||||||
type,
|
// val secretKey = kdfParameters.getByteArray(PARAM_SECRET_KEY)
|
||||||
|
// val assocData = kdfParameters.getByteArray(PARAM_ASSOC_DATA)
|
||||||
|
|
||||||
|
val argonType = if (type == Type.ARGON2_ID) Argon2Type.ARGON2_ID else Argon2Type.ARGON2_D
|
||||||
|
|
||||||
|
return Argon2Transformer.transformKey(
|
||||||
|
argonType,
|
||||||
masterKey,
|
masterKey,
|
||||||
salt,
|
salt,
|
||||||
parallelism,
|
parallelism,
|
||||||
memory,
|
memory,
|
||||||
iterations,
|
iterations,
|
||||||
secretKey,
|
|
||||||
assocData,
|
|
||||||
version)
|
version)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,32 +85,32 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getKeyRounds(kdfParameters: KdfParameters): Long {
|
override fun getKeyRounds(kdfParameters: KdfParameters): Long {
|
||||||
return kdfParameters.getUInt64(PARAM_ITERATIONS) ?: defaultKeyRounds
|
return kdfParameters.getUInt64(PARAM_ITERATIONS)?.toKotlinLong() ?: defaultKeyRounds
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setKeyRounds(kdfParameters: KdfParameters, keyRounds: Long) {
|
override fun setKeyRounds(kdfParameters: KdfParameters, keyRounds: Long) {
|
||||||
kdfParameters.setUInt64(PARAM_ITERATIONS, keyRounds)
|
kdfParameters.setUInt64(PARAM_ITERATIONS, UnsignedLong(keyRounds))
|
||||||
}
|
}
|
||||||
|
|
||||||
override val minKeyRounds: Long
|
override val minKeyRounds: Long
|
||||||
get() = MIN_ITERATIONS
|
get() = MIN_ITERATIONS.toKotlinLong()
|
||||||
|
|
||||||
override val maxKeyRounds: Long
|
override val maxKeyRounds: Long
|
||||||
get() = MAX_ITERATIONS
|
get() = MAX_ITERATIONS.toKotlinLong()
|
||||||
|
|
||||||
override fun getMemoryUsage(kdfParameters: KdfParameters): Long {
|
override fun getMemoryUsage(kdfParameters: KdfParameters): Long {
|
||||||
return kdfParameters.getUInt64(PARAM_MEMORY) ?: defaultMemoryUsage
|
return kdfParameters.getUInt64(PARAM_MEMORY)?.toKotlinLong() ?: defaultMemoryUsage
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setMemoryUsage(kdfParameters: KdfParameters, memory: Long) {
|
override fun setMemoryUsage(kdfParameters: KdfParameters, memory: Long) {
|
||||||
kdfParameters.setUInt64(PARAM_MEMORY, memory)
|
kdfParameters.setUInt64(PARAM_MEMORY, UnsignedLong(memory))
|
||||||
}
|
}
|
||||||
|
|
||||||
override val defaultMemoryUsage: Long
|
override val defaultMemoryUsage: Long
|
||||||
get() = DEFAULT_MEMORY
|
get() = DEFAULT_MEMORY.toKotlinLong()
|
||||||
|
|
||||||
override val minMemoryUsage: Long
|
override val minMemoryUsage: Long
|
||||||
get() = MIN_MEMORY
|
get() = MIN_MEMORY.toKotlinLong()
|
||||||
|
|
||||||
override val maxMemoryUsage: Long
|
override val maxMemoryUsage: Long
|
||||||
get() = MAX_MEMORY
|
get() = MAX_MEMORY
|
||||||
@@ -135,16 +125,20 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
|
|||||||
kdfParameters.setUInt32(PARAM_PARALLELISM, UnsignedInt.fromKotlinLong(parallelism))
|
kdfParameters.setUInt32(PARAM_PARALLELISM, UnsignedInt.fromKotlinLong(parallelism))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "$type"
|
||||||
|
}
|
||||||
|
|
||||||
override val defaultParallelism: Long
|
override val defaultParallelism: Long
|
||||||
get() = DEFAULT_PARALLELISM.toKotlinLong()
|
get() = DEFAULT_PARALLELISM.toKotlinLong()
|
||||||
|
|
||||||
override val minParallelism: Long
|
override val minParallelism: Long
|
||||||
get() = MIN_PARALLELISM
|
get() = MIN_PARALLELISM.toKotlinLong()
|
||||||
|
|
||||||
override val maxParallelism: Long
|
override val maxParallelism: Long
|
||||||
get() = MAX_PARALLELISM
|
get() = MAX_PARALLELISM.toKotlinLong()
|
||||||
|
|
||||||
enum class Type(val CIPHER_UUID: UUID, @StringRes val nameId: Int) {
|
enum class Type(val CIPHER_UUID: UUID, private val typeName: String) {
|
||||||
ARGON2_D(bytes16ToUuid(
|
ARGON2_D(bytes16ToUuid(
|
||||||
byteArrayOf(0xEF.toByte(),
|
byteArrayOf(0xEF.toByte(),
|
||||||
0x63.toByte(),
|
0x63.toByte(),
|
||||||
@@ -161,7 +155,7 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
|
|||||||
0x03.toByte(),
|
0x03.toByte(),
|
||||||
0xE3.toByte(),
|
0xE3.toByte(),
|
||||||
0x0A.toByte(),
|
0x0A.toByte(),
|
||||||
0x0C.toByte())), R.string.kdf_Argon2d),
|
0x0C.toByte())), "Argon2d"),
|
||||||
ARGON2_ID(bytes16ToUuid(
|
ARGON2_ID(bytes16ToUuid(
|
||||||
byteArrayOf(0x9E.toByte(),
|
byteArrayOf(0x9E.toByte(),
|
||||||
0x29.toByte(),
|
0x29.toByte(),
|
||||||
@@ -178,7 +172,11 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
|
|||||||
0xC6.toByte(),
|
0xC6.toByte(),
|
||||||
0xF0.toByte(),
|
0xF0.toByte(),
|
||||||
0xA1.toByte(),
|
0xA1.toByte(),
|
||||||
0xE6.toByte())), R.string.kdf_Argon2id);
|
0xE6.toByte())), "Argon2id");
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return typeName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -194,21 +192,17 @@ class Argon2Kdf(private val type: Type) : KdfEngine() {
|
|||||||
private val MIN_VERSION = UnsignedInt(0x10)
|
private val MIN_VERSION = UnsignedInt(0x10)
|
||||||
private val MAX_VERSION = UnsignedInt(0x13)
|
private val MAX_VERSION = UnsignedInt(0x13)
|
||||||
|
|
||||||
private const val MIN_SALT = 8
|
private val DEFAULT_ITERATIONS = UnsignedLong(2L)
|
||||||
private val MAX_SALT = UnsignedInt.MAX_VALUE.toKotlinLong()
|
private val MIN_ITERATIONS = UnsignedLong(1L)
|
||||||
|
private val MAX_ITERATIONS = UnsignedLong(4294967295L)
|
||||||
|
|
||||||
private const val MIN_ITERATIONS: Long = 1L
|
private val DEFAULT_MEMORY = UnsignedLong((1024L * 1024L))
|
||||||
private const val MAX_ITERATIONS = 4294967295L
|
private val MIN_MEMORY = UnsignedLong(1024L * 8L)
|
||||||
|
|
||||||
private const val MIN_MEMORY = (1024 * 8).toLong()
|
|
||||||
private val MAX_MEMORY = UnsignedInt.MAX_VALUE.toKotlinLong()
|
private val MAX_MEMORY = UnsignedInt.MAX_VALUE.toKotlinLong()
|
||||||
private const val MEMORY_BLOCK_SIZE: Long = 1024L
|
private const val MEMORY_BLOCK_SIZE: Long = 1024L
|
||||||
|
|
||||||
private const val MIN_PARALLELISM: Long = 1L
|
|
||||||
private const val MAX_PARALLELISM: Long = ((1 shl 24) - 1).toLong()
|
|
||||||
|
|
||||||
private const val DEFAULT_ITERATIONS: Long = 2L
|
|
||||||
private const val DEFAULT_MEMORY = (1024 * 1024).toLong()
|
|
||||||
private val DEFAULT_PARALLELISM = UnsignedInt(2)
|
private val DEFAULT_PARALLELISM = UnsignedInt(2)
|
||||||
|
private val MIN_PARALLELISM = UnsignedInt.fromKotlinLong(1L)
|
||||||
|
private val MAX_PARALLELISM = UnsignedInt.fromKotlinLong(((1 shl 24) - 1))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,17 +17,15 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto.keyDerivation
|
package com.kunzisoft.keepass.database.crypto.kdf
|
||||||
|
|
||||||
import com.kunzisoft.keepass.utils.ObjectNameResource
|
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import java.util.UUID
|
import java.util.*
|
||||||
|
|
||||||
// TODO Parcelable
|
// TODO Parcelable
|
||||||
abstract class KdfEngine : ObjectNameResource, Serializable {
|
abstract class KdfEngine : Serializable {
|
||||||
|
|
||||||
var uuid: UUID? = null
|
var uuid: UUID? = null
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto.keyDerivation
|
package com.kunzisoft.keepass.database.crypto.kdf
|
||||||
|
|
||||||
object KdfFactory {
|
object KdfFactory {
|
||||||
var aesKdf = AesKdf()
|
var aesKdf = AesKdf()
|
||||||
@@ -17,11 +17,11 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto.keyDerivation
|
package com.kunzisoft.keepass.database.crypto.kdf
|
||||||
|
|
||||||
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
import com.kunzisoft.keepass.utils.bytes16ToUuid
|
||||||
import com.kunzisoft.keepass.stream.uuidTo16Bytes
|
import com.kunzisoft.keepass.utils.uuidTo16Bytes
|
||||||
import com.kunzisoft.keepass.utils.VariantDictionary
|
import com.kunzisoft.keepass.database.crypto.VariantDictionary
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -23,7 +23,9 @@ import android.database.MatrixCursor
|
|||||||
import android.provider.BaseColumns
|
import android.provider.BaseColumns
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -49,12 +51,16 @@ abstract class EntryCursor<EntryId, PwEntryV : EntryVersioned<*, EntryId, *, *>>
|
|||||||
|
|
||||||
abstract fun getPwNodeId(): NodeId<EntryId>
|
abstract fun getPwNodeId(): NodeId<EntryId>
|
||||||
|
|
||||||
open fun populateEntry(pwEntry: PwEntryV, iconFactory: IconImageFactory) {
|
open fun populateEntry(pwEntry: PwEntryV,
|
||||||
|
retrieveStandardIcon: (Int) -> IconImageStandard,
|
||||||
|
retrieveCustomIcon: (UUID) -> IconImageCustom) {
|
||||||
pwEntry.nodeId = getPwNodeId()
|
pwEntry.nodeId = getPwNodeId()
|
||||||
pwEntry.title = getString(getColumnIndex(COLUMN_INDEX_TITLE))
|
pwEntry.title = getString(getColumnIndex(COLUMN_INDEX_TITLE))
|
||||||
|
|
||||||
val iconStandard = iconFactory.getIcon(getInt(getColumnIndex(COLUMN_INDEX_ICON_STANDARD)))
|
val iconStandard = retrieveStandardIcon.invoke(getInt(getColumnIndex(COLUMN_INDEX_ICON_STANDARD)))
|
||||||
pwEntry.icon = iconStandard
|
val iconCustom = retrieveCustomIcon.invoke(UUID(getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS)),
|
||||||
|
getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS))))
|
||||||
|
pwEntry.icon = IconImage(iconStandard, iconCustom)
|
||||||
|
|
||||||
pwEntry.username = getString(getColumnIndex(COLUMN_INDEX_USERNAME))
|
pwEntry.username = getString(getColumnIndex(COLUMN_INDEX_USERNAME))
|
||||||
pwEntry.password = getString(getColumnIndex(COLUMN_INDEX_PASSWORD))
|
pwEntry.password = getString(getColumnIndex(COLUMN_INDEX_PASSWORD))
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.cursor
|
package com.kunzisoft.keepass.database.cursor
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
|
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||||
|
|
||||||
class EntryCursorKDB : EntryCursorUUID<EntryKDB>() {
|
class EntryCursorKDB : EntryCursorUUID<EntryKDB>() {
|
||||||
@@ -30,9 +29,9 @@ class EntryCursorKDB : EntryCursorUUID<EntryKDB>() {
|
|||||||
entry.id.mostSignificantBits,
|
entry.id.mostSignificantBits,
|
||||||
entry.id.leastSignificantBits,
|
entry.id.leastSignificantBits,
|
||||||
entry.title,
|
entry.title,
|
||||||
entry.icon.iconId,
|
entry.icon.standard.id,
|
||||||
DatabaseVersioned.UUID_ZERO.mostSignificantBits,
|
entry.icon.custom.uuid.mostSignificantBits,
|
||||||
DatabaseVersioned.UUID_ZERO.leastSignificantBits,
|
entry.icon.custom.uuid.leastSignificantBits,
|
||||||
entry.username,
|
entry.username,
|
||||||
entry.password,
|
entry.password,
|
||||||
entry.url,
|
entry.url,
|
||||||
|
|||||||
@@ -20,9 +20,9 @@
|
|||||||
package com.kunzisoft.keepass.database.cursor
|
package com.kunzisoft.keepass.database.cursor
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
import java.util.UUID
|
import java.util.*
|
||||||
|
|
||||||
class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
|
class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
|
||||||
|
|
||||||
@@ -34,9 +34,9 @@ class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
|
|||||||
entry.id.mostSignificantBits,
|
entry.id.mostSignificantBits,
|
||||||
entry.id.leastSignificantBits,
|
entry.id.leastSignificantBits,
|
||||||
entry.title,
|
entry.title,
|
||||||
entry.icon.iconId,
|
entry.icon.standard.id,
|
||||||
entry.iconCustom.uuid.mostSignificantBits,
|
entry.icon.custom.uuid.mostSignificantBits,
|
||||||
entry.iconCustom.uuid.leastSignificantBits,
|
entry.icon.custom.uuid.leastSignificantBits,
|
||||||
entry.username,
|
entry.username,
|
||||||
entry.password,
|
entry.password,
|
||||||
entry.url,
|
entry.url,
|
||||||
@@ -52,14 +52,10 @@ class EntryCursorKDBX : EntryCursorUUID<EntryKDBX>() {
|
|||||||
entryId++
|
entryId++
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun populateEntry(pwEntry: EntryKDBX, iconFactory: IconImageFactory) {
|
override fun populateEntry(pwEntry: EntryKDBX,
|
||||||
super.populateEntry(pwEntry, iconFactory)
|
retrieveStandardIcon: (Int) -> IconImageStandard,
|
||||||
|
retrieveCustomIcon: (UUID) -> IconImageCustom) {
|
||||||
// Retrieve custom icon
|
super.populateEntry(pwEntry, retrieveStandardIcon, retrieveCustomIcon)
|
||||||
val iconCustom = iconFactory.getIcon(
|
|
||||||
UUID(getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS)),
|
|
||||||
getLong(getColumnIndex(COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS))))
|
|
||||||
pwEntry.iconCustom = iconCustom
|
|
||||||
|
|
||||||
// Retrieve extra fields
|
// Retrieve extra fields
|
||||||
if (extraFieldCursor.moveToFirst()) {
|
if (extraFieldCursor.moveToFirst()) {
|
||||||
|
|||||||
@@ -21,19 +21,21 @@ package com.kunzisoft.keepass.database.element
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
import com.kunzisoft.keepass.database.element.binary.BinaryByte
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
|
|
||||||
|
|
||||||
data class Attachment(var name: String,
|
data class Attachment(var name: String,
|
||||||
var binaryAttachment: BinaryAttachment) : Parcelable {
|
var binaryData: BinaryData) : Parcelable {
|
||||||
|
|
||||||
constructor(parcel: Parcel) : this(
|
constructor(parcel: Parcel) : this(
|
||||||
parcel.readString() ?: "",
|
parcel.readString() ?: "",
|
||||||
parcel.readParcelable(BinaryAttachment::class.java.classLoader) ?: BinaryAttachment()
|
parcel.readParcelable(BinaryData::class.java.classLoader) ?: BinaryByte()
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
parcel.writeString(name)
|
parcel.writeString(name)
|
||||||
parcel.writeParcelable(binaryAttachment, flags)
|
parcel.writeParcelable(binaryData, flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
override fun describeContents(): Int {
|
||||||
@@ -41,7 +43,7 @@ data class Attachment(var name: String,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "$name at $binaryAttachment"
|
return "$name at $binaryData"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
|||||||
@@ -23,17 +23,26 @@ import android.content.ContentResolver
|
|||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
|
||||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||||
import com.kunzisoft.keepass.database.element.database.*
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryCache
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
||||||
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||||
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconsManager
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
|
||||||
import com.kunzisoft.keepass.database.exception.*
|
import com.kunzisoft.keepass.database.exception.*
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
|
||||||
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
|
||||||
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB
|
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDB
|
||||||
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDBX
|
import com.kunzisoft.keepass.database.file.input.DatabaseInputKDBX
|
||||||
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB
|
import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDB
|
||||||
@@ -41,10 +50,11 @@ import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX
|
|||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
import com.kunzisoft.keepass.stream.readBytes4ToUInt
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
import com.kunzisoft.keepass.utils.SingletonHolder
|
import com.kunzisoft.keepass.utils.SingletonHolder
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.utils.readBytes4ToUInt
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
@@ -63,7 +73,10 @@ class Database {
|
|||||||
|
|
||||||
var isReadOnly = false
|
var isReadOnly = false
|
||||||
|
|
||||||
val drawFactory = IconDrawableFactory()
|
val iconDrawableFactory = IconDrawableFactory(
|
||||||
|
{ binaryCache },
|
||||||
|
{ iconId -> iconsManager.getBinaryForCustomIcon(iconId) }
|
||||||
|
)
|
||||||
|
|
||||||
var loaded = false
|
var loaded = false
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -71,13 +84,63 @@ class Database {
|
|||||||
loadTimestamp = if (field) System.currentTimeMillis() else null
|
loadTimestamp = if (field) System.currentTimeMillis() else null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To reload the main activity
|
||||||
|
*/
|
||||||
|
var wasReloaded = false
|
||||||
|
|
||||||
var loadTimestamp: Long? = null
|
var loadTimestamp: Long? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
val iconFactory: IconImageFactory
|
/**
|
||||||
get() {
|
* Cipher key regenerated when the database is loaded and closed
|
||||||
return mDatabaseKDB?.iconFactory ?: mDatabaseKDBX?.iconFactory ?: IconImageFactory()
|
* Can be used to temporarily store database elements
|
||||||
|
*/
|
||||||
|
var binaryCache: BinaryCache
|
||||||
|
private set(value) {
|
||||||
|
mDatabaseKDB?.binaryCache = value
|
||||||
|
mDatabaseKDBX?.binaryCache = value
|
||||||
}
|
}
|
||||||
|
get() {
|
||||||
|
return mDatabaseKDB?.binaryCache ?: mDatabaseKDBX?.binaryCache ?: BinaryCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val iconsManager: IconsManager
|
||||||
|
get() {
|
||||||
|
return mDatabaseKDB?.iconsManager ?: mDatabaseKDBX?.iconsManager ?: IconsManager(binaryCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doForEachStandardIcons(action: (IconImageStandard) -> Unit) {
|
||||||
|
return iconsManager.doForEachStandardIcon(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getStandardIcon(iconId: Int): IconImageStandard {
|
||||||
|
return iconsManager.getIcon(iconId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val allowCustomIcons: Boolean
|
||||||
|
get() = mDatabaseKDBX != null
|
||||||
|
|
||||||
|
fun doForEachCustomIcons(action: (IconImageCustom, BinaryData) -> Unit) {
|
||||||
|
return iconsManager.doForEachCustomIcon(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCustomIcon(iconId: UUID): IconImageCustom {
|
||||||
|
return iconsManager.getIcon(iconId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildNewCustomIcon(result: (IconImageCustom?, BinaryData?) -> Unit) {
|
||||||
|
mDatabaseKDBX?.buildNewCustomIcon(null, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean {
|
||||||
|
return mDatabaseKDBX?.isCustomIconBinaryDuplicate(binaryData) ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeCustomIcon(customIcon: IconImageCustom) {
|
||||||
|
iconDrawableFactory.clearFromCache(customIcon)
|
||||||
|
iconsManager.removeCustomIcon(binaryCache, customIcon.uuid)
|
||||||
|
}
|
||||||
|
|
||||||
val allowName: Boolean
|
val allowName: Boolean
|
||||||
get() = mDatabaseKDBX != null
|
get() = mDatabaseKDBX != null
|
||||||
@@ -159,7 +222,7 @@ class Database {
|
|||||||
// Default compression not necessary if stored in header
|
// Default compression not necessary if stored in header
|
||||||
mDatabaseKDBX?.let {
|
mDatabaseKDBX?.let {
|
||||||
return it.compressionAlgorithm == CompressionAlgorithm.GZip
|
return it.compressionAlgorithm == CompressionAlgorithm.GZip
|
||||||
&& it.kdbxVersion.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()
|
&& it.kdbxVersion.isBefore(FILE_VERSION_32_4)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -172,12 +235,9 @@ class Database {
|
|||||||
val allowNoMasterKey: Boolean
|
val allowNoMasterKey: Boolean
|
||||||
get() = mDatabaseKDBX != null
|
get() = mDatabaseKDBX != null
|
||||||
|
|
||||||
val allowEncryptionAlgorithmModification: Boolean
|
fun getEncryptionAlgorithmName(): String {
|
||||||
get() = availableEncryptionAlgorithms.size > 1
|
return mDatabaseKDB?.encryptionAlgorithm?.toString()
|
||||||
|
?: mDatabaseKDBX?.encryptionAlgorithm?.toString()
|
||||||
fun getEncryptionAlgorithmName(resources: Resources): String {
|
|
||||||
return mDatabaseKDB?.encryptionAlgorithm?.getName(resources)
|
|
||||||
?: mDatabaseKDBX?.encryptionAlgorithm?.getName(resources)
|
|
||||||
?: ""
|
?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +250,7 @@ class Database {
|
|||||||
algorithm?.let {
|
algorithm?.let {
|
||||||
mDatabaseKDBX?.encryptionAlgorithm = algorithm
|
mDatabaseKDBX?.encryptionAlgorithm = algorithm
|
||||||
mDatabaseKDBX?.setDataEngine(algorithm.cipherEngine)
|
mDatabaseKDBX?.setDataEngine(algorithm.cipherEngine)
|
||||||
mDatabaseKDBX?.dataCipher = algorithm.dataCipher
|
mDatabaseKDBX?.cipherUuid = algorithm.uuid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,8 +272,8 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getKeyDerivationName(resources: Resources): String {
|
fun getKeyDerivationName(): String {
|
||||||
return kdfEngine?.getName(resources) ?: ""
|
return kdfEngine?.toString() ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
var numberKeyEncryptionRounds: Long
|
var numberKeyEncryptionRounds: Long
|
||||||
@@ -299,15 +359,10 @@ class Database {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ensureRecycleBinExists(resources: Resources) {
|
val groupNamesNotAllowed: List<String>
|
||||||
mDatabaseKDB?.ensureBackupExists()
|
get() {
|
||||||
mDatabaseKDBX?.ensureRecycleBinExists(resources)
|
return mDatabaseKDB?.groupNamesNotAllowed ?: ArrayList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeRecycleBin() {
|
|
||||||
// Don't allow remove backup in KDB
|
|
||||||
mDatabaseKDBX?.removeRecycleBin()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setDatabaseKDB(databaseKDB: DatabaseKDB) {
|
private fun setDatabaseKDB(databaseKDB: DatabaseKDB) {
|
||||||
this.mDatabaseKDB = databaseKDB
|
this.mDatabaseKDB = databaseKDB
|
||||||
@@ -320,7 +375,8 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createData(databaseUri: Uri, databaseName: String, rootName: String) {
|
fun createData(databaseUri: Uri, databaseName: String, rootName: String) {
|
||||||
setDatabaseKDBX(DatabaseKDBX(databaseName, rootName))
|
val newDatabase = DatabaseKDBX(databaseName, rootName)
|
||||||
|
setDatabaseKDBX(newDatabase)
|
||||||
this.fileUri = databaseUri
|
this.fileUri = databaseUri
|
||||||
// Set Database state
|
// Set Database state
|
||||||
this.loaded = true
|
this.loaded = true
|
||||||
@@ -366,16 +422,21 @@ class Database {
|
|||||||
loaded = true
|
loaded = true
|
||||||
} catch (e: LoadDatabaseException) {
|
} catch (e: LoadDatabaseException) {
|
||||||
throw e
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw LoadDatabaseException(e)
|
||||||
} finally {
|
} finally {
|
||||||
databaseInputStream?.close()
|
databaseInputStream?.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
fun loadData(uri: Uri, password: String?, keyfile: Uri?,
|
fun loadData(uri: Uri,
|
||||||
|
mainCredential: MainCredential,
|
||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
contentResolver: ContentResolver,
|
contentResolver: ContentResolver,
|
||||||
cacheDirectory: File,
|
cacheDirectory: File,
|
||||||
|
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||||
|
tempCipherKey: LoadedKey,
|
||||||
fixDuplicateUUID: Boolean,
|
fixDuplicateUUID: Boolean,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||||
|
|
||||||
@@ -389,25 +450,27 @@ class Database {
|
|||||||
var keyFileInputStream: InputStream? = null
|
var keyFileInputStream: InputStream? = null
|
||||||
try {
|
try {
|
||||||
// Get keyFile inputStream
|
// Get keyFile inputStream
|
||||||
keyfile?.let {
|
mainCredential.keyFileUri?.let { keyFile ->
|
||||||
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyfile)
|
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read database stream for the first time
|
// Read database stream for the first time
|
||||||
readDatabaseStream(contentResolver, uri,
|
readDatabaseStream(contentResolver, uri,
|
||||||
{ databaseInputStream ->
|
{ databaseInputStream ->
|
||||||
DatabaseInputKDB(cacheDirectory)
|
DatabaseInputKDB(cacheDirectory, isRAMSufficient)
|
||||||
.openDatabase(databaseInputStream,
|
.openDatabase(databaseInputStream,
|
||||||
password,
|
mainCredential.masterPassword,
|
||||||
keyFileInputStream,
|
keyFileInputStream,
|
||||||
|
tempCipherKey,
|
||||||
progressTaskUpdater,
|
progressTaskUpdater,
|
||||||
fixDuplicateUUID)
|
fixDuplicateUUID)
|
||||||
},
|
},
|
||||||
{ databaseInputStream ->
|
{ databaseInputStream ->
|
||||||
DatabaseInputKDBX(cacheDirectory)
|
DatabaseInputKDBX(cacheDirectory, isRAMSufficient)
|
||||||
.openDatabase(databaseInputStream,
|
.openDatabase(databaseInputStream,
|
||||||
password,
|
mainCredential.masterPassword,
|
||||||
keyFileInputStream,
|
keyFileInputStream,
|
||||||
|
tempCipherKey,
|
||||||
progressTaskUpdater,
|
progressTaskUpdater,
|
||||||
fixDuplicateUUID)
|
fixDuplicateUUID)
|
||||||
}
|
}
|
||||||
@@ -427,27 +490,40 @@ class Database {
|
|||||||
@Throws(LoadDatabaseException::class)
|
@Throws(LoadDatabaseException::class)
|
||||||
fun reloadData(contentResolver: ContentResolver,
|
fun reloadData(contentResolver: ContentResolver,
|
||||||
cacheDirectory: File,
|
cacheDirectory: File,
|
||||||
|
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||||
|
tempCipherKey: LoadedKey,
|
||||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||||
|
|
||||||
// Retrieve the stream from the old database URI
|
// Retrieve the stream from the old database URI
|
||||||
fileUri?.let { oldDatabaseUri ->
|
try {
|
||||||
readDatabaseStream(contentResolver, oldDatabaseUri,
|
fileUri?.let { oldDatabaseUri ->
|
||||||
{ databaseInputStream ->
|
readDatabaseStream(contentResolver, oldDatabaseUri,
|
||||||
DatabaseInputKDB(cacheDirectory)
|
{ databaseInputStream ->
|
||||||
.openDatabase(databaseInputStream,
|
DatabaseInputKDB(cacheDirectory, isRAMSufficient)
|
||||||
masterKey,
|
.openDatabase(databaseInputStream,
|
||||||
progressTaskUpdater)
|
masterKey,
|
||||||
},
|
tempCipherKey,
|
||||||
{ databaseInputStream ->
|
progressTaskUpdater)
|
||||||
DatabaseInputKDBX(cacheDirectory)
|
},
|
||||||
.openDatabase(databaseInputStream,
|
{ databaseInputStream ->
|
||||||
masterKey,
|
DatabaseInputKDBX(cacheDirectory, isRAMSufficient)
|
||||||
progressTaskUpdater)
|
.openDatabase(databaseInputStream,
|
||||||
}
|
masterKey,
|
||||||
)
|
tempCipherKey,
|
||||||
} ?: run {
|
progressTaskUpdater)
|
||||||
Log.e(TAG, "Database URI is null, database cannot be reloaded")
|
}
|
||||||
throw IODatabaseException()
|
)
|
||||||
|
} ?: run {
|
||||||
|
Log.e(TAG, "Database URI is null, database cannot be reloaded")
|
||||||
|
throw IODatabaseException()
|
||||||
|
}
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
Log.e(TAG, "Unable to load keyfile", e)
|
||||||
|
throw FileNotFoundDatabaseException()
|
||||||
|
} catch (e: LoadDatabaseException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw LoadDatabaseException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,30 +537,33 @@ class Database {
|
|||||||
omitBackup: Boolean,
|
omitBackup: Boolean,
|
||||||
max: Int = Integer.MAX_VALUE): Group? {
|
max: Int = Integer.MAX_VALUE): Group? {
|
||||||
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
||||||
searchQuery, SearchParameters(), omitBackup, max)
|
SearchParameters().apply {
|
||||||
|
this.searchQuery = searchQuery
|
||||||
|
}, omitBackup, max)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createVirtualGroupFromSearchInfo(searchInfoString: String,
|
fun createVirtualGroupFromSearchInfo(searchInfoString: String,
|
||||||
omitBackup: Boolean,
|
omitBackup: Boolean,
|
||||||
max: Int = Integer.MAX_VALUE): Group? {
|
max: Int = Integer.MAX_VALUE): Group? {
|
||||||
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
||||||
searchInfoString, SearchParameters().apply {
|
SearchParameters().apply {
|
||||||
searchInTitles = true
|
searchQuery = searchInfoString
|
||||||
searchInUserNames = false
|
searchInTitles = true
|
||||||
searchInPasswords = false
|
searchInUserNames = false
|
||||||
searchInUrls = true
|
searchInPasswords = false
|
||||||
searchInNotes = true
|
searchInUrls = true
|
||||||
searchInOTP = false
|
searchInNotes = true
|
||||||
searchInOther = true
|
searchInOTP = false
|
||||||
searchInUUIDs = false
|
searchInOther = true
|
||||||
searchInTags = false
|
searchInUUIDs = false
|
||||||
ignoreCase = true
|
searchInTags = false
|
||||||
}, omitBackup, max)
|
ignoreCase = true
|
||||||
|
}, omitBackup, max)
|
||||||
}
|
}
|
||||||
|
|
||||||
val binaryPool: BinaryPool
|
val attachmentPool: AttachmentPool
|
||||||
get() {
|
get() {
|
||||||
return mDatabaseKDBX?.binaryPool ?: BinaryPool()
|
return mDatabaseKDB?.attachmentPool ?: mDatabaseKDBX?.attachmentPool ?: AttachmentPool(binaryCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
val allowMultipleAttachments: Boolean
|
val allowMultipleAttachments: Boolean
|
||||||
@@ -496,17 +575,16 @@ class Database {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildNewBinary(cacheDirectory: File,
|
fun buildNewBinaryAttachment(compressed: Boolean = false,
|
||||||
compressed: Boolean = false,
|
protected: Boolean = false): BinaryData? {
|
||||||
protected: Boolean = false): BinaryAttachment? {
|
return mDatabaseKDB?.buildNewAttachment()
|
||||||
return mDatabaseKDB?.buildNewBinary(cacheDirectory)
|
?: mDatabaseKDBX?.buildNewAttachment( false, compressed, protected)
|
||||||
?: mDatabaseKDBX?.buildNewBinary(cacheDirectory, compressed, protected)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAttachmentIfNotUsed(attachment: Attachment) {
|
fun removeAttachmentIfNotUsed(attachment: Attachment) {
|
||||||
// No need in KDB database because unique attachment by entry
|
// No need in KDB database because unique attachment by entry
|
||||||
// Don't clear to fix upload multiple times
|
// Don't clear to fix upload multiple times
|
||||||
mDatabaseKDBX?.removeUnlinkedAttachment(attachment.binaryAttachment, false)
|
mDatabaseKDBX?.removeUnlinkedAttachment(attachment.binaryData, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeUnlinkedAttachments() {
|
fun removeUnlinkedAttachments() {
|
||||||
@@ -575,7 +653,9 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun clear(filesDirectory: File? = null) {
|
fun clear(filesDirectory: File? = null) {
|
||||||
drawFactory.clearCache()
|
binaryCache.clear()
|
||||||
|
iconsManager.clearCache()
|
||||||
|
iconDrawableFactory.clearCache()
|
||||||
// Delete the cache of the database if present
|
// Delete the cache of the database if present
|
||||||
mDatabaseKDB?.clearCache()
|
mDatabaseKDB?.clearCache()
|
||||||
mDatabaseKDBX?.clearCache()
|
mDatabaseKDBX?.clearCache()
|
||||||
@@ -608,7 +688,9 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
|
fun validatePasswordEncoding(mainCredential: MainCredential): Boolean {
|
||||||
|
val password = mainCredential.masterPassword
|
||||||
|
val containsKeyFile = mainCredential.keyFileUri != null
|
||||||
return mDatabaseKDB?.validatePasswordEncoding(password, containsKeyFile)
|
return mDatabaseKDB?.validatePasswordEncoding(password, containsKeyFile)
|
||||||
?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile)
|
?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile)
|
||||||
?: false
|
?: false
|
||||||
@@ -706,11 +788,11 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addGroupTo(group: Group, parent: Group) {
|
fun addGroupTo(group: Group, parent: Group) {
|
||||||
group.groupKDB?.let { entryKDB ->
|
group.groupKDB?.let { groupKDB ->
|
||||||
mDatabaseKDB?.addGroupTo(entryKDB, parent.groupKDB)
|
mDatabaseKDB?.addGroupTo(groupKDB, parent.groupKDB)
|
||||||
}
|
}
|
||||||
group.groupKDBX?.let { entryKDBX ->
|
group.groupKDBX?.let { groupKDBX ->
|
||||||
mDatabaseKDBX?.addGroupTo(entryKDBX, parent.groupKDBX)
|
mDatabaseKDBX?.addGroupTo(groupKDBX, parent.groupKDBX)
|
||||||
}
|
}
|
||||||
group.afterAssignNewParent()
|
group.afterAssignNewParent()
|
||||||
}
|
}
|
||||||
@@ -725,11 +807,11 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun removeGroupFrom(group: Group, parent: Group) {
|
fun removeGroupFrom(group: Group, parent: Group) {
|
||||||
group.groupKDB?.let { entryKDB ->
|
group.groupKDB?.let { groupKDB ->
|
||||||
mDatabaseKDB?.removeGroupFrom(entryKDB, parent.groupKDB)
|
mDatabaseKDB?.removeGroupFrom(groupKDB, parent.groupKDB)
|
||||||
}
|
}
|
||||||
group.groupKDBX?.let { entryKDBX ->
|
group.groupKDBX?.let { groupKDBX ->
|
||||||
mDatabaseKDBX?.removeGroupFrom(entryKDBX, parent.groupKDBX)
|
mDatabaseKDBX?.removeGroupFrom(groupKDBX, parent.groupKDBX)
|
||||||
}
|
}
|
||||||
group.afterAssignNewParent()
|
group.afterAssignNewParent()
|
||||||
}
|
}
|
||||||
@@ -739,7 +821,7 @@ class Database {
|
|||||||
* @param entryToCopy
|
* @param entryToCopy
|
||||||
* @param newParent
|
* @param newParent
|
||||||
*/
|
*/
|
||||||
fun copyEntryTo(entryToCopy: Entry, newParent: Group): Entry? {
|
fun copyEntryTo(entryToCopy: Entry, newParent: Group): Entry {
|
||||||
val entryCopied = Entry(entryToCopy, false)
|
val entryCopied = Entry(entryToCopy, false)
|
||||||
entryCopied.nodeId = mDatabaseKDB?.newEntryId() ?: mDatabaseKDBX?.newEntryId() ?: NodeIdUUID()
|
entryCopied.nodeId = mDatabaseKDB?.newEntryId() ?: mDatabaseKDBX?.newEntryId() ?: NodeIdUUID()
|
||||||
entryCopied.parent = newParent
|
entryCopied.parent = newParent
|
||||||
@@ -804,6 +886,16 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ensureRecycleBinExists(resources: Resources) {
|
||||||
|
mDatabaseKDB?.ensureBackupExists()
|
||||||
|
mDatabaseKDBX?.ensureRecycleBinExists(resources)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeRecycleBin() {
|
||||||
|
// Don't allow remove backup in KDB
|
||||||
|
mDatabaseKDBX?.removeRecycleBin()
|
||||||
|
}
|
||||||
|
|
||||||
fun canRecycle(entry: Entry): Boolean {
|
fun canRecycle(entry: Entry): Boolean {
|
||||||
var canRecycle: Boolean? = null
|
var canRecycle: Boolean? = null
|
||||||
entry.entryKDB?.let {
|
entry.entryKDB?.let {
|
||||||
@@ -896,7 +988,7 @@ class Database {
|
|||||||
rootGroup?.doForEachChildAndForIt(
|
rootGroup?.doForEachChildAndForIt(
|
||||||
object : NodeHandler<Entry>() {
|
object : NodeHandler<Entry>() {
|
||||||
override fun operate(node: Entry): Boolean {
|
override fun operate(node: Entry): Boolean {
|
||||||
removeOldestEntryHistory(node, binaryPool)
|
removeOldestEntryHistory(node, attachmentPool)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -911,7 +1003,7 @@ class Database {
|
|||||||
/**
|
/**
|
||||||
* Remove oldest history if more than max items or max memory
|
* Remove oldest history if more than max items or max memory
|
||||||
*/
|
*/
|
||||||
fun removeOldestEntryHistory(entry: Entry, binaryPool: BinaryPool) {
|
fun removeOldestEntryHistory(entry: Entry, attachmentPool: AttachmentPool) {
|
||||||
mDatabaseKDBX?.let {
|
mDatabaseKDBX?.let {
|
||||||
val maxItems = historyMaxItems
|
val maxItems = historyMaxItems
|
||||||
if (maxItems >= 0) {
|
if (maxItems >= 0) {
|
||||||
@@ -925,7 +1017,7 @@ class Database {
|
|||||||
while (true) {
|
while (true) {
|
||||||
var historySize: Long = 0
|
var historySize: Long = 0
|
||||||
for (entryHistory in entry.getHistory()) {
|
for (entryHistory in entry.getHistory()) {
|
||||||
historySize += entryHistory.getSize(binaryPool)
|
historySize += entryHistory.getSize(attachmentPool)
|
||||||
}
|
}
|
||||||
if (historySize > maxSize) {
|
if (historySize > maxSize) {
|
||||||
removeOldestEntryHistory(entry)
|
removeOldestEntryHistory(entry)
|
||||||
@@ -939,7 +1031,7 @@ class Database {
|
|||||||
|
|
||||||
private fun removeOldestEntryHistory(entry: Entry) {
|
private fun removeOldestEntryHistory(entry: Entry) {
|
||||||
entry.removeOldestEntryFromHistory()?.let {
|
entry.removeOldestEntryFromHistory()?.let {
|
||||||
it.getAttachments(binaryPool, false).forEach { attachmentToRemove ->
|
it.getAttachments(attachmentPool, false).forEach { attachmentToRemove ->
|
||||||
removeAttachmentIfNotUsed(attachmentToRemove)
|
removeAttachmentIfNotUsed(attachmentToRemove)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -947,7 +1039,7 @@ class Database {
|
|||||||
|
|
||||||
fun removeEntryHistory(entry: Entry, entryHistoryPosition: Int) {
|
fun removeEntryHistory(entry: Entry, entryHistoryPosition: Int) {
|
||||||
entry.removeEntryFromHistory(entryHistoryPosition)?.let {
|
entry.removeEntryFromHistory(entryHistoryPosition)?.let {
|
||||||
it.getAttachments(binaryPool, false).forEach { attachmentToRemove ->
|
it.getAttachments(attachmentPool, false).forEach { attachmentToRemove ->
|
||||||
removeAttachmentIfNotUsed(attachmentToRemove)
|
removeAttachmentIfNotUsed(attachmentToRemove)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,12 @@ package com.kunzisoft.keepass.database.element
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.database.BinaryPool
|
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface
|
import com.kunzisoft.keepass.database.element.entry.EntryVersionedInterface
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
@@ -109,7 +107,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
|
|
||||||
override var icon: IconImage
|
override var icon: IconImage
|
||||||
get() {
|
get() {
|
||||||
return entryKDB?.icon ?: entryKDBX?.icon ?: IconImageStandard()
|
return entryKDB?.icon ?: entryKDBX?.icon ?: IconImage()
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
entryKDB?.icon = value
|
entryKDB?.icon = value
|
||||||
@@ -257,31 +255,12 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
------------
|
|
||||||
KDB Methods
|
|
||||||
------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If it's a node with only meta information like Meta-info SYSTEM Database Color
|
|
||||||
* @return false by default, true if it's a meta stream
|
|
||||||
*/
|
|
||||||
val isMetaStream: Boolean
|
|
||||||
get() = entryKDB?.isMetaStream ?: false
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
------------
|
------------
|
||||||
KDBX Methods
|
KDBX Methods
|
||||||
------------
|
------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var iconCustom: IconImageCustom
|
|
||||||
get() = entryKDBX?.iconCustom ?: IconImageCustom.UNKNOWN_ICON
|
|
||||||
set(value) {
|
|
||||||
entryKDBX?.iconCustom = value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve extra fields to show, key is the label, value is the value of field (protected or not)
|
* Retrieve extra fields to show, key is the label, value is the value of field (protected or not)
|
||||||
* @return Map of label/value
|
* @return Map of label/value
|
||||||
@@ -330,12 +309,12 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
entryKDBX?.stopToManageFieldReferences()
|
entryKDBX?.stopToManageFieldReferences()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List<Attachment> {
|
fun getAttachments(attachmentPool: AttachmentPool, inHistory: Boolean = false): List<Attachment> {
|
||||||
val attachments = ArrayList<Attachment>()
|
val attachments = ArrayList<Attachment>()
|
||||||
entryKDB?.getAttachment()?.let {
|
entryKDB?.getAttachment(attachmentPool)?.let {
|
||||||
attachments.add(it)
|
attachments.add(it)
|
||||||
}
|
}
|
||||||
entryKDBX?.getAttachments(binaryPool, inHistory)?.let {
|
entryKDBX?.getAttachments(attachmentPool, inHistory)?.let {
|
||||||
attachments.addAll(it)
|
attachments.addAll(it)
|
||||||
}
|
}
|
||||||
return attachments
|
return attachments
|
||||||
@@ -346,12 +325,6 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
|| entryKDBX?.containsAttachment() == true
|
|| entryKDBX?.containsAttachment() == true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addAttachments(binaryPool: BinaryPool, attachments: List<Attachment>) {
|
|
||||||
attachments.forEach {
|
|
||||||
putAttachment(it, binaryPool)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun removeAttachment(attachment: Attachment) {
|
private fun removeAttachment(attachment: Attachment) {
|
||||||
entryKDB?.removeAttachment(attachment)
|
entryKDB?.removeAttachment(attachment)
|
||||||
entryKDBX?.removeAttachment(attachment)
|
entryKDBX?.removeAttachment(attachment)
|
||||||
@@ -362,9 +335,9 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
entryKDBX?.removeAttachments()
|
entryKDBX?.removeAttachments()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) {
|
private fun putAttachment(attachment: Attachment, attachmentPool: AttachmentPool) {
|
||||||
entryKDB?.putAttachment(attachment)
|
entryKDB?.putAttachment(attachment, attachmentPool)
|
||||||
entryKDBX?.putAttachment(attachment, binaryPool)
|
entryKDBX?.putAttachment(attachment, attachmentPool)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getHistory(): ArrayList<Entry> {
|
fun getHistory(): ArrayList<Entry> {
|
||||||
@@ -396,8 +369,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSize(binaryPool: BinaryPool): Long {
|
fun getSize(attachmentPool: AttachmentPool): Long {
|
||||||
return entryKDBX?.getSize(binaryPool) ?: 0L
|
return entryKDBX?.getSize(attachmentPool) ?: 0L
|
||||||
}
|
}
|
||||||
|
|
||||||
fun containsCustomData(): Boolean {
|
fun containsCustomData(): Boolean {
|
||||||
@@ -427,7 +400,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
entryInfo.username = username
|
entryInfo.username = username
|
||||||
entryInfo.password = password
|
entryInfo.password = password
|
||||||
entryInfo.creationTime = creationTime
|
entryInfo.creationTime = creationTime
|
||||||
entryInfo.modificationTime = lastModificationTime
|
entryInfo.lastModificationTime = lastModificationTime
|
||||||
entryInfo.expires = expires
|
entryInfo.expires = expires
|
||||||
entryInfo.expiryTime = expiryTime
|
entryInfo.expiryTime = expiryTime
|
||||||
entryInfo.url = url
|
entryInfo.url = url
|
||||||
@@ -439,7 +412,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
// Replace parameter fields by generated OTP fields
|
// Replace parameter fields by generated OTP fields
|
||||||
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
|
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
|
||||||
}
|
}
|
||||||
database?.binaryPool?.let { binaryPool ->
|
database?.attachmentPool?.let { binaryPool ->
|
||||||
entryInfo.attachments = getAttachments(binaryPool)
|
entryInfo.attachments = getAttachments(binaryPool)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,8 +439,10 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
url = newEntryInfo.url
|
url = newEntryInfo.url
|
||||||
notes = newEntryInfo.notes
|
notes = newEntryInfo.notes
|
||||||
addExtraFields(newEntryInfo.customFields)
|
addExtraFields(newEntryInfo.customFields)
|
||||||
database?.binaryPool?.let { binaryPool ->
|
database?.attachmentPool?.let { binaryPool ->
|
||||||
addAttachments(binaryPool, newEntryInfo.attachments)
|
newEntryInfo.attachments.forEach { attachment ->
|
||||||
|
putAttachment(attachment, binaryPool)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
database?.stopManageEntry(this)
|
database?.stopManageEntry(this)
|
||||||
@@ -491,16 +466,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
companion object CREATOR : Parcelable.Creator<Entry> {
|
|
||||||
override fun createFromParcel(parcel: Parcel): Entry {
|
|
||||||
return Entry(parcel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newArray(size: Int): Array<Entry?> {
|
|
||||||
return arrayOfNulls(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
const val PMS_TAN_ENTRY = "<TAN>"
|
const val PMS_TAN_ENTRY = "<TAN>"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -509,5 +475,16 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
fun newExtraFieldNameAllowed(field: Field): Boolean {
|
fun newExtraFieldNameAllowed(field: Field): Boolean {
|
||||||
return EntryKDBX.newCustomNameAllowed(field.name)
|
return EntryKDBX.newCustomNameAllowed(field.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val CREATOR: Parcelable.Creator<Entry> = object : Parcelable.Creator<Entry> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): Entry {
|
||||||
|
return Entry(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<Entry?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ import com.kunzisoft.keepass.database.element.group.GroupKDB
|
|||||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
|
import com.kunzisoft.keepass.database.element.group.GroupVersionedInterface
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
|
||||||
import com.kunzisoft.keepass.database.element.node.*
|
import com.kunzisoft.keepass.database.element.node.*
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
@@ -40,6 +40,9 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
var groupKDBX: GroupKDBX? = null
|
var groupKDBX: GroupKDBX? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
// Virtual group is used to defined a detached database group
|
||||||
|
var isVirtual = false
|
||||||
|
|
||||||
fun updateWith(group: Group) {
|
fun updateWith(group: Group) {
|
||||||
group.groupKDB?.let {
|
group.groupKDB?.let {
|
||||||
this.groupKDB?.updateWith(it)
|
this.groupKDB?.updateWith(it)
|
||||||
@@ -77,6 +80,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
constructor(parcel: Parcel) {
|
constructor(parcel: Parcel) {
|
||||||
groupKDB = parcel.readParcelable(GroupKDB::class.java.classLoader)
|
groupKDB = parcel.readParcelable(GroupKDB::class.java.classLoader)
|
||||||
groupKDBX = parcel.readParcelable(GroupKDBX::class.java.classLoader)
|
groupKDBX = parcel.readParcelable(GroupKDBX::class.java.classLoader)
|
||||||
|
isVirtual = parcel.readByte().toInt() != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ChildFilter {
|
enum class ChildFilter {
|
||||||
@@ -110,6 +114,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
dest.writeParcelable(groupKDB, flags)
|
dest.writeParcelable(groupKDB, flags)
|
||||||
dest.writeParcelable(groupKDBX, flags)
|
dest.writeParcelable(groupKDBX, flags)
|
||||||
|
dest.writeByte((if (isVirtual) 1 else 0).toByte())
|
||||||
}
|
}
|
||||||
|
|
||||||
override val nodeId: NodeId<*>?
|
override val nodeId: NodeId<*>?
|
||||||
@@ -123,7 +128,7 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override var icon: IconImage
|
override var icon: IconImage
|
||||||
get() = groupKDB?.icon ?: groupKDBX?.icon ?: IconImageStandard()
|
get() = groupKDB?.icon ?: groupKDBX?.icon ?: IconImage()
|
||||||
set(value) {
|
set(value) {
|
||||||
groupKDB?.icon = value
|
groupKDB?.icon = value
|
||||||
groupKDBX?.icon = value
|
groupKDBX?.icon = value
|
||||||
@@ -232,6 +237,14 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
override val isCurrentlyExpires: Boolean
|
override val isCurrentlyExpires: Boolean
|
||||||
get() = groupKDB?.isCurrentlyExpires ?: groupKDBX?.isCurrentlyExpires ?: false
|
get() = groupKDB?.isCurrentlyExpires ?: groupKDBX?.isCurrentlyExpires ?: false
|
||||||
|
|
||||||
|
var notes: String?
|
||||||
|
get() = groupKDBX?.notes
|
||||||
|
set(value) {
|
||||||
|
value?.let {
|
||||||
|
groupKDBX?.notes = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getChildGroups(): List<Group> {
|
override fun getChildGroups(): List<Group> {
|
||||||
return groupKDB?.getChildGroups()?.map {
|
return groupKDB?.getChildGroups()?.map {
|
||||||
Group(it)
|
Group(it)
|
||||||
@@ -335,9 +348,11 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
groupKDBX?.removeChildren()
|
groupKDBX?.removeChildren()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun allowAddEntryIfIsRoot(): Boolean {
|
val allowAddEntryIfIsRoot: Boolean
|
||||||
return groupKDB?.allowAddEntryIfIsRoot() ?: groupKDBX?.allowAddEntryIfIsRoot() ?: false
|
get() = groupKDBX != null
|
||||||
}
|
|
||||||
|
val allowAddNoteInGroup: Boolean
|
||||||
|
get() = groupKDBX != null
|
||||||
|
|
||||||
/*
|
/*
|
||||||
------------
|
------------
|
||||||
@@ -353,14 +368,6 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
groupKDB?.nodeId = id
|
groupKDB?.nodeId = id
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLevel(): Int {
|
|
||||||
return groupKDB?.level ?: -1
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setLevel(level: Int) {
|
|
||||||
groupKDB?.level = level
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
------------
|
------------
|
||||||
KDBX Methods
|
KDBX Methods
|
||||||
@@ -391,6 +398,35 @@ class Group : Node, GroupVersionedInterface<Group, Entry> {
|
|||||||
return groupKDBX?.containsCustomData() ?: false
|
return groupKDBX?.containsCustomData() ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
------------
|
||||||
|
Converter
|
||||||
|
------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun getGroupInfo(): GroupInfo {
|
||||||
|
val groupInfo = GroupInfo()
|
||||||
|
groupInfo.title = title
|
||||||
|
groupInfo.icon = icon
|
||||||
|
groupInfo.creationTime = creationTime
|
||||||
|
groupInfo.lastModificationTime = lastModificationTime
|
||||||
|
groupInfo.expires = expires
|
||||||
|
groupInfo.expiryTime = expiryTime
|
||||||
|
groupInfo.notes = notes
|
||||||
|
return groupInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setGroupInfo(groupInfo: GroupInfo) {
|
||||||
|
title = groupInfo.title
|
||||||
|
icon = groupInfo.icon
|
||||||
|
// Update date time, creation time stay as is
|
||||||
|
lastModificationTime = DateInstant()
|
||||||
|
lastAccessTime = DateInstant()
|
||||||
|
expires = groupInfo.expires
|
||||||
|
expiryTime = groupInfo.expiryTime
|
||||||
|
notes = groupInfo.notes
|
||||||
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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.database.element.binary
|
||||||
|
|
||||||
|
class AttachmentPool(binaryCache: BinaryCache) : BinaryPool<Int>(binaryCache) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to find an unused key in the pool
|
||||||
|
*/
|
||||||
|
override fun findUnusedKey(): Int {
|
||||||
|
var unusedKey = 0
|
||||||
|
while (pool[unusedKey] != null)
|
||||||
|
unusedKey++
|
||||||
|
return unusedKey
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To register a binary with a ref corresponding to an ordered index
|
||||||
|
*/
|
||||||
|
fun getBinaryIndexFromKey(key: Int): Int? {
|
||||||
|
val index = orderedBinariesWithoutDuplication().indexOfFirst { it.keys.contains(key) }
|
||||||
|
return if (index < 0)
|
||||||
|
null
|
||||||
|
else
|
||||||
|
index
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.database.element.binary
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.Base64InputStream
|
||||||
|
import android.util.Base64OutputStream
|
||||||
|
import com.kunzisoft.keepass.utils.readAllBytes
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryCache.Companion.UNKNOWN
|
||||||
|
import java.io.*
|
||||||
|
import java.util.zip.GZIPOutputStream
|
||||||
|
|
||||||
|
class BinaryByte : BinaryData {
|
||||||
|
|
||||||
|
private var mDataByteId: String
|
||||||
|
|
||||||
|
private fun getByteArray(binaryCache: BinaryCache): ByteArray {
|
||||||
|
val keyData = binaryCache.getByteArray(mDataByteId)
|
||||||
|
mDataByteId = keyData.key
|
||||||
|
return keyData.data
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() : super() {
|
||||||
|
mDataByteId = UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(id: String,
|
||||||
|
compressed: Boolean = false,
|
||||||
|
protected: Boolean = false) : super(compressed, protected) {
|
||||||
|
mDataByteId = id
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(parcel: Parcel) : super(parcel) {
|
||||||
|
mDataByteId = parcel.readString() ?: UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
|
super.writeToParcel(dest, flags)
|
||||||
|
dest.writeString(mDataByteId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun getInputDataStream(binaryCache: BinaryCache): InputStream {
|
||||||
|
return Base64InputStream(ByteArrayInputStream(getByteArray(binaryCache)), Base64.NO_WRAP)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun getOutputDataStream(binaryCache: BinaryCache): OutputStream {
|
||||||
|
return BinaryCountingOutputStream(Base64OutputStream(ByteOutputStream(binaryCache), Base64.NO_WRAP))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun compress(binaryCache: BinaryCache) {
|
||||||
|
if (!isCompressed) {
|
||||||
|
GZIPOutputStream(getOutputDataStream(binaryCache)).use { outputStream ->
|
||||||
|
getInputDataStream(binaryCache).use { inputStream ->
|
||||||
|
inputStream.readAllBytes { buffer ->
|
||||||
|
outputStream.write(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isCompressed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun decompress(binaryCache: BinaryCache) {
|
||||||
|
if (isCompressed) {
|
||||||
|
getUnGzipInputDataStream(binaryCache).use { inputStream ->
|
||||||
|
getOutputDataStream(binaryCache).use { outputStream ->
|
||||||
|
inputStream.readAllBytes { buffer ->
|
||||||
|
outputStream.write(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isCompressed = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun clear(binaryCache: BinaryCache) {
|
||||||
|
binaryCache.removeByteArray(mDataByteId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is BinaryByte) return false
|
||||||
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
|
if (mDataByteId != other.mDataByteId) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = super.hashCode()
|
||||||
|
result = 31 * result + mDataByteId.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom OutputStream to calculate the size and hash of binary file
|
||||||
|
*/
|
||||||
|
private inner class ByteOutputStream(private val binaryCache: BinaryCache) : ByteArrayOutputStream() {
|
||||||
|
override fun close() {
|
||||||
|
binaryCache.setByteArray(mDataByteId, this.toByteArray())
|
||||||
|
super.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = BinaryByte::class.java.name
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val CREATOR: Parcelable.Creator<BinaryByte> = object : Parcelable.Creator<BinaryByte> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): BinaryByte {
|
||||||
|
return BinaryByte(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<BinaryByte?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.kunzisoft.keepass.database.element.binary
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class BinaryCache {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cipher key generated when the database is loaded, and destroyed when the database is closed
|
||||||
|
* Can be used to temporarily store database elements
|
||||||
|
*/
|
||||||
|
var loadedCipherKey: LoadedKey = LoadedKey.generateNewCipherKey()
|
||||||
|
|
||||||
|
var cacheDirectory: File? = null
|
||||||
|
|
||||||
|
private val voidBinary = KeyByteArray(UNKNOWN, ByteArray(0))
|
||||||
|
|
||||||
|
fun getBinaryData(binaryId: String,
|
||||||
|
smallSize: Boolean = false,
|
||||||
|
compression: Boolean = false,
|
||||||
|
protection: Boolean = false): BinaryData {
|
||||||
|
val cacheDir = cacheDirectory
|
||||||
|
return if (smallSize || cacheDir == null) {
|
||||||
|
BinaryByte(binaryId, compression, protection)
|
||||||
|
} else {
|
||||||
|
val fileInCache = File(cacheDir, binaryId)
|
||||||
|
BinaryFile(fileInCache, compression, protection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Similar to file storage but much faster TODO SparseArray
|
||||||
|
private val byteArrayList = HashMap<String, ByteArray>()
|
||||||
|
|
||||||
|
fun getByteArray(key: String): KeyByteArray {
|
||||||
|
if (key == UNKNOWN) {
|
||||||
|
return voidBinary
|
||||||
|
}
|
||||||
|
if (!byteArrayList.containsKey(key)) {
|
||||||
|
val newItem = KeyByteArray(key, ByteArray(0))
|
||||||
|
byteArrayList[newItem.key] = newItem.data
|
||||||
|
return newItem
|
||||||
|
}
|
||||||
|
return KeyByteArray(key, byteArrayList[key]!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setByteArray(key: String, data: ByteArray): KeyByteArray {
|
||||||
|
if (key == UNKNOWN) {
|
||||||
|
return voidBinary
|
||||||
|
}
|
||||||
|
byteArrayList[key] = data
|
||||||
|
return KeyByteArray(key, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeByteArray(key: String?) {
|
||||||
|
key?.let {
|
||||||
|
byteArrayList.remove(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
byteArrayList.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val UNKNOWN = "UNKNOWN"
|
||||||
|
}
|
||||||
|
|
||||||
|
data class KeyByteArray(val key: String, val data: ByteArray) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is KeyByteArray) return false
|
||||||
|
|
||||||
|
if (key != other.key) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return key.hashCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.database.element.binary
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import org.apache.commons.io.output.CountingOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.util.zip.GZIPInputStream
|
||||||
|
import java.util.zip.GZIPOutputStream
|
||||||
|
|
||||||
|
abstract class BinaryData : Parcelable {
|
||||||
|
|
||||||
|
var isCompressed: Boolean = false
|
||||||
|
protected set
|
||||||
|
var isProtected: Boolean = false
|
||||||
|
protected set
|
||||||
|
var isCorrupted: Boolean = false
|
||||||
|
private var mLength: Long = 0
|
||||||
|
private var mBinaryHash = 0
|
||||||
|
|
||||||
|
protected constructor(compressed: Boolean = false, protected: Boolean = false) {
|
||||||
|
this.isCompressed = compressed
|
||||||
|
this.isProtected = protected
|
||||||
|
this.mLength = 0
|
||||||
|
this.mBinaryHash = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
protected constructor(parcel: Parcel) {
|
||||||
|
isCompressed = parcel.readByte().toInt() != 0
|
||||||
|
isProtected = parcel.readByte().toInt() != 0
|
||||||
|
isCorrupted = parcel.readByte().toInt() != 0
|
||||||
|
mLength = parcel.readLong()
|
||||||
|
mBinaryHash = parcel.readInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
|
dest.writeByte((if (isCompressed) 1 else 0).toByte())
|
||||||
|
dest.writeByte((if (isProtected) 1 else 0).toByte())
|
||||||
|
dest.writeByte((if (isCorrupted) 1 else 0).toByte())
|
||||||
|
dest.writeLong(mLength)
|
||||||
|
dest.writeInt(mBinaryHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
abstract fun getInputDataStream(binaryCache: BinaryCache): InputStream
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
abstract fun getOutputDataStream(binaryCache: BinaryCache): OutputStream
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getUnGzipInputDataStream(binaryCache: BinaryCache): InputStream {
|
||||||
|
return if (isCompressed) {
|
||||||
|
GZIPInputStream(getInputDataStream(binaryCache))
|
||||||
|
} else {
|
||||||
|
getInputDataStream(binaryCache)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getGzipOutputDataStream(binaryCache: BinaryCache): OutputStream {
|
||||||
|
return if (isCompressed) {
|
||||||
|
GZIPOutputStream(getOutputDataStream(binaryCache))
|
||||||
|
} else {
|
||||||
|
getOutputDataStream(binaryCache)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
abstract fun compress(binaryCache: BinaryCache)
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
abstract fun decompress(binaryCache: BinaryCache)
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun dataExists(): Boolean {
|
||||||
|
return mLength > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getSize(): Long {
|
||||||
|
return mLength
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun binaryHash(): Int {
|
||||||
|
return mBinaryHash
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
abstract fun clear(binaryCache: BinaryCache)
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is BinaryData) return false
|
||||||
|
|
||||||
|
if (isCompressed != other.isCompressed) return false
|
||||||
|
if (isProtected != other.isProtected) return false
|
||||||
|
if (isCorrupted != other.isCorrupted) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = isCompressed.hashCode()
|
||||||
|
result = 31 * result + isProtected.hashCode()
|
||||||
|
result = 31 * result + isCorrupted.hashCode()
|
||||||
|
result = 31 * result + mLength.hashCode()
|
||||||
|
result = 31 * result + mBinaryHash
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom OutputStream to calculate the size and hash of binary file
|
||||||
|
*/
|
||||||
|
protected inner class BinaryCountingOutputStream(out: OutputStream): CountingOutputStream(out) {
|
||||||
|
|
||||||
|
private val mMessageDigest: MessageDigest
|
||||||
|
init {
|
||||||
|
mLength = 0
|
||||||
|
mMessageDigest = MessageDigest.getInstance("MD5")
|
||||||
|
mBinaryHash = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beforeWrite(n: Int) {
|
||||||
|
super.beforeWrite(n)
|
||||||
|
mLength = byteCount
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(idx: Int) {
|
||||||
|
super.write(idx)
|
||||||
|
mMessageDigest.update(idx.toByte())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(bts: ByteArray) {
|
||||||
|
super.write(bts)
|
||||||
|
mMessageDigest.update(bts)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(bts: ByteArray, st: Int, end: Int) {
|
||||||
|
super.write(bts, st, end)
|
||||||
|
mMessageDigest.update(bts, st, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
super.close()
|
||||||
|
mLength = byteCount
|
||||||
|
val bytes = mMessageDigest.digest()
|
||||||
|
mBinaryHash = ByteBuffer.wrap(bytes).int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = BinaryData::class.java.name
|
||||||
|
|
||||||
|
fun canMemoryBeAllocatedInRAM(context: Context, memoryWanted: Long): Boolean {
|
||||||
|
val memoryInfo = ActivityManager.MemoryInfo()
|
||||||
|
(context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).getMemoryInfo(memoryInfo)
|
||||||
|
val availableMemory = memoryInfo.availMem
|
||||||
|
return availableMemory > memoryWanted * 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.database.element.binary
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.Base64InputStream
|
||||||
|
import android.util.Base64OutputStream
|
||||||
|
import com.kunzisoft.keepass.utils.readAllBytes
|
||||||
|
import java.io.*
|
||||||
|
import java.util.zip.GZIPOutputStream
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.CipherInputStream
|
||||||
|
import javax.crypto.CipherOutputStream
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
|
||||||
|
class BinaryFile : BinaryData {
|
||||||
|
|
||||||
|
private var mDataFile: File? = null
|
||||||
|
|
||||||
|
// Cipher to encrypt temp file
|
||||||
|
@Transient
|
||||||
|
private var cipherEncryption: Cipher = Cipher.getInstance(LoadedKey.BINARY_CIPHER)
|
||||||
|
@Transient
|
||||||
|
private var cipherDecryption: Cipher = Cipher.getInstance(LoadedKey.BINARY_CIPHER)
|
||||||
|
|
||||||
|
constructor(dataFile: File,
|
||||||
|
compressed: Boolean = false,
|
||||||
|
protected: Boolean = false) : super(compressed, protected) {
|
||||||
|
this.mDataFile = dataFile
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(parcel: Parcel) : super(parcel) {
|
||||||
|
parcel.readString()?.let {
|
||||||
|
mDataFile = File(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
|
super.writeToParcel(dest, flags)
|
||||||
|
dest.writeString(mDataFile?.absolutePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun getInputDataStream(binaryCache: BinaryCache): InputStream {
|
||||||
|
return buildInputStream(mDataFile, binaryCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun getOutputDataStream(binaryCache: BinaryCache): OutputStream {
|
||||||
|
return buildOutputStream(mDataFile, binaryCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun buildInputStream(file: File?, binaryCache: BinaryCache): InputStream {
|
||||||
|
val cipherKey = binaryCache.loadedCipherKey
|
||||||
|
return when {
|
||||||
|
file != null && file.length() > 0 -> {
|
||||||
|
cipherDecryption.init(Cipher.DECRYPT_MODE, cipherKey.key, IvParameterSpec(cipherKey.iv))
|
||||||
|
Base64InputStream(CipherInputStream(FileInputStream(file), cipherDecryption), Base64.NO_WRAP)
|
||||||
|
}
|
||||||
|
else -> ByteArrayInputStream(ByteArray(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun buildOutputStream(file: File?, binaryCache: BinaryCache): OutputStream {
|
||||||
|
val cipherKey = binaryCache.loadedCipherKey
|
||||||
|
return when {
|
||||||
|
file != null -> {
|
||||||
|
cipherEncryption.init(Cipher.ENCRYPT_MODE, cipherKey.key, IvParameterSpec(cipherKey.iv))
|
||||||
|
BinaryCountingOutputStream(Base64OutputStream(CipherOutputStream(FileOutputStream(file), cipherEncryption), Base64.NO_WRAP))
|
||||||
|
}
|
||||||
|
else -> throw IOException("Unable to write in an unknown file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun compress(binaryCache: BinaryCache) {
|
||||||
|
mDataFile?.let { concreteDataFile ->
|
||||||
|
// To compress, create a new binary with file
|
||||||
|
if (!isCompressed) {
|
||||||
|
// Encrypt the new gzipped temp file
|
||||||
|
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
||||||
|
getInputDataStream(binaryCache).use { inputStream ->
|
||||||
|
GZIPOutputStream(buildOutputStream(fileBinaryCompress, binaryCache)).use { outputStream ->
|
||||||
|
inputStream.readAllBytes { buffer ->
|
||||||
|
outputStream.write(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove ungzip file
|
||||||
|
if (concreteDataFile.delete()) {
|
||||||
|
if (fileBinaryCompress.renameTo(concreteDataFile)) {
|
||||||
|
// Harmonize with database compression
|
||||||
|
isCompressed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun decompress(binaryCache: BinaryCache) {
|
||||||
|
mDataFile?.let { concreteDataFile ->
|
||||||
|
if (isCompressed) {
|
||||||
|
// Encrypt the new ungzipped temp file
|
||||||
|
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
||||||
|
getUnGzipInputDataStream(binaryCache).use { inputStream ->
|
||||||
|
buildOutputStream(fileBinaryDecompress, binaryCache).use { outputStream ->
|
||||||
|
inputStream.readAllBytes { buffer ->
|
||||||
|
outputStream.write(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove gzip file
|
||||||
|
if (concreteDataFile.delete()) {
|
||||||
|
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
|
||||||
|
// Harmonize with database compression
|
||||||
|
isCompressed = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clear(binaryCache: BinaryCache) {
|
||||||
|
if (mDataFile != null && !mDataFile!!.delete())
|
||||||
|
throw IOException("Unable to delete temp file " + mDataFile!!.absolutePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return mDataFile.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is BinaryFile) return false
|
||||||
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
|
return mDataFile != null && mDataFile == other.mDataFile
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = super.hashCode()
|
||||||
|
result = 31 * result + (mDataFile?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = BinaryFile::class.java.name
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val CREATOR: Parcelable.Creator<BinaryFile> = object : Parcelable.Creator<BinaryFile> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): BinaryFile {
|
||||||
|
return BinaryFile(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<BinaryFile?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.database.element.binary
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import java.io.IOException
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
abstract class BinaryPool<T>(private val mBinaryCache: BinaryCache) {
|
||||||
|
|
||||||
|
protected val pool = LinkedHashMap<T, BinaryData>()
|
||||||
|
|
||||||
|
// To build unique file id
|
||||||
|
private var creationId: Long = System.currentTimeMillis()
|
||||||
|
private var poolId: Int = abs(javaClass.simpleName.hashCode())
|
||||||
|
private var binaryFileIncrement = 0L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To get a binary by the pool key (ref attribute in entry)
|
||||||
|
*/
|
||||||
|
operator fun get(key: T): BinaryData? {
|
||||||
|
return pool[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and return a new binary file not yet linked to a binary
|
||||||
|
*/
|
||||||
|
fun put(key: T? = null,
|
||||||
|
builder: (uniqueBinaryId: String) -> BinaryData): KeyBinary<T> {
|
||||||
|
binaryFileIncrement++
|
||||||
|
val newBinaryFile: BinaryData = builder("$poolId$creationId$binaryFileIncrement")
|
||||||
|
val newKey = put(key, newBinaryFile)
|
||||||
|
return KeyBinary(newBinaryFile, newKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To linked a binary with a pool key, if the pool key doesn't exists, create an unused one
|
||||||
|
*/
|
||||||
|
fun put(key: T?, value: BinaryData): T {
|
||||||
|
if (key == null)
|
||||||
|
return put(value)
|
||||||
|
else
|
||||||
|
pool[key] = value
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To put a [binaryData] in the pool,
|
||||||
|
* if already exists, replace the current one,
|
||||||
|
* else add it with a new key
|
||||||
|
*/
|
||||||
|
fun put(binaryData: BinaryData): T {
|
||||||
|
var key: T? = findKey(binaryData)
|
||||||
|
if (key == null) {
|
||||||
|
key = findUnusedKey()
|
||||||
|
}
|
||||||
|
pool[key!!] = binaryData
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a binary from the pool with its [key], the file is not deleted
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun remove(key: T) {
|
||||||
|
pool.remove(key)
|
||||||
|
// Don't clear attachment here because a file can be used in many BinaryAttachment
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a binary from the pool, the file is not deleted
|
||||||
|
*/
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun remove(binaryData: BinaryData) {
|
||||||
|
findKey(binaryData)?.let {
|
||||||
|
pool.remove(it)
|
||||||
|
}
|
||||||
|
// Don't clear attachment here because a file can be used in many BinaryAttachment
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to find an unused key in the pool
|
||||||
|
*/
|
||||||
|
abstract fun findUnusedKey(): T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return key of [binaryDataToRetrieve] or null if not found
|
||||||
|
*/
|
||||||
|
private fun findKey(binaryDataToRetrieve: BinaryData): T? {
|
||||||
|
val contains = pool.containsValue(binaryDataToRetrieve)
|
||||||
|
return if (!contains)
|
||||||
|
null
|
||||||
|
else {
|
||||||
|
for ((key, binary) in pool) {
|
||||||
|
if (binary == binaryDataToRetrieve) {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isBinaryDuplicate(binaryData: BinaryData?): Boolean {
|
||||||
|
try {
|
||||||
|
binaryData?.let {
|
||||||
|
if (it.getSize() > 0) {
|
||||||
|
val searchBinaryMD5 = it.binaryHash()
|
||||||
|
var i = 0
|
||||||
|
for ((_, binary) in pool) {
|
||||||
|
if (binary.binaryHash() == searchBinaryMD5) {
|
||||||
|
i++
|
||||||
|
if (i > 1)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to check binary duplication", e)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To do an action on each binary in the pool (order is not important)
|
||||||
|
*/
|
||||||
|
private fun doForEachBinary(action: (key: T, binary: BinaryData) -> Unit,
|
||||||
|
condition: (key: T, binary: BinaryData) -> Boolean) {
|
||||||
|
for ((key, value) in pool) {
|
||||||
|
if (condition.invoke(key, value)) {
|
||||||
|
action.invoke(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doForEachBinary(action: (key: T, binary: BinaryData) -> Unit) {
|
||||||
|
doForEachBinary(action) { _, _ -> true }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to order binaries and solve index problem in database v4
|
||||||
|
*/
|
||||||
|
protected fun orderedBinariesWithoutDuplication(condition: ((binary: BinaryData) -> Boolean) = { true })
|
||||||
|
: List<KeyBinary<T>> {
|
||||||
|
val keyBinaryList = ArrayList<KeyBinary<T>>()
|
||||||
|
for ((key, binary) in pool) {
|
||||||
|
// Don't deduplicate
|
||||||
|
val existentBinary =
|
||||||
|
try {
|
||||||
|
if (binary.getSize() > 0) {
|
||||||
|
keyBinaryList.find {
|
||||||
|
val hash0 = it.binary.binaryHash()
|
||||||
|
val hash1 = binary.binaryHash()
|
||||||
|
hash0 != 0 && hash1 != 0 && hash0 == hash1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to check binary hash", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
if (existentBinary == null) {
|
||||||
|
val newKeyBinary = KeyBinary(binary, key)
|
||||||
|
if (condition.invoke(newKeyBinary.binary)) {
|
||||||
|
keyBinaryList.add(newKeyBinary)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (condition.invoke(existentBinary.binary)) {
|
||||||
|
existentBinary.addKey(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keyBinaryList
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Different from doForEach, provide an ordered index to each binary
|
||||||
|
*/
|
||||||
|
private fun doForEachBinaryWithoutDuplication(action: (keyBinary: KeyBinary<T>) -> Unit,
|
||||||
|
conditionToAdd: (binary: BinaryData) -> Boolean) {
|
||||||
|
orderedBinariesWithoutDuplication(conditionToAdd).forEach { keyBinary ->
|
||||||
|
action.invoke(keyBinary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doForEachBinaryWithoutDuplication(action: (keyBinary: KeyBinary<T>) -> Unit) {
|
||||||
|
doForEachBinaryWithoutDuplication(action, { true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Different from doForEach, provide an ordered index to each binary
|
||||||
|
*/
|
||||||
|
private fun doForEachOrderedBinaryWithoutDuplication(action: (index: Int, binary: BinaryData) -> Unit,
|
||||||
|
conditionToAdd: (binary: BinaryData) -> Boolean) {
|
||||||
|
orderedBinariesWithoutDuplication(conditionToAdd).forEachIndexed { index, keyBinary ->
|
||||||
|
action.invoke(index, keyBinary.binary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doForEachOrderedBinaryWithoutDuplication(action: (index: Int, binary: BinaryData) -> Unit) {
|
||||||
|
doForEachOrderedBinaryWithoutDuplication(action, { true })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isEmpty(): Boolean {
|
||||||
|
return pool.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun clear() {
|
||||||
|
doForEachBinary { _, binary ->
|
||||||
|
binary.clear(mBinaryCache)
|
||||||
|
}
|
||||||
|
pool.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
val stringBuffer = StringBuffer()
|
||||||
|
for ((key, value) in pool) {
|
||||||
|
if (stringBuffer.isNotEmpty())
|
||||||
|
stringBuffer.append(", {$key:$value}")
|
||||||
|
else
|
||||||
|
stringBuffer.append("{$key:$value}")
|
||||||
|
}
|
||||||
|
return stringBuffer.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class to order binaries
|
||||||
|
*/
|
||||||
|
class KeyBinary<T>(val binary: BinaryData, key: T) {
|
||||||
|
val keys = HashSet<T>()
|
||||||
|
init {
|
||||||
|
addKey(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addKey(key: T) {
|
||||||
|
keys.add(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = BinaryPool::class.java.name
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.kunzisoft.keepass.database.element.binary
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class CustomIconPool(binaryCache: BinaryCache) : BinaryPool<UUID>(binaryCache) {
|
||||||
|
|
||||||
|
override fun findUnusedKey(): UUID {
|
||||||
|
var newUUID = UUID.randomUUID()
|
||||||
|
while (pool.containsKey(newUUID)) {
|
||||||
|
newUUID = UUID.randomUUID()
|
||||||
|
}
|
||||||
|
return newUUID
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.kunzisoft.keepass.database.element.binary
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
import java.security.Key
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import javax.crypto.KeyGenerator
|
||||||
|
|
||||||
|
class LoadedKey(val key: Key, val iv: ByteArray): Serializable {
|
||||||
|
companion object {
|
||||||
|
const val BINARY_CIPHER = "Blowfish/CBC/PKCS5Padding"
|
||||||
|
|
||||||
|
fun generateNewCipherKey(): LoadedKey {
|
||||||
|
val iv = ByteArray(8)
|
||||||
|
SecureRandom().nextBytes(iv)
|
||||||
|
return LoadedKey(KeyGenerator.getInstance("Blowfish").generateKey(), iv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.database.element.database
|
|
||||||
|
|
||||||
import android.os.Parcel
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.kunzisoft.keepass.stream.readBytes
|
|
||||||
import java.io.*
|
|
||||||
import java.util.zip.GZIPInputStream
|
|
||||||
import java.util.zip.GZIPOutputStream
|
|
||||||
|
|
||||||
class BinaryAttachment : Parcelable {
|
|
||||||
|
|
||||||
private var dataFile: File? = null
|
|
||||||
var isCompressed: Boolean = false
|
|
||||||
private set
|
|
||||||
var isProtected: Boolean = false
|
|
||||||
private set
|
|
||||||
var isCorrupted: Boolean = false
|
|
||||||
|
|
||||||
fun length(): Long {
|
|
||||||
return dataFile?.length() ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Empty protected binary
|
|
||||||
*/
|
|
||||||
constructor()
|
|
||||||
|
|
||||||
constructor(dataFile: File, compressed: Boolean = false, protected: Boolean = false) {
|
|
||||||
this.dataFile = dataFile
|
|
||||||
this.isCompressed = compressed
|
|
||||||
this.isProtected = protected
|
|
||||||
}
|
|
||||||
|
|
||||||
private constructor(parcel: Parcel) {
|
|
||||||
parcel.readString()?.let {
|
|
||||||
dataFile = File(it)
|
|
||||||
}
|
|
||||||
isCompressed = parcel.readByte().toInt() != 0
|
|
||||||
isProtected = parcel.readByte().toInt() != 0
|
|
||||||
isCorrupted = parcel.readByte().toInt() != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun getInputDataStream(): InputStream {
|
|
||||||
return when {
|
|
||||||
length() > 0 -> FileInputStream(dataFile!!)
|
|
||||||
else -> ByteArrayInputStream(ByteArray(0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun getUnGzipInputDataStream(): InputStream {
|
|
||||||
return if (isCompressed)
|
|
||||||
GZIPInputStream(getInputDataStream())
|
|
||||||
else
|
|
||||||
getInputDataStream()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun getOutputDataStream(): OutputStream {
|
|
||||||
return when {
|
|
||||||
dataFile != null -> FileOutputStream(dataFile!!)
|
|
||||||
else -> throw IOException("Unable to write in an unknown file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun getGzipOutputDataStream(): OutputStream {
|
|
||||||
return if (isCompressed) {
|
|
||||||
GZIPOutputStream(getOutputDataStream())
|
|
||||||
} else {
|
|
||||||
getOutputDataStream()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
|
||||||
dataFile?.let { concreteDataFile ->
|
|
||||||
// To compress, create a new binary with file
|
|
||||||
if (!isCompressed) {
|
|
||||||
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
|
||||||
GZIPOutputStream(FileOutputStream(fileBinaryCompress)).use { outputStream ->
|
|
||||||
getInputDataStream().use { inputStream ->
|
|
||||||
inputStream.readBytes(bufferSize) { buffer ->
|
|
||||||
outputStream.write(buffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Remove unGzip file
|
|
||||||
if (concreteDataFile.delete()) {
|
|
||||||
if (fileBinaryCompress.renameTo(concreteDataFile)) {
|
|
||||||
// Harmonize with database compression
|
|
||||||
isCompressed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
|
|
||||||
dataFile?.let { concreteDataFile ->
|
|
||||||
if (isCompressed) {
|
|
||||||
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
|
|
||||||
FileOutputStream(fileBinaryDecompress).use { outputStream ->
|
|
||||||
getUnGzipInputDataStream().use { inputStream ->
|
|
||||||
inputStream.readBytes(bufferSize) { buffer ->
|
|
||||||
outputStream.write(buffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Remove gzip file
|
|
||||||
if (concreteDataFile.delete()) {
|
|
||||||
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
|
|
||||||
// Harmonize with database compression
|
|
||||||
isCompressed = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun clear() {
|
|
||||||
if (dataFile != null && !dataFile!!.delete())
|
|
||||||
throw IOException("Unable to delete temp file " + dataFile!!.absolutePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other)
|
|
||||||
return true
|
|
||||||
if (other == null || javaClass != other.javaClass)
|
|
||||||
return false
|
|
||||||
if (other !is BinaryAttachment)
|
|
||||||
return false
|
|
||||||
|
|
||||||
var sameData = false
|
|
||||||
if (dataFile != null && dataFile == other.dataFile)
|
|
||||||
sameData = true
|
|
||||||
|
|
||||||
return isCompressed == other.isCompressed
|
|
||||||
&& isProtected == other.isProtected
|
|
||||||
&& isCorrupted == other.isCorrupted
|
|
||||||
&& sameData
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
|
|
||||||
var result = 0
|
|
||||||
result = 31 * result + if (isCompressed) 1 else 0
|
|
||||||
result = 31 * result + if (isProtected) 1 else 0
|
|
||||||
result = 31 * result + if (isCorrupted) 1 else 0
|
|
||||||
result = 31 * result + dataFile!!.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return dataFile.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
|
||||||
dest.writeString(dataFile?.absolutePath)
|
|
||||||
dest.writeByte((if (isCompressed) 1 else 0).toByte())
|
|
||||||
dest.writeByte((if (isProtected) 1 else 0).toByte())
|
|
||||||
dest.writeByte((if (isCorrupted) 1 else 0).toByte())
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private val TAG = BinaryAttachment::class.java.name
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
val CREATOR: Parcelable.Creator<BinaryAttachment> = object : Parcelable.Creator<BinaryAttachment> {
|
|
||||||
override fun createFromParcel(parcel: Parcel): BinaryAttachment {
|
|
||||||
return BinaryAttachment(parcel)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newArray(size: Int): Array<BinaryAttachment?> {
|
|
||||||
return arrayOfNulls(size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.database.element.database
|
|
||||||
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
class BinaryPool {
|
|
||||||
private val pool = LinkedHashMap<Int, BinaryAttachment>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To get a binary by the pool key (ref attribute in entry)
|
|
||||||
*/
|
|
||||||
operator fun get(key: Int): BinaryAttachment? {
|
|
||||||
return pool[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To linked a binary with a pool key, if the pool key doesn't exists, create an unused one
|
|
||||||
*/
|
|
||||||
fun put(key: Int?, value: BinaryAttachment) {
|
|
||||||
if (key == null)
|
|
||||||
put(value)
|
|
||||||
else
|
|
||||||
pool[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To put a [binaryAttachment] in the pool,
|
|
||||||
* if already exists, replace the current one,
|
|
||||||
* else add it with a new key
|
|
||||||
*/
|
|
||||||
fun put(binaryAttachment: BinaryAttachment): Int {
|
|
||||||
var key = findKey(binaryAttachment)
|
|
||||||
if (key == null) {
|
|
||||||
key = findUnusedKey()
|
|
||||||
}
|
|
||||||
pool[key] = binaryAttachment
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a binary from the pool, the file is not deleted
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun remove(binaryAttachment: BinaryAttachment) {
|
|
||||||
findKey(binaryAttachment)?.let {
|
|
||||||
pool.remove(it)
|
|
||||||
}
|
|
||||||
// Don't clear attachment here because a file can be used in many BinaryAttachment
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility method to find an unused key in the pool
|
|
||||||
*/
|
|
||||||
private fun findUnusedKey(): Int {
|
|
||||||
var unusedKey = 0
|
|
||||||
while (pool[unusedKey] != null)
|
|
||||||
unusedKey++
|
|
||||||
return unusedKey
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return key of [binaryAttachmentToRetrieve] or null if not found
|
|
||||||
*/
|
|
||||||
private fun findKey(binaryAttachmentToRetrieve: BinaryAttachment): Int? {
|
|
||||||
val contains = pool.containsValue(binaryAttachmentToRetrieve)
|
|
||||||
return if (!contains)
|
|
||||||
null
|
|
||||||
else {
|
|
||||||
for ((key, binary) in pool) {
|
|
||||||
if (binary == binaryAttachmentToRetrieve) {
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility method to order binaries and solve index problem in database v4
|
|
||||||
*/
|
|
||||||
private fun orderedBinaries(): List<KeyBinary> {
|
|
||||||
val keyBinaryList = ArrayList<KeyBinary>()
|
|
||||||
for ((key, binary) in pool) {
|
|
||||||
keyBinaryList.add(KeyBinary(key, binary))
|
|
||||||
}
|
|
||||||
return keyBinaryList
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To register a binary with a ref corresponding to an ordered index
|
|
||||||
*/
|
|
||||||
fun getBinaryIndexFromKey(key: Int): Int? {
|
|
||||||
val index = orderedBinaries().indexOfFirst { it.key == key }
|
|
||||||
return if (index < 0)
|
|
||||||
null
|
|
||||||
else
|
|
||||||
index
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Different from doForEach, provide an ordered index to each binary
|
|
||||||
*/
|
|
||||||
fun doForEachOrderedBinary(action: (index: Int, keyBinary: KeyBinary) -> Unit) {
|
|
||||||
orderedBinaries().forEachIndexed(action)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To do an action on each binary in the pool
|
|
||||||
*/
|
|
||||||
fun doForEachBinary(action: (binary: BinaryAttachment) -> Unit) {
|
|
||||||
pool.values.forEach { action.invoke(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun clear() {
|
|
||||||
doForEachBinary {
|
|
||||||
it.clear()
|
|
||||||
}
|
|
||||||
pool.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
val stringBuffer = StringBuffer()
|
|
||||||
for ((key, value) in pool) {
|
|
||||||
if (stringBuffer.isNotEmpty())
|
|
||||||
stringBuffer.append(", {$key:$value}")
|
|
||||||
else
|
|
||||||
stringBuffer.append("{$key:$value}")
|
|
||||||
}
|
|
||||||
return stringBuffer.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility data class to order binaries
|
|
||||||
*/
|
|
||||||
data class KeyBinary(val key: Int, val binary: BinaryAttachment)
|
|
||||||
}
|
|
||||||
@@ -19,33 +19,27 @@
|
|||||||
|
|
||||||
package com.kunzisoft.keepass.database.element.database
|
package com.kunzisoft.keepass.database.element.database
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.finalkey.AESKeyTransformerFactory
|
import com.kunzisoft.encrypt.HashManager
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.encrypt.aes.AESTransformer
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||||
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
import com.kunzisoft.keepass.database.element.entry.EntryKDB
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
|
||||||
import com.kunzisoft.keepass.stream.NullOutputStream
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.security.DigestOutputStream
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||||
|
|
||||||
private var backupGroupId: Int = BACKUP_FOLDER_UNDEFINED_ID
|
|
||||||
|
|
||||||
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
|
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
|
||||||
|
|
||||||
private var binaryIncrement = 0
|
|
||||||
|
|
||||||
override val version: String
|
override val version: String
|
||||||
get() = "KeePass 1"
|
get() = "KeePass 1"
|
||||||
|
|
||||||
@@ -59,16 +53,17 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
return getGroupById(NodeIdInt(groupId))
|
return getGroupById(NodeIdInt(groupId))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve backup group in index
|
|
||||||
val backupGroup: GroupKDB?
|
val backupGroup: GroupKDB?
|
||||||
get() {
|
get() {
|
||||||
return if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID)
|
return retrieveBackup()
|
||||||
null
|
|
||||||
else
|
|
||||||
getGroupById(backupGroupId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override val kdfEngine: KdfEngine?
|
val groupNamesNotAllowed: List<String>
|
||||||
|
get() {
|
||||||
|
return listOf(BACKUP_FOLDER_TITLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val kdfEngine: KdfEngine
|
||||||
get() = kdfListV3[0]
|
get() = kdfListV3[0]
|
||||||
|
|
||||||
override val kdfAvailableList: List<KdfEngine>
|
override val kdfAvailableList: List<KdfEngine>
|
||||||
@@ -78,17 +73,13 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
get() {
|
get() {
|
||||||
val list = ArrayList<EncryptionAlgorithm>()
|
val list = ArrayList<EncryptionAlgorithm>()
|
||||||
list.add(EncryptionAlgorithm.AESRijndael)
|
list.add(EncryptionAlgorithm.AESRijndael)
|
||||||
|
list.add(EncryptionAlgorithm.Twofish)
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
val rootGroups: List<GroupKDB>
|
val rootGroups: List<GroupKDB>
|
||||||
get() {
|
get() {
|
||||||
val kids = ArrayList<GroupKDB>()
|
return rootGroup?.getChildGroups() ?: ArrayList()
|
||||||
doForEachGroupInIndex { group ->
|
|
||||||
if (group.level == 0)
|
|
||||||
kids.add(group)
|
|
||||||
}
|
|
||||||
return kids
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override val passwordEncoding: String
|
override val passwordEncoding: String
|
||||||
@@ -143,24 +134,11 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun makeFinalKey(masterSeed: ByteArray, masterSeed2: ByteArray, numRounds: Long) {
|
fun makeFinalKey(masterSeed: ByteArray, transformSeed: ByteArray, numRounds: Long) {
|
||||||
|
|
||||||
// Write checksum Checksum
|
|
||||||
val messageDigest: MessageDigest
|
|
||||||
try {
|
|
||||||
messageDigest = MessageDigest.getInstance("SHA-256")
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
|
||||||
throw IOException("SHA-256 not implemented here.")
|
|
||||||
}
|
|
||||||
|
|
||||||
val nos = NullOutputStream()
|
|
||||||
val dos = DigestOutputStream(nos, messageDigest)
|
|
||||||
|
|
||||||
// Encrypt the master key a few times to make brute-force key-search harder
|
// Encrypt the master key a few times to make brute-force key-search harder
|
||||||
dos.write(masterSeed)
|
val transformedKey = AESTransformer.transformKey(transformSeed, masterKey, numRounds) ?: ByteArray(0)
|
||||||
dos.write(AESKeyTransformerFactory.transformMasterKey(masterSeed2, masterKey, numRounds) ?: ByteArray(0))
|
// Write checksum Checksum
|
||||||
|
finalKey = HashManager.hashSha256(masterSeed, transformedKey)
|
||||||
finalKey = messageDigest.digest()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createGroup(): GroupKDB {
|
override fun createGroup(): GroupKDB {
|
||||||
@@ -175,27 +153,24 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getStandardIcon(iconId: Int): IconImageStandard {
|
||||||
|
return this.iconsManager.getIcon(iconId)
|
||||||
|
}
|
||||||
|
|
||||||
override fun containsCustomData(): Boolean {
|
override fun containsCustomData(): Boolean {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isInRecycleBin(group: GroupKDB): Boolean {
|
override fun isInRecycleBin(group: GroupKDB): Boolean {
|
||||||
var currentGroup: GroupKDB? = group
|
var currentGroup: GroupKDB? = group
|
||||||
|
val currentBackupGroup = backupGroup ?: return false
|
||||||
|
|
||||||
// Init backup group variable
|
if (currentGroup == currentBackupGroup)
|
||||||
if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID)
|
|
||||||
findBackupGroupId()
|
|
||||||
|
|
||||||
if (backupGroup == null)
|
|
||||||
return false
|
|
||||||
|
|
||||||
if (currentGroup == backupGroup)
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
val backupGroupId = currentBackupGroup.id
|
||||||
while (currentGroup != null) {
|
while (currentGroup != null) {
|
||||||
if (currentGroup.level == 0
|
if (backupGroupId == currentGroup.id) {
|
||||||
&& currentGroup.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)) {
|
|
||||||
backupGroupId = currentGroup.id
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
currentGroup = currentGroup.parent
|
currentGroup = currentGroup.parent
|
||||||
@@ -203,12 +178,12 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findBackupGroupId() {
|
/**
|
||||||
rootGroups.forEach { currentGroup ->
|
* Retrieve backup group with his name
|
||||||
if (currentGroup.level == 0
|
*/
|
||||||
&& currentGroup.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)) {
|
private fun retrieveBackup(): GroupKDB? {
|
||||||
backupGroupId = currentGroup.id
|
return rootGroup?.searchChildGroup {
|
||||||
}
|
it.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,16 +192,13 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
* if it doesn't exist
|
* if it doesn't exist
|
||||||
*/
|
*/
|
||||||
fun ensureBackupExists() {
|
fun ensureBackupExists() {
|
||||||
findBackupGroupId()
|
|
||||||
|
|
||||||
if (backupGroup == null) {
|
if (backupGroup == null) {
|
||||||
// Create recycle bin
|
// Create recycle bin
|
||||||
val recycleBinGroup = createGroup().apply {
|
val recycleBinGroup = createGroup().apply {
|
||||||
title = BACKUP_FOLDER_TITLE
|
title = BACKUP_FOLDER_TITLE
|
||||||
icon = iconFactory.trashIcon
|
icon.standard = getStandardIcon(IconImageStandard.TRASH_ID)
|
||||||
}
|
}
|
||||||
addGroupTo(recycleBinGroup, rootGroup)
|
addGroupTo(recycleBinGroup, rootGroup)
|
||||||
backupGroupId = recycleBinGroup.id
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,19 +241,16 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
|||||||
addEntryTo(entry, origParent)
|
addEntryTo(entry, origParent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildNewBinary(cacheDirectory: File): BinaryAttachment {
|
fun buildNewAttachment(): BinaryData {
|
||||||
// Generate an unique new file
|
// Generate an unique new file
|
||||||
val fileInCache = File(cacheDirectory, binaryIncrement.toString())
|
return attachmentPool.put { uniqueBinaryId ->
|
||||||
binaryIncrement++
|
binaryCache.getBinaryData(uniqueBinaryId, false)
|
||||||
return BinaryAttachment(fileInCache)
|
}.binary
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TYPE = DatabaseKDB::class.java
|
val TYPE = DatabaseKDB::class.java
|
||||||
|
|
||||||
const val BACKUP_FOLDER_TITLE = "Backup"
|
const val BACKUP_FOLDER_TITLE = "Backup"
|
||||||
private const val BACKUP_FOLDER_UNDEFINED_ID = -1
|
|
||||||
|
|
||||||
const val BUFFER_SIZE_BYTES = 3 * 128
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,49 +22,53 @@ package com.kunzisoft.keepass.database.element.database
|
|||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.kunzisoft.encrypt.HashManager
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.crypto.CryptoUtil
|
|
||||||
import com.kunzisoft.keepass.crypto.engine.AesEngine
|
|
||||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
|
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
|
|
||||||
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
import com.kunzisoft.keepass.database.action.node.NodeHandler
|
||||||
|
import com.kunzisoft.keepass.database.crypto.AesEngine
|
||||||
|
import com.kunzisoft.keepass.database.crypto.CipherEngine
|
||||||
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.database.crypto.VariantDictionary
|
||||||
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||||
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
|
||||||
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.DeletedObject
|
import com.kunzisoft.keepass.database.element.DeletedObject
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
import com.kunzisoft.keepass.database.element.group.GroupKDBX
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
import com.kunzisoft.keepass.database.element.node.NodeVersioned
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
|
||||||
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
|
||||||
import com.kunzisoft.keepass.database.exception.UnknownKDF
|
import com.kunzisoft.keepass.database.exception.UnknownKDF
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_3
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_3
|
||||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
|
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
|
||||||
import com.kunzisoft.keepass.utils.StringUtil.hexStringToByteArray
|
|
||||||
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
|
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
|
||||||
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
import com.kunzisoft.keepass.utils.VariantDictionary
|
import com.kunzisoft.keepass.utils.longTo8Bytes
|
||||||
|
import org.apache.commons.codec.binary.Hex
|
||||||
import org.w3c.dom.Node
|
import org.w3c.dom.Node
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import javax.crypto.Mac
|
||||||
import javax.xml.XMLConstants
|
import javax.xml.XMLConstants
|
||||||
import javax.xml.parsers.DocumentBuilderFactory
|
import javax.xml.parsers.DocumentBuilderFactory
|
||||||
import javax.xml.parsers.ParserConfigurationException
|
import javax.xml.parsers.ParserConfigurationException
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
|
||||||
class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||||
|
|
||||||
var hmacKey: ByteArray? = null
|
var hmacKey: ByteArray? = null
|
||||||
private set
|
private set
|
||||||
var dataCipher = AesEngine.CIPHER_UUID
|
var cipherUuid = EncryptionAlgorithm.AESRijndael.uuid
|
||||||
private var dataEngine: CipherEngine = AesEngine()
|
private var dataEngine: CipherEngine = AesEngine()
|
||||||
var compressionAlgorithm = CompressionAlgorithm.GZip
|
var compressionAlgorithm = CompressionAlgorithm.GZip
|
||||||
var kdfParameters: KdfParameters? = null
|
var kdfParameters: KdfParameters? = null
|
||||||
@@ -105,12 +109,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
var lastTopVisibleGroupUUID = UUID_ZERO
|
var lastTopVisibleGroupUUID = UUID_ZERO
|
||||||
var memoryProtection = MemoryProtectionConfig()
|
var memoryProtection = MemoryProtectionConfig()
|
||||||
val deletedObjects = ArrayList<DeletedObject>()
|
val deletedObjects = ArrayList<DeletedObject>()
|
||||||
val customIcons = ArrayList<IconImageCustom>()
|
|
||||||
val customData = HashMap<String, String>()
|
val customData = HashMap<String, String>()
|
||||||
|
|
||||||
var binaryPool = BinaryPool()
|
|
||||||
private var binaryIncrement = 0 // Unique id (don't use current time because CPU too fast)
|
|
||||||
|
|
||||||
var localizedAppName = "KeePassDX"
|
var localizedAppName = "KeePassDX"
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -126,12 +126,12 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
*/
|
*/
|
||||||
constructor(databaseName: String, rootName: String) {
|
constructor(databaseName: String, rootName: String) {
|
||||||
name = databaseName
|
name = databaseName
|
||||||
|
kdbxVersion = FILE_VERSION_32_3
|
||||||
val group = createGroup().apply {
|
val group = createGroup().apply {
|
||||||
title = rootName
|
title = rootName
|
||||||
icon = iconFactory.folderIcon
|
icon.standard = getStandardIcon(IconImageStandard.FOLDER_ID)
|
||||||
}
|
}
|
||||||
rootGroup = group
|
rootGroup = group
|
||||||
addGroupIndex(group)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override val version: String
|
override val version: String
|
||||||
@@ -186,7 +186,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
}
|
}
|
||||||
CompressionAlgorithm.GZip -> {
|
CompressionAlgorithm.GZip -> {
|
||||||
// Only in databaseV3.1, in databaseV4 the header is zipped during the save
|
// Only in databaseV3.1, in databaseV4 the header is zipped during the save
|
||||||
if (kdbxVersion.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong()) {
|
if (kdbxVersion.isBefore(FILE_VERSION_32_4)) {
|
||||||
compressAllBinaries()
|
compressAllBinaries()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,9 +194,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
}
|
}
|
||||||
CompressionAlgorithm.GZip -> {
|
CompressionAlgorithm.GZip -> {
|
||||||
// In databaseV4 the header is zipped during the save, so not necessary here
|
// In databaseV4 the header is zipped during the save, so not necessary here
|
||||||
if (kdbxVersion.toKotlinLong() >= FILE_VERSION_32_4.toKotlinLong()) {
|
if (kdbxVersion.isBefore(FILE_VERSION_32_4)) {
|
||||||
decompressAllBinaries()
|
|
||||||
} else {
|
|
||||||
when (newCompression) {
|
when (newCompression) {
|
||||||
CompressionAlgorithm.None -> {
|
CompressionAlgorithm.None -> {
|
||||||
decompressAllBinaries()
|
decompressAllBinaries()
|
||||||
@@ -204,16 +202,18 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
CompressionAlgorithm.GZip -> {
|
CompressionAlgorithm.GZip -> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
decompressAllBinaries()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun compressAllBinaries() {
|
private fun compressAllBinaries() {
|
||||||
binaryPool.doForEachBinary { binary ->
|
attachmentPool.doForEachBinary { _, binary ->
|
||||||
try {
|
try {
|
||||||
// To compress, create a new binary with file
|
// To compress, create a new binary with file
|
||||||
binary.compress(BUFFER_SIZE_BYTES)
|
binary.compress(binaryCache)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to compress $binary", e)
|
Log.e(TAG, "Unable to compress $binary", e)
|
||||||
}
|
}
|
||||||
@@ -221,9 +221,9 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun decompressAllBinaries() {
|
private fun decompressAllBinaries() {
|
||||||
binaryPool.doForEachBinary { binary ->
|
attachmentPool.doForEachBinary { _, binary ->
|
||||||
try {
|
try {
|
||||||
binary.decompress(BUFFER_SIZE_BYTES)
|
binary.decompress(binaryCache)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to decompress $binary", e)
|
Log.e(TAG, "Unable to decompress $binary", e)
|
||||||
}
|
}
|
||||||
@@ -302,16 +302,27 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
this.dataEngine = dataEngine
|
this.dataEngine = dataEngine
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCustomIcons(): List<IconImageCustom> {
|
override fun getStandardIcon(iconId: Int): IconImageStandard {
|
||||||
return customIcons
|
return this.iconsManager.getIcon(iconId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addCustomIcon(customIcon: IconImageCustom) {
|
fun buildNewCustomIcon(customIconId: UUID? = null,
|
||||||
this.customIcons.add(customIcon)
|
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||||
|
iconsManager.buildNewCustomIcon(customIconId, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCustomData(): Map<String, String> {
|
fun addCustomIcon(customIconId: UUID? = null,
|
||||||
return customData
|
smallSize: Boolean,
|
||||||
|
result: (IconImageCustom, BinaryData?) -> Unit) {
|
||||||
|
iconsManager.addCustomIcon(customIconId, smallSize, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isCustomIconBinaryDuplicate(binary: BinaryData): Boolean {
|
||||||
|
return iconsManager.isCustomIconBinaryDuplicate(binary)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCustomIcon(iconUuid: UUID): IconImageCustom {
|
||||||
|
return this.iconsManager.getIcon(iconUuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun putCustomData(label: String, value: String) {
|
fun putCustomData(label: String, value: String) {
|
||||||
@@ -319,7 +330,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun containsCustomData(): Boolean {
|
override fun containsCustomData(): Boolean {
|
||||||
return getCustomData().isNotEmpty()
|
return customData.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
@@ -335,14 +346,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
masterKey = getFileKey(keyInputStream)
|
masterKey = getFileKey(keyInputStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
val messageDigest: MessageDigest
|
return HashManager.hashSha256(masterKey)
|
||||||
try {
|
|
||||||
messageDigest = MessageDigest.getInstance("SHA-256")
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
|
||||||
throw IOException("No SHA-256 implementation")
|
|
||||||
}
|
|
||||||
|
|
||||||
return messageDigest.digest(masterKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
@@ -353,13 +357,13 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
|
|
||||||
var transformedMasterKey = kdfEngine.transform(masterKey, keyDerivationFunctionParameters)
|
var transformedMasterKey = kdfEngine.transform(masterKey, keyDerivationFunctionParameters)
|
||||||
if (transformedMasterKey.size != 32) {
|
if (transformedMasterKey.size != 32) {
|
||||||
transformedMasterKey = CryptoUtil.hashSha256(transformedMasterKey)
|
transformedMasterKey = HashManager.hashSha256(transformedMasterKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
val cmpKey = ByteArray(65)
|
val cmpKey = ByteArray(65)
|
||||||
System.arraycopy(masterSeed, 0, cmpKey, 0, 32)
|
System.arraycopy(masterSeed, 0, cmpKey, 0, 32)
|
||||||
System.arraycopy(transformedMasterKey, 0, cmpKey, 32, 32)
|
System.arraycopy(transformedMasterKey, 0, cmpKey, 32, 32)
|
||||||
finalKey = CryptoUtil.resizeKey(cmpKey, 0, 64, dataEngine.keyLength())
|
finalKey = resizeKey(cmpKey, dataEngine.keyLength())
|
||||||
|
|
||||||
val messageDigest: MessageDigest
|
val messageDigest: MessageDigest
|
||||||
try {
|
try {
|
||||||
@@ -374,6 +378,47 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resizeKey(inBytes: ByteArray, cbOut: Int): ByteArray {
|
||||||
|
if (cbOut == 0) return ByteArray(0)
|
||||||
|
|
||||||
|
val messageDigest = if (cbOut <= 32) HashManager.getHash256() else HashManager.getHash512()
|
||||||
|
messageDigest.update(inBytes, 0, 64)
|
||||||
|
val hash: ByteArray = messageDigest.digest()
|
||||||
|
|
||||||
|
if (cbOut == hash.size) {
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
|
val ret = ByteArray(cbOut)
|
||||||
|
if (cbOut < hash.size) {
|
||||||
|
System.arraycopy(hash, 0, ret, 0, cbOut)
|
||||||
|
} else {
|
||||||
|
var pos = 0
|
||||||
|
var r: Long = 0
|
||||||
|
while (pos < cbOut) {
|
||||||
|
val hmac: Mac
|
||||||
|
try {
|
||||||
|
hmac = Mac.getInstance("HmacSHA256")
|
||||||
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
val pbR = longTo8Bytes(r)
|
||||||
|
val part = hmac.doFinal(pbR)
|
||||||
|
|
||||||
|
val copy = min(cbOut - pos, part.size)
|
||||||
|
System.arraycopy(part, 0, ret, pos, copy)
|
||||||
|
pos += copy
|
||||||
|
r++
|
||||||
|
|
||||||
|
Arrays.fill(part, 0.toByte())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Arrays.fill(hash, 0.toByte())
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
override fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
|
override fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
|
||||||
try {
|
try {
|
||||||
val documentBuilderFactory = DocumentBuilderFactory.newInstance()
|
val documentBuilderFactory = DocumentBuilderFactory.newInstance()
|
||||||
@@ -445,16 +490,19 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
when (xmlKeyFileVersion) {
|
when (xmlKeyFileVersion) {
|
||||||
1F -> {
|
1F -> {
|
||||||
// No hash in KeyFile XML version 1
|
// No hash in KeyFile XML version 1
|
||||||
|
return Base64.decode(dataString, BASE_64_FLAG)
|
||||||
}
|
}
|
||||||
2F -> {
|
2F -> {
|
||||||
if (hashString != null
|
return if (hashString != null
|
||||||
&& checkKeyFileHash(dataString, hashString))
|
&& checkKeyFileHash(dataString, hashString)) {
|
||||||
Log.i(TAG, "Successful key file hash check.")
|
Log.i(TAG, "Successful key file hash check.")
|
||||||
else
|
Hex.decodeHex(dataString.toCharArray())
|
||||||
|
} else {
|
||||||
Log.e(TAG, "Unable to check the hash of the key file.")
|
Log.e(TAG, "Unable to check the hash of the key file.")
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Base64.decode(dataString, BASE_64_FLAG)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -468,17 +516,13 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun checkKeyFileHash(data: String, hash: String): Boolean {
|
private fun checkKeyFileHash(data: String, hash: String): Boolean {
|
||||||
val digest: MessageDigest?
|
|
||||||
var success = false
|
var success = false
|
||||||
try {
|
try {
|
||||||
digest = MessageDigest.getInstance("SHA-256")
|
|
||||||
digest?.reset()
|
|
||||||
// hexadecimal encoding of the first 4 bytes of the SHA-256 hash of the key.
|
// hexadecimal encoding of the first 4 bytes of the SHA-256 hash of the key.
|
||||||
val dataDigest = digest.digest(data.hexStringToByteArray())
|
val dataDigest = HashManager.hashSha256(Hex.decodeHex(data.toCharArray()))
|
||||||
.copyOfRange(0, 4)
|
.copyOfRange(0, 4).toHexString()
|
||||||
.toHexString()
|
|
||||||
success = dataDigest == hash
|
success = dataDigest == hash
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
return success
|
return success
|
||||||
@@ -542,7 +586,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
// Create recycle bin
|
// Create recycle bin
|
||||||
val recycleBinGroup = createGroup().apply {
|
val recycleBinGroup = createGroup().apply {
|
||||||
title = resources.getString(R.string.recycle_bin)
|
title = resources.getString(R.string.recycle_bin)
|
||||||
icon = iconFactory.trashIcon
|
icon.standard = getStandardIcon(IconImageStandard.TRASH_ID)
|
||||||
enableAutoType = false
|
enableAutoType = false
|
||||||
enableSearching = false
|
enableSearching = false
|
||||||
isExpanded = false
|
isExpanded = false
|
||||||
@@ -570,6 +614,9 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
return false
|
return false
|
||||||
if (recycleBin == null)
|
if (recycleBin == null)
|
||||||
return false
|
return false
|
||||||
|
if (node is GroupKDBX
|
||||||
|
&& recycleBin!!.isContainedIn(node))
|
||||||
|
return false
|
||||||
if (!node.isContainedIn(recycleBin!!))
|
if (!node.isContainedIn(recycleBin!!))
|
||||||
return true
|
return true
|
||||||
return false
|
return false
|
||||||
@@ -621,21 +668,17 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
return publicCustomData.size() > 0
|
return publicCustomData.size() > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildNewBinary(cacheDirectory: File,
|
fun buildNewAttachment(smallSize: Boolean,
|
||||||
compression: Boolean,
|
compression: Boolean,
|
||||||
protection: Boolean,
|
protection: Boolean,
|
||||||
binaryPoolId: Int? = null): BinaryAttachment {
|
binaryPoolId: Int? = null): BinaryData {
|
||||||
// New file with current time
|
return attachmentPool.put(binaryPoolId) { uniqueBinaryId ->
|
||||||
val fileInCache = File(cacheDirectory, binaryIncrement.toString())
|
binaryCache.getBinaryData(uniqueBinaryId, smallSize, compression, protection)
|
||||||
binaryIncrement++
|
}.binary
|
||||||
val binaryAttachment = BinaryAttachment(fileInCache, compression, protection)
|
|
||||||
// add attachment to pool
|
|
||||||
binaryPool.put(binaryPoolId, binaryAttachment)
|
|
||||||
return binaryAttachment
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeUnlinkedAttachment(binary: BinaryAttachment, clear: Boolean) {
|
fun removeUnlinkedAttachment(binary: BinaryData, clear: Boolean) {
|
||||||
val listBinaries = ArrayList<BinaryAttachment>()
|
val listBinaries = ArrayList<BinaryData>()
|
||||||
listBinaries.add(binary)
|
listBinaries.add(binary)
|
||||||
removeUnlinkedAttachments(listBinaries, clear)
|
removeUnlinkedAttachments(listBinaries, clear)
|
||||||
}
|
}
|
||||||
@@ -644,11 +687,11 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
removeUnlinkedAttachments(emptyList(), clear)
|
removeUnlinkedAttachments(emptyList(), clear)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeUnlinkedAttachments(binaries: List<BinaryAttachment>, clear: Boolean) {
|
private fun removeUnlinkedAttachments(binaries: List<BinaryData>, clear: Boolean) {
|
||||||
// Build binaries to remove with all binaries known
|
// Build binaries to remove with all binaries known
|
||||||
val binariesToRemove = ArrayList<BinaryAttachment>()
|
val binariesToRemove = ArrayList<BinaryData>()
|
||||||
if (binaries.isEmpty()) {
|
if (binaries.isEmpty()) {
|
||||||
binaryPool.doForEachBinary { binary ->
|
attachmentPool.doForEachBinary { _, binary ->
|
||||||
binariesToRemove.add(binary)
|
binariesToRemove.add(binary)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -657,8 +700,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
// Remove binaries from the list
|
// Remove binaries from the list
|
||||||
rootGroup?.doForEachChild(object : NodeHandler<EntryKDBX>() {
|
rootGroup?.doForEachChild(object : NodeHandler<EntryKDBX>() {
|
||||||
override fun operate(node: EntryKDBX): Boolean {
|
override fun operate(node: EntryKDBX): Boolean {
|
||||||
node.getAttachments(binaryPool, true).forEach {
|
node.getAttachments(attachmentPool, true).forEach {
|
||||||
binariesToRemove.remove(it.binaryAttachment)
|
binariesToRemove.remove(it.binaryData)
|
||||||
}
|
}
|
||||||
return binariesToRemove.isNotEmpty()
|
return binariesToRemove.isNotEmpty()
|
||||||
}
|
}
|
||||||
@@ -666,9 +709,9 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
// Effective removing
|
// Effective removing
|
||||||
binariesToRemove.forEach {
|
binariesToRemove.forEach {
|
||||||
try {
|
try {
|
||||||
binaryPool.remove(it)
|
attachmentPool.remove(it)
|
||||||
if (clear)
|
if (clear)
|
||||||
it.clear()
|
it.clear(binaryCache)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Unable to clean binaries", e)
|
Log.w(TAG, "Unable to clean binaries", e)
|
||||||
}
|
}
|
||||||
@@ -684,7 +727,7 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
override fun clearCache() {
|
override fun clearCache() {
|
||||||
try {
|
try {
|
||||||
super.clearCache()
|
super.clearCache()
|
||||||
binaryPool.clear()
|
attachmentPool.clear()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to clear cache", e)
|
Log.e(TAG, "Unable to clear cache", e)
|
||||||
}
|
}
|
||||||
@@ -698,14 +741,12 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
|||||||
private const val DEFAULT_HISTORY_MAX_SIZE = (6 * 1024 * 1024).toLong() // -1 unlimited
|
private const val DEFAULT_HISTORY_MAX_SIZE = (6 * 1024 * 1024).toLong() // -1 unlimited
|
||||||
|
|
||||||
private const val XML_NODE_ROOT_NAME = "KeyFile"
|
private const val XML_NODE_ROOT_NAME = "KeyFile"
|
||||||
private const val XML_NODE_META_NAME = "Meta";
|
private const val XML_NODE_META_NAME = "Meta"
|
||||||
private const val XML_NODE_VERSION_NAME = "Version";
|
private const val XML_NODE_VERSION_NAME = "Version"
|
||||||
private const val XML_NODE_KEY_NAME = "Key"
|
private const val XML_NODE_KEY_NAME = "Key"
|
||||||
private const val XML_NODE_DATA_NAME = "Data"
|
private const val XML_NODE_DATA_NAME = "Data"
|
||||||
private const val XML_ATTRIBUTE_DATA_HASH = "Hash"
|
private const val XML_ATTRIBUTE_DATA_HASH = "Hash"
|
||||||
|
|
||||||
const val BASE_64_FLAG = Base64.NO_WRAP
|
const val BASE_64_FLAG = Base64.NO_WRAP
|
||||||
|
|
||||||
const val BUFFER_SIZE_BYTES = 3 * 128
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,20 +19,22 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.database.element.database
|
package com.kunzisoft.keepass.database.element.database
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
|
import com.kunzisoft.encrypt.HashManager
|
||||||
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryCache
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
import com.kunzisoft.keepass.database.element.entry.EntryVersioned
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupVersioned
|
import com.kunzisoft.keepass.database.element.group.GroupVersioned
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconsManager
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
|
import org.apache.commons.codec.binary.Hex
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.UnsupportedEncodingException
|
import java.io.UnsupportedEncodingException
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
abstract class DatabaseVersioned<
|
abstract class DatabaseVersioned<
|
||||||
@@ -45,16 +47,22 @@ abstract class DatabaseVersioned<
|
|||||||
// Algorithm used to encrypt the database
|
// Algorithm used to encrypt the database
|
||||||
protected var algorithm: EncryptionAlgorithm? = null
|
protected var algorithm: EncryptionAlgorithm? = null
|
||||||
|
|
||||||
abstract val kdfEngine: KdfEngine?
|
abstract val kdfEngine: com.kunzisoft.keepass.database.crypto.kdf.KdfEngine?
|
||||||
|
|
||||||
abstract val kdfAvailableList: List<KdfEngine>
|
abstract val kdfAvailableList: List<com.kunzisoft.keepass.database.crypto.kdf.KdfEngine>
|
||||||
|
|
||||||
var masterKey = ByteArray(32)
|
var masterKey = ByteArray(32)
|
||||||
var finalKey: ByteArray? = null
|
var finalKey: ByteArray? = null
|
||||||
protected set
|
protected set
|
||||||
|
|
||||||
var iconFactory = IconImageFactory()
|
/**
|
||||||
protected set
|
* To manage binaries in faster way
|
||||||
|
* Cipher key generated when the database is loaded, and destroyed when the database is closed
|
||||||
|
* Can be used to temporarily store database elements
|
||||||
|
*/
|
||||||
|
var binaryCache = BinaryCache()
|
||||||
|
val iconsManager = IconsManager(binaryCache)
|
||||||
|
var attachmentPool = AttachmentPool(binaryCache)
|
||||||
|
|
||||||
var changeDuplicateId = false
|
var changeDuplicateId = false
|
||||||
|
|
||||||
@@ -78,74 +86,59 @@ abstract class DatabaseVersioned<
|
|||||||
abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
|
abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
|
||||||
|
|
||||||
var rootGroup: Group? = null
|
var rootGroup: Group? = null
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
value?.let {
|
||||||
|
addGroupIndex(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
|
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun retrieveMasterKey(key: String?, keyInputStream: InputStream?) {
|
fun retrieveMasterKey(key: String?, keyfileInputStream: InputStream?) {
|
||||||
masterKey = getMasterKey(key, keyInputStream)
|
masterKey = getMasterKey(key, keyfileInputStream)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
protected fun getCompositeKey(key: String, keyInputStream: InputStream): ByteArray {
|
protected fun getCompositeKey(key: String, keyfileInputStream: InputStream): ByteArray {
|
||||||
val fileKey = getFileKey(keyInputStream)
|
val fileKey = getFileKey(keyfileInputStream)
|
||||||
val passwordKey = getPasswordKey(key)
|
val passwordKey = getPasswordKey(key)
|
||||||
|
return HashManager.hashSha256(passwordKey, fileKey)
|
||||||
val messageDigest: MessageDigest
|
|
||||||
try {
|
|
||||||
messageDigest = MessageDigest.getInstance("SHA-256")
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
|
||||||
throw IOException("SHA-256 not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
messageDigest.update(passwordKey)
|
|
||||||
|
|
||||||
return messageDigest.digest(fileKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
protected fun getPasswordKey(key: String): ByteArray {
|
protected fun getPasswordKey(key: String): ByteArray {
|
||||||
val messageDigest: MessageDigest
|
|
||||||
try {
|
|
||||||
messageDigest = MessageDigest.getInstance("SHA-256")
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
|
||||||
throw IOException("SHA-256 not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
val bKey: ByteArray = try {
|
val bKey: ByteArray = try {
|
||||||
key.toByteArray(charset(passwordEncoding))
|
key.toByteArray(charset(passwordEncoding))
|
||||||
} catch (e: UnsupportedEncodingException) {
|
} catch (e: UnsupportedEncodingException) {
|
||||||
key.toByteArray()
|
key.toByteArray()
|
||||||
}
|
}
|
||||||
|
return HashManager.hashSha256(bKey)
|
||||||
messageDigest.update(bKey, 0, bKey.size)
|
|
||||||
|
|
||||||
return messageDigest.digest()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
protected fun getFileKey(keyInputStream: InputStream): ByteArray {
|
protected fun getFileKey(keyInputStream: InputStream): ByteArray {
|
||||||
|
|
||||||
val keyData = keyInputStream.readBytes()
|
val keyData = keyInputStream.readBytes()
|
||||||
|
|
||||||
// Check 32 bits key file
|
|
||||||
if (keyData.size == 32) {
|
|
||||||
return keyData
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check XML key file
|
// Check XML key file
|
||||||
val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData))
|
val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData))
|
||||||
if (xmlKeyByteArray != null) {
|
if (xmlKeyByteArray != null) {
|
||||||
return xmlKeyByteArray
|
return xmlKeyByteArray
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash file as binary data
|
// Check 32 bytes key file
|
||||||
try {
|
when (keyData.size) {
|
||||||
return MessageDigest.getInstance("SHA-256").digest(keyData)
|
32 -> return keyData
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
64 -> try {
|
||||||
throw IOException("SHA-256 not supported")
|
return Hex.decodeHex(String(keyData).toCharArray())
|
||||||
|
} catch (ignoredException: Exception) {
|
||||||
|
// Key is not base 64, treat it as binary data
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Hash file as binary data
|
||||||
|
return HashManager.hashSha256(keyData)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
|
protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
|
||||||
@@ -322,6 +315,8 @@ abstract class DatabaseVersioned<
|
|||||||
|
|
||||||
abstract fun rootCanContainsEntry(): Boolean
|
abstract fun rootCanContainsEntry(): Boolean
|
||||||
|
|
||||||
|
abstract fun getStandardIcon(iconId: Int): IconImageStandard
|
||||||
|
|
||||||
abstract fun containsCustomData(): Boolean
|
abstract fun containsCustomData(): Boolean
|
||||||
|
|
||||||
fun addGroupTo(newGroup: Group, parent: Group?) {
|
fun addGroupTo(newGroup: Group, parent: Group?) {
|
||||||
|
|||||||
@@ -21,15 +21,16 @@ package com.kunzisoft.keepass.database.element.entry
|
|||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.AttachmentPool
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
import com.kunzisoft.keepass.database.element.group.GroupKDB
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard.Companion.KEY_ID
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeKDBInterface
|
import com.kunzisoft.keepass.database.element.node.NodeKDBInterface
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Structure containing information about one entry.
|
* Structure containing information about one entry.
|
||||||
@@ -56,7 +57,7 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
|
|
||||||
/** A string describing what is in binaryData */
|
/** A string describing what is in binaryData */
|
||||||
var binaryDescription = ""
|
var binaryDescription = ""
|
||||||
var binaryData: BinaryAttachment? = null
|
private var binaryDataId: Int? = null
|
||||||
|
|
||||||
// Determine if this is a MetaStream entry
|
// Determine if this is a MetaStream entry
|
||||||
val isMetaStream: Boolean
|
val isMetaStream: Boolean
|
||||||
@@ -68,7 +69,8 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
if (username.isEmpty()) return false
|
if (username.isEmpty()) return false
|
||||||
if (username != PMS_ID_USER) return false
|
if (username != PMS_ID_USER) return false
|
||||||
if (url.isEmpty()) return false
|
if (url.isEmpty()) return false
|
||||||
return if (url != PMS_ID_URL) false else icon.isMetaStreamIcon
|
if (url != PMS_ID_URL) return false
|
||||||
|
return icon.standard.id == KEY_ID
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun initNodeId(): NodeId<UUID> {
|
override fun initNodeId(): NodeId<UUID> {
|
||||||
@@ -88,7 +90,8 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
url = parcel.readString() ?: url
|
url = parcel.readString() ?: url
|
||||||
notes = parcel.readString() ?: notes
|
notes = parcel.readString() ?: notes
|
||||||
binaryDescription = parcel.readString() ?: binaryDescription
|
binaryDescription = parcel.readString() ?: binaryDescription
|
||||||
binaryData = parcel.readParcelable(BinaryAttachment::class.java.classLoader)
|
val rawBinaryDataId = parcel.readInt()
|
||||||
|
binaryDataId = if (rawBinaryDataId == -1) null else rawBinaryDataId
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun readParentParcelable(parcel: Parcel): GroupKDB? {
|
override fun readParentParcelable(parcel: Parcel): GroupKDB? {
|
||||||
@@ -107,7 +110,7 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
dest.writeString(url)
|
dest.writeString(url)
|
||||||
dest.writeString(notes)
|
dest.writeString(notes)
|
||||||
dest.writeString(binaryDescription)
|
dest.writeString(binaryDescription)
|
||||||
dest.writeParcelable(binaryData, flags)
|
dest.writeInt(binaryDataId ?: -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateWith(source: EntryKDB) {
|
fun updateWith(source: EntryKDB) {
|
||||||
@@ -118,7 +121,7 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
url = source.url
|
url = source.url
|
||||||
notes = source.notes
|
notes = source.notes
|
||||||
binaryDescription = source.binaryDescription
|
binaryDescription = source.binaryDescription
|
||||||
binaryData = source.binaryData
|
binaryDataId = source.binaryDataId
|
||||||
}
|
}
|
||||||
|
|
||||||
override var username = ""
|
override var username = ""
|
||||||
@@ -137,26 +140,39 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
|||||||
override val type: Type
|
override val type: Type
|
||||||
get() = Type.ENTRY
|
get() = Type.ENTRY
|
||||||
|
|
||||||
fun getAttachment(): Attachment? {
|
fun getAttachment(attachmentPool: AttachmentPool): Attachment? {
|
||||||
val binary = binaryData
|
binaryDataId?.let { poolId ->
|
||||||
return if (binary != null)
|
attachmentPool[poolId]?.let { binary ->
|
||||||
Attachment(binaryDescription, binary)
|
return Attachment(binaryDescription, binary)
|
||||||
else null
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun containsAttachment(): Boolean {
|
fun containsAttachment(): Boolean {
|
||||||
return binaryData != null
|
return binaryDataId != null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun putAttachment(attachment: Attachment) {
|
fun getBinary(attachmentPool: AttachmentPool): BinaryData? {
|
||||||
|
this.binaryDataId?.let {
|
||||||
|
return attachmentPool[it]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putBinary(binaryData: BinaryData, attachmentPool: AttachmentPool) {
|
||||||
|
this.binaryDataId = attachmentPool.put(binaryData)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putAttachment(attachment: Attachment, attachmentPool: AttachmentPool) {
|
||||||
this.binaryDescription = attachment.name
|
this.binaryDescription = attachment.name
|
||||||
this.binaryData = attachment.binaryAttachment
|
this.binaryDataId = attachmentPool.put(attachment.binaryData)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAttachment(attachment: Attachment? = null) {
|
fun removeAttachment(attachment: Attachment? = null) {
|
||||||
if (attachment == null || this.binaryDescription == attachment.name) {
|
if (attachment == null || this.binaryDescription == attachment.name) {
|
||||||
this.binaryDescription = ""
|
this.binaryDescription = ""
|
||||||
this.binaryData = null
|
this.binaryDataId = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user