mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
597 Commits
2.5.0.0bet
...
2.5.0.0bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9700dbcc3f | ||
|
|
4e344458b2 | ||
|
|
6f95cc7296 | ||
|
|
cd66f8f57e | ||
|
|
83f0eb9a33 | ||
|
|
ca89bba768 | ||
|
|
2d2bd5013e | ||
|
|
b93d7bbf41 | ||
|
|
000277705a | ||
|
|
088712e784 | ||
|
|
117592387e | ||
|
|
76bb1a369c | ||
|
|
9331c281fe | ||
|
|
f2666316e1 | ||
|
|
7c2ff5067d | ||
|
|
5fed641c7c | ||
|
|
18bd62ee5a | ||
|
|
bc57e6e257 | ||
|
|
69b1aba218 | ||
|
|
df04d998c2 | ||
|
|
e986fe5f60 | ||
|
|
e4ac0ee258 | ||
|
|
426aa0e7da | ||
|
|
6c0a48af48 | ||
|
|
d865da1613 | ||
|
|
6fa4c1e06e | ||
|
|
52a6b3e046 | ||
|
|
fa26d2f938 | ||
|
|
4777cdc7ae | ||
|
|
cd8d3cbf6a | ||
|
|
e6a6feb5c0 | ||
|
|
98d6fb9214 | ||
|
|
94244cd15b | ||
|
|
c5e2ca9907 | ||
|
|
e4fef44caf | ||
|
|
5bd9da9bb1 | ||
|
|
5f0e899679 | ||
|
|
4b1806900b | ||
|
|
30e2912885 | ||
|
|
fc3608ff69 | ||
|
|
6de47ec9b2 | ||
|
|
f7253764a2 | ||
|
|
c54b134c31 | ||
|
|
a6bdca52be | ||
|
|
29eec05f8f | ||
|
|
250aef9738 | ||
|
|
50097914a2 | ||
|
|
ecff4fb2c5 | ||
|
|
ab3d17f352 | ||
|
|
a75d237e53 | ||
|
|
a0c7786e1b | ||
|
|
0d32c38c79 | ||
|
|
b846eda410 | ||
|
|
7c33c9ec02 | ||
|
|
74c08340a6 | ||
|
|
82161536be | ||
|
|
752dbca356 | ||
|
|
9ef56f6fd8 | ||
|
|
0239f115ae | ||
|
|
9775e09221 | ||
|
|
c975a1bfc0 | ||
|
|
c263536078 | ||
|
|
b559eeaad0 | ||
|
|
63373083ab | ||
|
|
bf525807b0 | ||
|
|
921021078b | ||
|
|
e5b60a8413 | ||
|
|
e01402f2fa | ||
|
|
27f92e1bb5 | ||
|
|
63db6de30b | ||
|
|
7a038126cf | ||
|
|
edcfa8cf7b | ||
|
|
c9594948a2 | ||
|
|
66988ecb66 | ||
|
|
186ca30be8 | ||
|
|
027d581dcc | ||
|
|
adcc1c745a | ||
|
|
8682856c01 | ||
|
|
9ec976d246 | ||
|
|
9d17b49586 | ||
|
|
698496d37c | ||
|
|
2af8c4f3c8 | ||
|
|
b164099b6d | ||
|
|
c19357605f | ||
|
|
5a08fa0088 | ||
|
|
bff87d16b1 | ||
|
|
5b0afa447c | ||
|
|
5a882a954f | ||
|
|
47f340d576 | ||
|
|
41df139c17 | ||
|
|
469c267161 | ||
|
|
f336d4fe58 | ||
|
|
e1d997cc91 | ||
|
|
53cf4bba1b | ||
|
|
030417dbe1 | ||
|
|
59abcb115c | ||
|
|
598dbd3794 | ||
|
|
2c4a7e5576 | ||
|
|
02429d5790 | ||
|
|
d990cb24ea | ||
|
|
e549b16dce | ||
|
|
e11864a64f | ||
|
|
a3e74f8ee5 | ||
|
|
36cb683404 | ||
|
|
f7f0028033 | ||
|
|
f06088fa12 | ||
|
|
b62ef8a2ed | ||
|
|
622ba65841 | ||
|
|
3a48d20d12 | ||
|
|
8d6db78f55 | ||
|
|
f3ba6e800a | ||
|
|
1c4aaf9807 | ||
|
|
c65ed41efd | ||
|
|
1965336077 | ||
|
|
9b7095ad4c | ||
|
|
33767c2bf9 | ||
|
|
82f7e861e7 | ||
|
|
846b5fa449 | ||
|
|
baf4e676eb | ||
|
|
a9bf3e83c4 | ||
|
|
710a1b0996 | ||
|
|
c4671b84a0 | ||
|
|
c0c98d0299 | ||
|
|
ffdec77d11 | ||
|
|
868bbe2e70 | ||
|
|
4aac655a5d | ||
|
|
510244aa70 | ||
|
|
50840b04b4 | ||
|
|
4d5962f5ca | ||
|
|
c9456c771c | ||
|
|
41a7a583d4 | ||
|
|
2e5220fa8a | ||
|
|
b75475d785 | ||
|
|
362ef06bb5 | ||
|
|
4a80c9f9f9 | ||
|
|
f1fdb9fc84 | ||
|
|
5bb9168c29 | ||
|
|
0245dcd8e8 | ||
|
|
b31bfa1d4f | ||
|
|
0778f22b68 | ||
|
|
4808696398 | ||
|
|
0ea7b5b25f | ||
|
|
995785de9f | ||
|
|
5deef427c0 | ||
|
|
024c6631d8 | ||
|
|
78354e4736 | ||
|
|
b9a792e6bd | ||
|
|
c533d21250 | ||
|
|
74572c8102 | ||
|
|
b8de64fab0 | ||
|
|
877b909205 | ||
|
|
1b1dcc0f45 | ||
|
|
94e5988794 | ||
|
|
b22333fda5 | ||
|
|
400c6bef78 | ||
|
|
edf6c2ff07 | ||
|
|
5eec1a276c | ||
|
|
f707fd7649 | ||
|
|
75b028daf3 | ||
|
|
c6f259d18f | ||
|
|
954d522341 | ||
|
|
1f8d17d27e | ||
|
|
c2781af38d | ||
|
|
a1cd2683d4 | ||
|
|
6d671f53c2 | ||
|
|
4efb81f597 | ||
|
|
faddeb6e2d | ||
|
|
e36629968d | ||
|
|
0b756797f6 | ||
|
|
110c7bbbc7 | ||
|
|
639c6dc4ac | ||
|
|
f605d36adf | ||
|
|
f6d6c134f4 | ||
|
|
a0c07654df | ||
|
|
03ab688abe | ||
|
|
4a28802b02 | ||
|
|
18f8fe7cc3 | ||
|
|
459606f5d5 | ||
|
|
3d53c31680 | ||
|
|
173dd5b59b | ||
|
|
8536de3555 | ||
|
|
722ba5c34d | ||
|
|
3ccfd7226b | ||
|
|
578da7bae5 | ||
|
|
6916389657 | ||
|
|
da4f41732a | ||
|
|
0414adf5de | ||
|
|
d9b2942f66 | ||
|
|
cbe7907b07 | ||
|
|
b737501d4d | ||
|
|
c303ffafb5 | ||
|
|
6f513b4920 | ||
|
|
a83c60583f | ||
|
|
cde8950257 | ||
|
|
0b4dd1e909 | ||
|
|
28e2600271 | ||
|
|
53cc4f74c8 | ||
|
|
3989ca3ad1 | ||
|
|
977782a9f7 | ||
|
|
f100dda20b | ||
|
|
9c0a140a17 | ||
|
|
b4bcaf54ad | ||
|
|
82c0ca0f3c | ||
|
|
bfbd81e3ee | ||
|
|
67fecf3fef | ||
|
|
c65d96802e | ||
|
|
64af8ddc2e | ||
|
|
732f472146 | ||
|
|
6dc46604a4 | ||
|
|
f0c3071de1 | ||
|
|
b250ad2f8b | ||
|
|
69390a81ab | ||
|
|
c398b92eb1 | ||
|
|
3018206e2f | ||
|
|
811d0f2534 | ||
|
|
2b33d785ac | ||
|
|
c7a8322c5d | ||
|
|
37e722847a | ||
|
|
4aa8d892a4 | ||
|
|
e164062cf3 | ||
|
|
935d3e1c4b | ||
|
|
17eadcee2b | ||
|
|
5ffbe0e9ee | ||
|
|
4c3da45141 | ||
|
|
c516ef7c28 | ||
|
|
dcf654cf0a | ||
|
|
23f2e5decc | ||
|
|
ddbf03bc91 | ||
|
|
94070ea5e0 | ||
|
|
725d39626f | ||
|
|
25655abbbb | ||
|
|
60bee79bc5 | ||
|
|
8f9d278d2f | ||
|
|
dc120135b1 | ||
|
|
d2f56e7472 | ||
|
|
3877e1aa22 | ||
|
|
e08e5eca3a | ||
|
|
7321cc067f | ||
|
|
43f14e1474 | ||
|
|
ced6a77819 | ||
|
|
bca777a97e | ||
|
|
5b98129da9 | ||
|
|
dc37ab74c1 | ||
|
|
ee76a35728 | ||
|
|
e31ea8c916 | ||
|
|
41561cb7b6 | ||
|
|
0c8fdca6f3 | ||
|
|
a50626dafb | ||
|
|
ca55081437 | ||
|
|
5cd66d074e | ||
|
|
9529e3a7ba | ||
|
|
d3e2668d85 | ||
|
|
b3b2bb90e1 | ||
|
|
9c597665bf | ||
|
|
a845436af4 | ||
|
|
cabd487fa5 | ||
|
|
9c7c43e7da | ||
|
|
0c421c4906 | ||
|
|
4df8880b0d | ||
|
|
45046ee01a | ||
|
|
2ff6522b60 | ||
|
|
ee8c589ea3 | ||
|
|
b31d40dbb6 | ||
|
|
970b966bcb | ||
|
|
6f1dc14bdc | ||
|
|
949e6f247d | ||
|
|
77a848cf0e | ||
|
|
21e268b8c2 | ||
|
|
580c761fa0 | ||
|
|
e13b12550c | ||
|
|
05dc421ea8 | ||
|
|
423957d023 | ||
|
|
c3b584973b | ||
|
|
74e1805970 | ||
|
|
e2743e2c61 | ||
|
|
59f0b90c72 | ||
|
|
c962096fd5 | ||
|
|
d9aa9f6cb3 | ||
|
|
874e422dbc | ||
|
|
63c825c056 | ||
|
|
b507fa2a09 | ||
|
|
eb2040edbd | ||
|
|
02ba1dabe7 | ||
|
|
03494d78a5 | ||
|
|
b598945903 | ||
|
|
8bf017e14e | ||
|
|
00e4d77503 | ||
|
|
e6efdadb6f | ||
|
|
02a8e7ea75 | ||
|
|
dc9b7591a0 | ||
|
|
7ae0d329e1 | ||
|
|
bff9ec86ff | ||
|
|
f445fbca3d | ||
|
|
9079ce30a0 | ||
|
|
2202f718b9 | ||
|
|
63b80634c2 | ||
|
|
9bbfeaba72 | ||
|
|
2d640dbe62 | ||
|
|
2ce3fc3bd6 | ||
|
|
009710f854 | ||
|
|
7eeff1b50a | ||
|
|
6f736d6415 | ||
|
|
3dc988dd08 | ||
|
|
ca292fbb85 | ||
|
|
53a3fe83cb | ||
|
|
d667940157 | ||
|
|
404fb9b39f | ||
|
|
d1c4bdb8eb | ||
|
|
7b67e4e811 | ||
|
|
2b387c8613 | ||
|
|
c7f6cb6747 | ||
|
|
5e5dc58482 | ||
|
|
80d0510b86 | ||
|
|
fcd3f7c3fe | ||
|
|
38abb9ca49 | ||
|
|
c47834f470 | ||
|
|
b899212d14 | ||
|
|
5a11e47653 | ||
|
|
d0faf0f1b6 | ||
|
|
27de6b456b | ||
|
|
92f6684ca9 | ||
|
|
59c45f5627 | ||
|
|
35c2075f73 | ||
|
|
4b659248f7 | ||
|
|
3d01e09198 | ||
|
|
a7805d9d4c | ||
|
|
2439c0e7cd | ||
|
|
67f99a4430 | ||
|
|
0a8c7700dc | ||
|
|
68ca17a0da | ||
|
|
dcc7c4d39e | ||
|
|
06fcbf8995 | ||
|
|
a939d06f77 | ||
|
|
11e342ae19 | ||
|
|
5ac40d13a5 | ||
|
|
1dea366cec | ||
|
|
7e5ae5a8af | ||
|
|
4e430b2fd4 | ||
|
|
d61c345630 | ||
|
|
0c7aa3fad2 | ||
|
|
8de7e4a326 | ||
|
|
4fd5d72d97 | ||
|
|
d08d2552aa | ||
|
|
6b6968cbbe | ||
|
|
49157a38ae | ||
|
|
402a0d3e43 | ||
|
|
5cd8cbd514 | ||
|
|
32537e85a2 | ||
|
|
d937ca85a2 | ||
|
|
6a5ed7f460 | ||
|
|
a158e96ba6 | ||
|
|
7ad43dbed7 | ||
|
|
63f261f169 | ||
|
|
f36849e815 | ||
|
|
b0bc0f9d85 | ||
|
|
cd271e6f04 | ||
|
|
d6bac74e1b | ||
|
|
eda7fa0fe7 | ||
|
|
191e0cc654 | ||
|
|
cc35a1e8aa | ||
|
|
7dfe85450d | ||
|
|
e8b341c69f | ||
|
|
77c397e0d1 | ||
|
|
15794f09c3 | ||
|
|
cdf8baeb10 | ||
|
|
e4b550afc1 | ||
|
|
49bb34b742 | ||
|
|
ab01d01fce | ||
|
|
4905c262ba | ||
|
|
ce05bb467e | ||
|
|
8f0fe89579 | ||
|
|
23bd6e20a4 | ||
|
|
3f0071ac58 | ||
|
|
776660923b | ||
|
|
32f170f644 | ||
|
|
93222990b5 | ||
|
|
8890296beb | ||
|
|
9907af8135 | ||
|
|
17ec357c1b | ||
|
|
575e967627 | ||
|
|
0b53e84761 | ||
|
|
fadf78aabd | ||
|
|
47fce14d16 | ||
|
|
149b6fbcd4 | ||
|
|
e649be230e | ||
|
|
6ba45c3d87 | ||
|
|
0e701189a3 | ||
|
|
af316607e1 | ||
|
|
548039c66f | ||
|
|
e75a2502d1 | ||
|
|
6be2148951 | ||
|
|
f149688da6 | ||
|
|
8f77e5874e | ||
|
|
eabd2d3e4c | ||
|
|
e460a4fe4f | ||
|
|
9e79da0efc | ||
|
|
6851d4a1d3 | ||
|
|
5705d367ed | ||
|
|
d1f88112ce | ||
|
|
eba1dbc8fa | ||
|
|
dcb819b087 | ||
|
|
25ced826c4 | ||
|
|
5a30c3219e | ||
|
|
6a5ac3729d | ||
|
|
31326224ac | ||
|
|
811a44574a | ||
|
|
6782442d8e | ||
|
|
a376d2f286 | ||
|
|
ce3e1c86bc | ||
|
|
20f7675135 | ||
|
|
dad3edee5e | ||
|
|
1bfdae53c6 | ||
|
|
e5902b9cf6 | ||
|
|
88829ca91a | ||
|
|
4bdf99b2f4 | ||
|
|
54883bb043 | ||
|
|
6db55b5eaa | ||
|
|
821dc6df63 | ||
|
|
8de86e26d6 | ||
|
|
1cf8b1fc7d | ||
|
|
f469fa224e | ||
|
|
56912ddaf7 | ||
|
|
620b1b4f02 | ||
|
|
2208c67b62 | ||
|
|
6f6b53ff73 | ||
|
|
529197fb7d | ||
|
|
953e63f55d | ||
|
|
783cbc634e | ||
|
|
9ac25b0d74 | ||
|
|
5f051ed2a9 | ||
|
|
b40d2f3b9a | ||
|
|
3f90276ce0 | ||
|
|
66e077f8f1 | ||
|
|
8426b1d91f | ||
|
|
3b55937085 | ||
|
|
da86a971b5 | ||
|
|
ff7f0e0a60 | ||
|
|
9233b610c2 | ||
|
|
9dc183b9b7 | ||
|
|
d000b6f884 | ||
|
|
e0b0dd134f | ||
|
|
e3d09bff36 | ||
|
|
5e901812ad | ||
|
|
b7f9ff8c98 | ||
|
|
76ed603bf1 | ||
|
|
337816b7f2 | ||
|
|
37b1566535 | ||
|
|
9864d1e62d | ||
|
|
e3a07377e3 | ||
|
|
f54b908dbb | ||
|
|
e3f3a5faf7 | ||
|
|
2046e15f65 | ||
|
|
4e8f554ae6 | ||
|
|
0f21040888 | ||
|
|
0d86fa0d95 | ||
|
|
0ad8826b51 | ||
|
|
a535f123ff | ||
|
|
4fcb2de2bb | ||
|
|
8885192db1 | ||
|
|
7ebf64939b | ||
|
|
6712b0927d | ||
|
|
8853bb7618 | ||
|
|
cbc8ec9880 | ||
|
|
6372099cb2 | ||
|
|
15fa114742 | ||
|
|
c16ded1377 | ||
|
|
8ecd0e0eb1 | ||
|
|
c5895ed61b | ||
|
|
35751844b0 | ||
|
|
fd66e9acb8 | ||
|
|
310ae58388 | ||
|
|
d4a31567cd | ||
|
|
d54abf66fe | ||
|
|
792b3b7a81 | ||
|
|
0345606903 | ||
|
|
06aec23c8a | ||
|
|
71a77250d0 | ||
|
|
af32ef24db | ||
|
|
214dca1d4e | ||
|
|
af8ca15e29 | ||
|
|
ac71aec39d | ||
|
|
9b7b363b49 | ||
|
|
948b54dc58 | ||
|
|
20553713a8 | ||
|
|
1424001ffd | ||
|
|
fa1c49aaf2 | ||
|
|
316369f117 | ||
|
|
2253d3c2cc | ||
|
|
b09bf68b7c | ||
|
|
526111af98 | ||
|
|
5942a33f42 | ||
|
|
62b077c4b2 | ||
|
|
89996f09f1 | ||
|
|
4b760c6361 | ||
|
|
293b7e3a05 | ||
|
|
12686ecb83 | ||
|
|
456912b134 | ||
|
|
88ac2ecf98 | ||
|
|
227cb078a5 | ||
|
|
a17582bf6d | ||
|
|
e8ba5dc6e2 | ||
|
|
f37d3324e6 | ||
|
|
fcb06c8c22 | ||
|
|
d8bdf4500a | ||
|
|
69ea4d5414 | ||
|
|
7e9f99e5c8 | ||
|
|
95541d99c4 | ||
|
|
05ca7e6d69 | ||
|
|
644a4a4886 | ||
|
|
7eb82ddbea | ||
|
|
e721d4ca9e | ||
|
|
c72582b679 | ||
|
|
217f0a82b5 | ||
|
|
1efd172b42 | ||
|
|
e8d0d03ad9 | ||
|
|
6cb97de793 | ||
|
|
77dbdc235a | ||
|
|
a9c87ea2b8 | ||
|
|
232829526c | ||
|
|
b5d8b2f14c | ||
|
|
c2673a627c | ||
|
|
bb49256188 | ||
|
|
1b6a284dd6 | ||
|
|
36b35b907e | ||
|
|
f61b310ff2 | ||
|
|
c1e84b3bf1 | ||
|
|
9971d27ef3 | ||
|
|
3c0835f725 | ||
|
|
023aba2b60 | ||
|
|
abc0a14dda | ||
|
|
dc23c6a445 | ||
|
|
161a0ffc59 | ||
|
|
b96436c906 | ||
|
|
cee7419208 | ||
|
|
9ff6e7a080 | ||
|
|
9112a568d9 | ||
|
|
8d0c9bc894 | ||
|
|
e7b4e4501e | ||
|
|
df80084822 | ||
|
|
4df2891b1a | ||
|
|
d0560677fa | ||
|
|
9130a3851f | ||
|
|
bb3fb26847 | ||
|
|
8406264138 | ||
|
|
e51f48ebe0 | ||
|
|
a0a2aa4c5d | ||
|
|
65cea7b5ec | ||
|
|
fb589419de | ||
|
|
ae9844d5a1 | ||
|
|
645ea21581 | ||
|
|
60fd675dff | ||
|
|
48903da446 | ||
|
|
5399df134c | ||
|
|
a858040ace | ||
|
|
d792b7d5ae | ||
|
|
8b3ed1a7dc | ||
|
|
47004857ea | ||
|
|
82a6b8d7ef | ||
|
|
a30bcf1fa7 | ||
|
|
febe978fa1 | ||
|
|
1eeefcf495 | ||
|
|
ff6b0eee6c | ||
|
|
a7c8eaa937 | ||
|
|
55545e31e8 | ||
|
|
34132c6da7 | ||
|
|
fbafa99a3b | ||
|
|
bd63805611 | ||
|
|
ae8398a434 | ||
|
|
0b9318de88 | ||
|
|
55e4bbf9db | ||
|
|
f33a572751 | ||
|
|
2849cedbd8 | ||
|
|
041d1b29d1 | ||
|
|
cdcdc2fee1 | ||
|
|
8e35d5bc8d | ||
|
|
f12e368707 | ||
|
|
f01895c7a7 | ||
|
|
4ce0c067e1 | ||
|
|
728a2cbfae | ||
|
|
a4aab3b933 | ||
|
|
11ee0d6544 | ||
|
|
2b80dad47c | ||
|
|
7bb13ec1c2 | ||
|
|
344010d2bb | ||
|
|
ef37ecddc6 | ||
|
|
91a38e205e | ||
|
|
bd4de382e4 | ||
|
|
623244c3bd | ||
|
|
7a06ff6dec | ||
|
|
6d735e24ff | ||
|
|
5c30d544fa | ||
|
|
54a3876288 | ||
|
|
57f71afb98 | ||
|
|
d8ec198f6f | ||
|
|
faa6d7c9f6 | ||
|
|
d79a5b17a5 | ||
|
|
34deea5d32 |
50
CHANGELOG
50
CHANGELOG
@@ -1,3 +1,53 @@
|
||||
KeepassDX (2.5.0.0beta23)
|
||||
* New, more secure database creation workflow
|
||||
* Recognize more database files
|
||||
* Add alias for history files (WARNING: history is erased)
|
||||
* New Biometric unlock (Fingerprint with new API)
|
||||
* Fix entry references
|
||||
* Fix OOM with KeyFile
|
||||
* Fix small issues
|
||||
|
||||
KeepassDX (2.5.0.0beta22)
|
||||
* Rebuild code for actions
|
||||
* Add UUID as entry view
|
||||
* Fix bug with natural order
|
||||
* Fix number of entries in databaseV1
|
||||
* New entry views
|
||||
|
||||
KeepassDX (2.5.0.0beta21)
|
||||
* Fix nested groups no longer visible in V1 databases
|
||||
* Improved data import algorithm for V1 databases
|
||||
* Add natural database sort
|
||||
* Add username database sort
|
||||
* Fix button disabled with only KeyFile
|
||||
* Show the number of entries in a group
|
||||
|
||||
KeepassDX (2.5.0.0beta20)
|
||||
* Fix a major bug that displays an entry history
|
||||
|
||||
KeepassDX (2.5.0.0beta19)
|
||||
* Add lock button always visible
|
||||
* New connection workflow
|
||||
* Code refactored in Kotlin
|
||||
* Better notification implementation
|
||||
* Better views for large screen
|
||||
* Magikeyboard enhancement
|
||||
* Fix Recycle Bin
|
||||
* Fix memory when load database
|
||||
* Fix small bugs
|
||||
|
||||
KeepassDX (2.5.0.0beta18)
|
||||
* New recent databases views
|
||||
* New information dialog
|
||||
* Custom fields for the Magikeyboard
|
||||
* Timeout for the Magikeyboard
|
||||
* Long press for keyboard selection
|
||||
* Fix memory when opening the database
|
||||
* Memory management for attachments
|
||||
|
||||
KeepassDX (2.5.0.0beta17)
|
||||
* Fix font and search
|
||||
|
||||
KeepassDX (2.5.0.0beta16)
|
||||
* New search in a single fragment
|
||||
* Search suggestions
|
||||
|
||||
55
FAQ.md
55
FAQ.md
@@ -1,55 +0,0 @@
|
||||
# F.A.Q.
|
||||
|
||||
## Why KeePass DX?
|
||||
|
||||
KeePass DX is an **Android password manager** implemented from Keepass password manager.
|
||||
|
||||
KeePass DX was created to meet the security and usability needs of a KeePass application on Android :
|
||||
|
||||
- To be easy to use with **secure password management and form filling tools**.
|
||||
- To use only tools under **open source license** to guarantee the security of the application (With [open source store](https://f-droid.org/en/) and no closed API).
|
||||
- To be in a **native langage** (java) for weight, security and a better integration of the application.
|
||||
- To respect **Android design, architecture and ergonomic**.
|
||||
|
||||
## What makes KeePass DX stand out from other password managers?
|
||||
|
||||
- We **do not recover your sensitive data** on a private server or a closed cloud, you have control of your passwords.
|
||||
- We respect **KeePass file standards** to maintain compatibility and data porting on different devices (computers and portable devices with different operating system).
|
||||
- The code is **open source**, which implies increased **security**, you can check how the encryption algorithms are implemented.
|
||||
- We remain attentive to **your needs** and we can even integrate the features that you have defined.
|
||||
- We **do not put advertising** even in the free version.
|
||||
|
||||
## How am I sure my passwords are safely stored on the application?
|
||||
|
||||
- We allow users to save and use passwords, keys and digital identities in a secure way by **integrating the last encryption algorithms** and **Android architecture standards**.
|
||||
- You can increase the security of your database by increasing the rounds of encryption keys. *(In Settings -> Database Settings when your database is open)* **Warning**: *Increase the number of rounds sparingly to have a reasonable opening time.*
|
||||
|
||||
## Can I store my data on a cloud storage?
|
||||
|
||||
**Yes** this is possible. Otherwise, we **recommend using cloud with personal server and open source license**, like [NextCloud](https://f-droid.org/en/packages/com.nextcloud.client/) to be sure how your databases are stored.
|
||||
|
||||
## Can I recover my passwords on another device if I loose my main device?
|
||||
|
||||
**Yes** you can, but you **must first save the .kdb or .kdbx file from your database to an external storage** *(like a hardrive or a cloud)*.
|
||||
We recommend you save your data after each modification so incase you loose your android device you could retrieve the data and import it into the new KeePass DX installed on the new android device.
|
||||
|
||||
## Why are updates not available at the same time on all stores?
|
||||
|
||||
- **PlayStore** only needs an APK generated and manually signed to be available on the store, it usually takes **20 minutes** to be available because it is deployed with fastlane. But the management of the APK and its data by the google servers is obscure.
|
||||
- **F-Droid**, to **ensure that the code is open source**, checks the sources directly on git repository (by checking the presence of new tags) and builds itself the APK that the server signs during the compilation of code and dependencies. Updating the project will take **1-10 days** for F-Droid to analyze all available repositories, build sources and deploy the generated APK. So F-Droid is slower for deployment but it is run by **volunteers** and guaranteed a **clean APK**. :)
|
||||
|
||||
## Why not an online version?
|
||||
|
||||
The offline and online client concepts only exists with Keepass2Android because the file access network tools are directly integrated into the code of the main application. Which is a very dubious choice knowing that **it is not normally the purpose of a password management application to take care of external file synchronization on clouds** (which can be under closed licensed and recover your data base), it is rather the purpose of the [file management application](https://developer.android.com/guide/topics/providers/document-provider).
|
||||
|
||||
## Can I open my database easily other than with a password?
|
||||
|
||||
**Yes**, we have integrated a secure openning option of fingerprint for android devices that support this feature, so no one can access the application without scanning his/her fingerprint or fill a master key.
|
||||
|
||||
## Can I open my database without my master key (master password and/or key file)?
|
||||
|
||||
**No**, you can not open a database file without the master password (and / or) the associated key file. Be sure to remember your master password and save the key file in a safe place.
|
||||
|
||||
## Can I suggest features and report bugs for the application?
|
||||
**Yes**, we welcome this you could go ahead and do that on our github:
|
||||
https://github.com/Kunzisoft/KeePassDX
|
||||
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
93
LICENSES/LICENSE_FONT_FIRA_MONO_REGULAR.txt
Normal file
93
LICENSES/LICENSE_FONT_FIRA_MONO_REGULAR.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
Digitized data copyright (c) 2012-2015, The Mozilla Foundation and Telefonica S.A.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -15,6 +15,7 @@
|
||||
* **AutoFill** and Integration
|
||||
* Field filling **keyboard**
|
||||
* Precise management of **settings**
|
||||
* Code written in **native language** *(Kotlin / Java / JNI / C)*
|
||||
|
||||
Keepass DX is **open source** and **ad-free**.
|
||||
|
||||
@@ -53,7 +54,7 @@ You can contribute in different ways to help us on our work.
|
||||
|
||||
## F.A.Q.
|
||||
|
||||
Other questions? You can read the [F.A.Q.](https://github.com/Kunzisoft/KeePassDX/blob/master/FAQ.md)
|
||||
Other questions? You can read the [F.A.Q.](https://www.keepassdx.com/FAQ)
|
||||
|
||||
## Other devices
|
||||
|
||||
@@ -63,11 +64,11 @@ Other questions? You can read the [F.A.Q.](https://github.com/Kunzisoft/KeePassD
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2017 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||
Copyright (c) 2019 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||
|
||||
This file is part of KeePass DX.
|
||||
|
||||
KeePass DX is free software: you can redistribute it and/or modify
|
||||
[KeePass DX](https://www.keepassdx.com) is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
.cxx
|
||||
.externalNativeBuild
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
compileSdkVersion 27
|
||||
buildToolsVersion '27.0.3'
|
||||
compileSdkVersion 28
|
||||
buildToolsVersion '28.0.3'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 27
|
||||
versionCode = 16
|
||||
versionName = "2.5.0.0beta16"
|
||||
targetSdkVersion 28
|
||||
versionCode = 23
|
||||
versionName = "2.5.0.0beta23"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
@@ -39,7 +42,7 @@ android {
|
||||
productFlavors {
|
||||
libre {
|
||||
applicationIdSuffix = ".libre"
|
||||
versionNameSuffix "-libre"
|
||||
buildConfigField "String", "BUILD_VERSION", "\"libre\""
|
||||
buildConfigField "boolean", "FULL_VERSION", "true"
|
||||
buildConfigField "boolean", "CLOSED_STORE", "false"
|
||||
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Dark\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
||||
@@ -48,7 +51,7 @@ android {
|
||||
}
|
||||
pro {
|
||||
applicationIdSuffix = ".pro"
|
||||
versionNameSuffix "-pro"
|
||||
buildConfigField "String", "BUILD_VERSION", "\"pro\""
|
||||
buildConfigField "boolean", "FULL_VERSION", "true"
|
||||
buildConfigField "boolean", "CLOSED_STORE", "true"
|
||||
buildConfigField "String[]", "STYLES_DISABLED", "{}"
|
||||
@@ -56,7 +59,7 @@ android {
|
||||
}
|
||||
free {
|
||||
applicationIdSuffix = ".free"
|
||||
versionNameSuffix "-free"
|
||||
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
||||
buildConfigField "boolean", "FULL_VERSION", "false"
|
||||
buildConfigField "boolean", "CLOSED_STORE", "true"
|
||||
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Dark\",\"KeepassDXStyle_Blue\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
||||
@@ -76,40 +79,36 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
def supportVersion = "27.1.1"
|
||||
def spongycastleVersion = "1.58.0.0"
|
||||
def permissionDispatcherVersion = "3.1.0"
|
||||
def room_version = "2.1.0"
|
||||
|
||||
dependencies {
|
||||
implementation "com.android.support:appcompat-v7:$supportVersion"
|
||||
implementation "com.android.support:design:$supportVersion"
|
||||
implementation "com.android.support:preference-v7:$supportVersion"
|
||||
implementation "com.android.support:preference-v14:$supportVersion"
|
||||
implementation "com.android.support:cardview-v7:$supportVersion"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.preference:preference:1.1.0'
|
||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.biometric:biometric:1.0.0-beta01'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
|
||||
implementation "com.madgag.spongycastle:core:$spongycastleVersion"
|
||||
implementation "com.madgag.spongycastle:prov:$spongycastleVersion"
|
||||
// Expandable view
|
||||
implementation 'net.cachapa.expandablelayout:expandablelayout:2.9.2'
|
||||
// Time
|
||||
implementation 'joda-time:joda-time:2.9.9'
|
||||
implementation 'org.sufficientlysecure:html-textview:3.5'
|
||||
implementation 'com.nononsenseapps:filepicker:4.1.0'
|
||||
// Education
|
||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.12.0'
|
||||
// Permissions
|
||||
implementation("com.github.hotchemi:permissionsdispatcher:$permissionDispatcherVersion") {
|
||||
// if you don't use android.app.Fragment you can exclude support for them
|
||||
exclude module: "support-v13"
|
||||
}
|
||||
annotationProcessor "com.github.hotchemi:permissionsdispatcher-processor:$permissionDispatcherVersion"
|
||||
// Apache Commons Collections
|
||||
implementation 'commons-collections:commons-collections:3.2.1'
|
||||
implementation 'org.apache.commons:commons-io:1.3.2'
|
||||
// Base64
|
||||
implementation 'biz.source_code:base64coder:2010-12-19'
|
||||
implementation 'com.google.code.gson:gson:2.8.4'
|
||||
implementation 'com.google.guava:guava:23.0-android'
|
||||
// Icon pack, classic for all, material for libre and pro
|
||||
// Icon pack
|
||||
implementation project(path: ':icon-pack-classic')
|
||||
implementation project(path: ':icon-pack-material')
|
||||
implementation project(path: ':magikeyboard')
|
||||
implementation project(path: ':keepass-model')
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests;
|
||||
|
||||
import android.test.AndroidTestCase;
|
||||
|
||||
import com.kunzisoft.keepass.tests.database.TestData;
|
||||
|
||||
public class AccentTest extends AndroidTestCase {
|
||||
|
||||
private static final String KEYFILE = "";
|
||||
private static final String PASSWORD = "é";
|
||||
private static final String ASSET = "accent.kdb";
|
||||
private static final String FILENAME = "/sdcard/accent.kdb";
|
||||
|
||||
public void testOpen() {
|
||||
|
||||
try {
|
||||
TestData.GetDb(getContext(), ASSET, PASSWORD, KEYFILE, FILENAME);
|
||||
} catch (Exception e) {
|
||||
assertTrue("Failed to open database", false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests;
|
||||
|
||||
import junit.framework.Test;
|
||||
import junit.framework.TestSuite;
|
||||
|
||||
import android.test.suitebuilder.TestSuiteBuilder;
|
||||
|
||||
public class AllTests extends TestSuite {
|
||||
|
||||
public static Test suite() {
|
||||
return new TestSuiteBuilder(AllTests.class)
|
||||
.includeAllPackagesUnderHere()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import com.kunzisoft.keepass.database.PwDate;
|
||||
|
||||
public class PwDateTest extends TestCase {
|
||||
public void testDate() {
|
||||
PwDate jDate = new PwDate(System.currentTimeMillis());
|
||||
|
||||
PwDate intermediate = (PwDate) jDate.clone();
|
||||
|
||||
PwDate cDate = new PwDate(intermediate.getCDate(), 0);
|
||||
|
||||
assertTrue("jDate and intermediate not equal", jDate.equals(intermediate));
|
||||
assertTrue("jDate and cDate not equal", cDate.equals(jDate));
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2018 Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
@@ -17,19 +17,21 @@
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.adapters;
|
||||
package com.kunzisoft.keepass.tests
|
||||
|
||||
import android.view.View;
|
||||
import junit.framework.TestCase
|
||||
|
||||
import com.kunzisoft.keepass.R;
|
||||
import com.kunzisoft.keepass.database.element.PwDate
|
||||
import org.junit.Assert
|
||||
|
||||
class EntryViewHolder extends BasicViewHolder {
|
||||
class PwDateTest : TestCase() {
|
||||
|
||||
EntryViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
container = itemView.findViewById(R.id.entry_container);
|
||||
icon = itemView.findViewById(R.id.entry_icon);
|
||||
text = itemView.findViewById(R.id.entry_text);
|
||||
subText = itemView.findViewById(R.id.entry_subtext);
|
||||
fun testDate() {
|
||||
val jDate = PwDate(System.currentTimeMillis())
|
||||
val intermediate = PwDate(jDate)
|
||||
val cDate = PwDate(intermediate.byteArrayDate!!, 0)
|
||||
|
||||
Assert.assertTrue("jDate and intermediate not equal", jDate == intermediate)
|
||||
Assert.assertTrue("jDate and cDate not equal", cDate == jDate)
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Calendar;
|
||||
|
||||
|
||||
import android.test.AndroidTestCase;
|
||||
|
||||
import com.kunzisoft.keepass.database.PwEntryV3;
|
||||
import com.kunzisoft.keepass.tests.database.TestData;
|
||||
|
||||
public class PwEntryTestV3 extends AndroidTestCase {
|
||||
PwEntryV3 mPE;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
mPE = (PwEntryV3) TestData.GetTest1(getContext()).getEntryAt(0);
|
||||
|
||||
}
|
||||
|
||||
public void testName() {
|
||||
assertTrue("Name was " + mPE.getTitle(), mPE.getTitle().equals("Amazon"));
|
||||
}
|
||||
|
||||
public void testPassword() throws UnsupportedEncodingException {
|
||||
String sPass = "12345";
|
||||
byte[] password = sPass.getBytes("UTF-8");
|
||||
|
||||
assertArrayEquals(password, mPE.getPasswordBytes());
|
||||
}
|
||||
|
||||
public void testCreation() {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTime(mPE.getCreationTime().getDate());
|
||||
|
||||
assertEquals("Incorrect year.", cal.get(Calendar.YEAR), 2009);
|
||||
assertEquals("Incorrect month.", cal.get(Calendar.MONTH), 3);
|
||||
assertEquals("Incorrect day.", cal.get(Calendar.DAY_OF_MONTH), 23);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests;
|
||||
|
||||
import com.kunzisoft.keepass.database.AutoType;
|
||||
import com.kunzisoft.keepass.database.PwEntryV4;
|
||||
import com.kunzisoft.keepass.database.PwGroupV4;
|
||||
import com.kunzisoft.keepass.database.PwIconCustom;
|
||||
import com.kunzisoft.keepass.database.PwIconStandard;
|
||||
import com.kunzisoft.keepass.database.security.ProtectedBinary;
|
||||
import com.kunzisoft.keepass.database.security.ProtectedString;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class PwEntryTestV4 extends TestCase {
|
||||
public void testAssign() {
|
||||
PwEntryV4 entry = new PwEntryV4();
|
||||
|
||||
entry.setAdditional("test223");
|
||||
|
||||
entry.setAutoType(new AutoType());
|
||||
entry.getAutoType().defaultSequence = "1324";
|
||||
entry.getAutoType().enabled = true;
|
||||
entry.getAutoType().obfuscationOptions = 123412432109L;
|
||||
entry.getAutoType().put("key", "value");
|
||||
|
||||
entry.setBackgroupColor("blue");
|
||||
entry.putProtectedBinary("key1", new ProtectedBinary(false, new byte[] {0,1}));
|
||||
entry.setIconCustom(new PwIconCustom(UUID.randomUUID(), new byte[0]));
|
||||
entry.setForegroundColor("red");
|
||||
entry.addToHistory(new PwEntryV4());
|
||||
entry.setIconStandard(new PwIconStandard(5));
|
||||
entry.setOverrideURL("override");
|
||||
entry.setParent(new PwGroupV4());
|
||||
entry.addExtraField("key2", new ProtectedString(false, "value2"));
|
||||
entry.setUrl("http://localhost");
|
||||
entry.setUUID(UUID.randomUUID());
|
||||
|
||||
PwEntryV4 target = new PwEntryV4();
|
||||
target.updateWith(entry);
|
||||
|
||||
/* This test is not so useful now that I am not implementing value equality for Entries
|
||||
assertTrue("Entries do not match.", entry.equals(target));
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests;
|
||||
|
||||
|
||||
import android.test.AndroidTestCase;
|
||||
|
||||
import com.kunzisoft.keepass.database.PwGroupV3;
|
||||
import com.kunzisoft.keepass.tests.database.TestData;
|
||||
|
||||
public class PwGroupTest extends AndroidTestCase {
|
||||
|
||||
PwGroupV3 mPG;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
mPG = (PwGroupV3) TestData.GetTest1(getContext()).getGroups().get(0);
|
||||
|
||||
}
|
||||
|
||||
public void testGroupName() {
|
||||
assertTrue("Name was " + mPG.getName(), mPG.getName().equals("Internet"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
|
||||
import com.kunzisoft.keepass.utils.EmptyUtils;
|
||||
import com.kunzisoft.keepass.utils.UriUtil;
|
||||
|
||||
public class TestUtil {
|
||||
private static final File sdcard = Environment.getExternalStorageDirectory();
|
||||
|
||||
public static void extractKey(Context ctx, String asset, String target) throws Exception {
|
||||
|
||||
InputStream key = ctx.getAssets().open(asset, AssetManager.ACCESS_STREAMING);
|
||||
|
||||
FileOutputStream keyFile = new FileOutputStream(target);
|
||||
while (true) {
|
||||
byte[] buf = new byte[1024];
|
||||
int read = key.read(buf);
|
||||
if ( read == -1 ) {
|
||||
break;
|
||||
} else {
|
||||
keyFile.write(buf, 0, read);
|
||||
}
|
||||
}
|
||||
|
||||
keyFile.close();
|
||||
|
||||
}
|
||||
|
||||
public static InputStream getKeyFileInputStream(Context ctx, String keyfile) throws FileNotFoundException {
|
||||
InputStream keyIs = null;
|
||||
if (!EmptyUtils.isNullOrEmpty(keyfile)) {
|
||||
Uri uri = UriUtil.parseDefaultFile(keyfile);
|
||||
keyIs = UriUtil.getUriInputStream(ctx, uri);
|
||||
}
|
||||
|
||||
return keyIs;
|
||||
}
|
||||
|
||||
public static String getSdPath(String filename) {
|
||||
File file = new File(sdcard, filename);
|
||||
return file.getAbsolutePath();
|
||||
}
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Calendar;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import com.kunzisoft.keepass.database.PwDate;
|
||||
import com.kunzisoft.keepass.stream.LEDataInputStream;
|
||||
import com.kunzisoft.keepass.stream.LEDataOutputStream;
|
||||
import com.kunzisoft.keepass.utils.Types;
|
||||
|
||||
public class TypesTest extends TestCase {
|
||||
|
||||
public void testReadWriteLongZero() {
|
||||
testReadWriteLong((byte) 0);
|
||||
}
|
||||
|
||||
public void testReadWriteLongMax() {
|
||||
testReadWriteLong(Byte.MAX_VALUE);
|
||||
}
|
||||
|
||||
public void testReadWriteLongMin() {
|
||||
testReadWriteLong(Byte.MIN_VALUE);
|
||||
}
|
||||
|
||||
public void testReadWriteLongRnd() {
|
||||
Random rnd = new Random();
|
||||
byte[] buf = new byte[1];
|
||||
rnd.nextBytes(buf);
|
||||
|
||||
testReadWriteLong(buf[0]);
|
||||
}
|
||||
|
||||
private void testReadWriteLong(byte value) {
|
||||
byte[] orig = new byte[8];
|
||||
byte[] dest = new byte[8];
|
||||
|
||||
setArray(orig, value, 0, 8);
|
||||
|
||||
long one = LEDataInputStream.readLong(orig, 0);
|
||||
LEDataOutputStream.writeLong(one, dest, 0);
|
||||
|
||||
assertArrayEquals(orig, dest);
|
||||
|
||||
}
|
||||
|
||||
public void testReadWriteIntZero() {
|
||||
testReadWriteInt((byte) 0);
|
||||
}
|
||||
|
||||
public void testReadWriteIntMin() {
|
||||
testReadWriteInt(Byte.MIN_VALUE);
|
||||
}
|
||||
|
||||
public void testReadWriteIntMax() {
|
||||
testReadWriteInt(Byte.MAX_VALUE);
|
||||
}
|
||||
|
||||
private void testReadWriteInt(byte value) {
|
||||
byte[] orig = new byte[4];
|
||||
byte[] dest = new byte[4];
|
||||
|
||||
for (int i = 0; i < 4; i++ ) {
|
||||
orig[i] = 0;
|
||||
}
|
||||
|
||||
setArray(orig, value, 0, 4);
|
||||
|
||||
int one = LEDataInputStream.readInt(orig, 0);
|
||||
|
||||
LEDataOutputStream.writeInt(one, dest, 0);
|
||||
|
||||
assertArrayEquals(orig, dest);
|
||||
|
||||
}
|
||||
|
||||
private void setArray(byte[] buf, byte value, int offset, int size) {
|
||||
for (int i = offset; i < offset + size; i++) {
|
||||
buf[i] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public void testReadWriteShortOne() {
|
||||
byte[] orig = new byte[2];
|
||||
byte[] dest = new byte[2];
|
||||
|
||||
orig[0] = 0;
|
||||
orig[1] = 1;
|
||||
|
||||
int one = LEDataInputStream.readUShort(orig, 0);
|
||||
dest = LEDataOutputStream.writeUShortBuf(one);
|
||||
|
||||
assertArrayEquals(orig, dest);
|
||||
|
||||
}
|
||||
|
||||
public void testReadWriteShortMin() {
|
||||
testReadWriteShort(Byte.MIN_VALUE);
|
||||
}
|
||||
|
||||
public void testReadWriteShortMax() {
|
||||
testReadWriteShort(Byte.MAX_VALUE);
|
||||
}
|
||||
|
||||
private void testReadWriteShort(byte value) {
|
||||
byte[] orig = new byte[2];
|
||||
byte[] dest = new byte[2];
|
||||
|
||||
setArray(orig, value, 0, 2);
|
||||
|
||||
int one = LEDataInputStream.readUShort(orig, 0);
|
||||
LEDataOutputStream.writeUShort(one, dest, 0);
|
||||
|
||||
assertArrayEquals(orig, dest);
|
||||
|
||||
}
|
||||
|
||||
public void testReadWriteByteZero() {
|
||||
testReadWriteByte((byte) 0);
|
||||
}
|
||||
|
||||
public void testReadWriteByteMin() {
|
||||
testReadWriteByte(Byte.MIN_VALUE);
|
||||
}
|
||||
|
||||
public void testReadWriteByteMax() {
|
||||
testReadWriteShort(Byte.MAX_VALUE);
|
||||
}
|
||||
|
||||
private void testReadWriteByte(byte value) {
|
||||
byte[] orig = new byte[1];
|
||||
byte[] dest = new byte[1];
|
||||
|
||||
setArray(orig, value, 0, 1);
|
||||
|
||||
int one = Types.readUByte(orig, 0);
|
||||
Types.writeUByte(one, dest, 0);
|
||||
|
||||
assertArrayEquals(orig, dest);
|
||||
|
||||
}
|
||||
|
||||
public void testDate() {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
|
||||
Calendar expected = Calendar.getInstance();
|
||||
expected.set(2008, 1, 2, 3, 4, 5);
|
||||
|
||||
byte[] buf = PwDate.writeTime(expected.getTime(), cal);
|
||||
Calendar actual = Calendar.getInstance();
|
||||
actual.setTime(PwDate.readTime(buf, 0, cal));
|
||||
|
||||
assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR));
|
||||
assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH));
|
||||
assertEquals("Day mismatch: ", 1, actual.get(Calendar.DAY_OF_MONTH));
|
||||
assertEquals("Hour mismatch: ", 3, actual.get(Calendar.HOUR_OF_DAY));
|
||||
assertEquals("Minute mismatch: ", 4, actual.get(Calendar.MINUTE));
|
||||
assertEquals("Second mismatch: ", 5, actual.get(Calendar.SECOND));
|
||||
}
|
||||
|
||||
public void testUUID() {
|
||||
Random rnd = new Random();
|
||||
byte[] bUUID = new byte[16];
|
||||
rnd.nextBytes(bUUID);
|
||||
|
||||
UUID uuid = Types.bytestoUUID(bUUID);
|
||||
byte[] eUUID = Types.UUIDtoBytes(uuid);
|
||||
|
||||
assertArrayEquals("UUID match failed", bUUID, eUUID);
|
||||
}
|
||||
|
||||
public void testULongMax() throws Exception {
|
||||
byte[] ulongBytes = new byte[8];
|
||||
for (int i = 0; i < ulongBytes.length; i++) {
|
||||
ulongBytes[i] = -1;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
LEDataOutputStream leos = new LEDataOutputStream(bos);
|
||||
leos.writeLong(Types.ULONG_MAX_VALUE);
|
||||
leos.close();
|
||||
|
||||
byte[] uLongMax = bos.toByteArray();
|
||||
|
||||
assertArrayEquals(ulongBytes, uLongMax);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.Calendar
|
||||
import java.util.Random
|
||||
|
||||
import junit.framework.TestCase
|
||||
|
||||
import com.kunzisoft.keepass.database.element.PwDate
|
||||
import com.kunzisoft.keepass.stream.LEDataInputStream
|
||||
import com.kunzisoft.keepass.stream.LEDataOutputStream
|
||||
import com.kunzisoft.keepass.utils.Types
|
||||
|
||||
class TypesTest : TestCase() {
|
||||
|
||||
fun testReadWriteLongZero() {
|
||||
testReadWriteLong(0.toByte())
|
||||
}
|
||||
|
||||
fun testReadWriteLongMax() {
|
||||
testReadWriteLong(java.lang.Byte.MAX_VALUE)
|
||||
}
|
||||
|
||||
fun testReadWriteLongMin() {
|
||||
testReadWriteLong(java.lang.Byte.MIN_VALUE)
|
||||
}
|
||||
|
||||
fun testReadWriteLongRnd() {
|
||||
val rnd = Random()
|
||||
val buf = ByteArray(1)
|
||||
rnd.nextBytes(buf)
|
||||
|
||||
testReadWriteLong(buf[0])
|
||||
}
|
||||
|
||||
private fun testReadWriteLong(value: Byte) {
|
||||
val orig = ByteArray(8)
|
||||
val dest = ByteArray(8)
|
||||
|
||||
setArray(orig, value, 0, 8)
|
||||
|
||||
val one = LEDataInputStream.readLong(orig, 0)
|
||||
LEDataOutputStream.writeLong(one, dest, 0)
|
||||
|
||||
assertArrayEquals(orig, dest)
|
||||
|
||||
}
|
||||
|
||||
fun testReadWriteIntZero() {
|
||||
testReadWriteInt(0.toByte())
|
||||
}
|
||||
|
||||
fun testReadWriteIntMin() {
|
||||
testReadWriteInt(java.lang.Byte.MIN_VALUE)
|
||||
}
|
||||
|
||||
fun testReadWriteIntMax() {
|
||||
testReadWriteInt(java.lang.Byte.MAX_VALUE)
|
||||
}
|
||||
|
||||
private fun testReadWriteInt(value: Byte) {
|
||||
val orig = ByteArray(4)
|
||||
val dest = ByteArray(4)
|
||||
|
||||
for (i in 0..3) {
|
||||
orig[i] = 0
|
||||
}
|
||||
|
||||
setArray(orig, value, 0, 4)
|
||||
|
||||
val one = LEDataInputStream.readInt(orig, 0)
|
||||
|
||||
LEDataOutputStream.writeInt(one, dest, 0)
|
||||
|
||||
assertArrayEquals(orig, dest)
|
||||
|
||||
}
|
||||
|
||||
private fun setArray(buf: ByteArray, value: Byte, offset: Int, size: Int) {
|
||||
for (i in offset until offset + size) {
|
||||
buf[i] = value
|
||||
}
|
||||
}
|
||||
|
||||
fun testReadWriteShortOne() {
|
||||
val orig = ByteArray(2)
|
||||
|
||||
orig[0] = 0
|
||||
orig[1] = 1
|
||||
|
||||
val one = LEDataInputStream.readUShort(orig, 0)
|
||||
val dest = LEDataOutputStream.writeUShortBuf(one)
|
||||
|
||||
assertArrayEquals(orig, dest)
|
||||
|
||||
}
|
||||
|
||||
fun testReadWriteShortMin() {
|
||||
testReadWriteShort(java.lang.Byte.MIN_VALUE)
|
||||
}
|
||||
|
||||
fun testReadWriteShortMax() {
|
||||
testReadWriteShort(java.lang.Byte.MAX_VALUE)
|
||||
}
|
||||
|
||||
private fun testReadWriteShort(value: Byte) {
|
||||
val orig = ByteArray(2)
|
||||
val dest = ByteArray(2)
|
||||
|
||||
setArray(orig, value, 0, 2)
|
||||
|
||||
val one = LEDataInputStream.readUShort(orig, 0)
|
||||
LEDataOutputStream.writeUShort(one, dest, 0)
|
||||
|
||||
assertArrayEquals(orig, dest)
|
||||
|
||||
}
|
||||
|
||||
fun testReadWriteByteZero() {
|
||||
testReadWriteByte(0.toByte())
|
||||
}
|
||||
|
||||
fun testReadWriteByteMin() {
|
||||
testReadWriteByte(java.lang.Byte.MIN_VALUE)
|
||||
}
|
||||
|
||||
fun testReadWriteByteMax() {
|
||||
testReadWriteShort(java.lang.Byte.MAX_VALUE)
|
||||
}
|
||||
|
||||
private fun testReadWriteByte(value: Byte) {
|
||||
val orig = ByteArray(1)
|
||||
val dest = ByteArray(1)
|
||||
|
||||
setArray(orig, value, 0, 1)
|
||||
|
||||
val one = Types.readUByte(orig, 0)
|
||||
Types.writeUByte(one, dest, 0)
|
||||
|
||||
assertArrayEquals(orig, dest)
|
||||
|
||||
}
|
||||
|
||||
fun testDate() {
|
||||
val cal = Calendar.getInstance()
|
||||
|
||||
val expected = Calendar.getInstance()
|
||||
expected.set(2008, 1, 2, 3, 4, 5)
|
||||
|
||||
val buf = PwDate.writeTime(expected.time, cal)
|
||||
val actual = Calendar.getInstance()
|
||||
actual.time = PwDate.readTime(buf, 0, cal)
|
||||
|
||||
assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR))
|
||||
assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH))
|
||||
assertEquals("Day mismatch: ", 1, actual.get(Calendar.DAY_OF_MONTH))
|
||||
assertEquals("Hour mismatch: ", 3, actual.get(Calendar.HOUR_OF_DAY))
|
||||
assertEquals("Minute mismatch: ", 4, actual.get(Calendar.MINUTE))
|
||||
assertEquals("Second mismatch: ", 5, actual.get(Calendar.SECOND))
|
||||
}
|
||||
|
||||
fun testUUID() {
|
||||
val rnd = Random()
|
||||
val bUUID = ByteArray(16)
|
||||
rnd.nextBytes(bUUID)
|
||||
|
||||
val uuid = Types.bytestoUUID(bUUID)
|
||||
val eUUID = Types.UUIDtoBytes(uuid)
|
||||
|
||||
assertArrayEquals("UUID match failed", bUUID, eUUID)
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun testULongMax() {
|
||||
val ulongBytes = ByteArray(8)
|
||||
for (i in ulongBytes.indices) {
|
||||
ulongBytes[i] = -1
|
||||
}
|
||||
|
||||
val bos = ByteArrayOutputStream()
|
||||
val leos = LEDataOutputStream(bos)
|
||||
leos.writeLong(Types.ULONG_MAX_VALUE)
|
||||
leos.close()
|
||||
|
||||
val uLongMax = bos.toByteArray()
|
||||
|
||||
assertArrayEquals(ulongBytes, uLongMax)
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.crypto;
|
||||
|
||||
import com.kunzisoft.keepass.crypto.CipherFactory;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Random;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
|
||||
public class AESTest extends TestCase {
|
||||
|
||||
private Random mRand = new Random();
|
||||
|
||||
public void testEncrypt() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
|
||||
// Test above below and at the blocksize
|
||||
testFinal(15);
|
||||
testFinal(16);
|
||||
testFinal(17);
|
||||
|
||||
// Test random larger sizes
|
||||
int size = mRand.nextInt(494) + 18;
|
||||
testFinal(size);
|
||||
}
|
||||
|
||||
private void testFinal(int dataSize) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
|
||||
|
||||
// Generate some input
|
||||
byte[] input = new byte[dataSize];
|
||||
mRand.nextBytes(input);
|
||||
|
||||
// Generate key
|
||||
byte[] keyArray = new byte[32];
|
||||
mRand.nextBytes(keyArray);
|
||||
SecretKeySpec key = new SecretKeySpec(keyArray, "AES");
|
||||
|
||||
// Generate IV
|
||||
byte[] ivArray = new byte[16];
|
||||
mRand.nextBytes(ivArray);
|
||||
IvParameterSpec iv = new IvParameterSpec(ivArray);
|
||||
|
||||
Cipher android = CipherFactory.getInstance("AES/CBC/PKCS5Padding", true);
|
||||
android.init(Cipher.ENCRYPT_MODE, key, iv);
|
||||
byte[] outAndroid = android.doFinal(input, 0, dataSize);
|
||||
|
||||
Cipher nat = CipherFactory.getInstance("AES/CBC/PKCS5Padding");
|
||||
nat.init(Cipher.ENCRYPT_MODE, key, iv);
|
||||
byte[] outNative = nat.doFinal(input, 0, dataSize);
|
||||
|
||||
assertArrayEquals("Arrays differ on size: " + dataSize, outAndroid, outNative);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.crypto
|
||||
|
||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||
|
||||
import junit.framework.TestCase
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.Random
|
||||
|
||||
import javax.crypto.BadPaddingException
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.IllegalBlockSizeException
|
||||
import javax.crypto.NoSuchPaddingException
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
|
||||
class AESTest : TestCase() {
|
||||
|
||||
private val mRand = Random()
|
||||
|
||||
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, IllegalBlockSizeException::class, BadPaddingException::class, InvalidAlgorithmParameterException::class)
|
||||
fun testEncrypt() {
|
||||
// Test above below and at the blocksize
|
||||
testFinal(15)
|
||||
testFinal(16)
|
||||
testFinal(17)
|
||||
|
||||
// Test random larger sizes
|
||||
val size = mRand.nextInt(494) + 18
|
||||
testFinal(size)
|
||||
}
|
||||
|
||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, IllegalBlockSizeException::class, BadPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
||||
private fun testFinal(dataSize: Int) {
|
||||
|
||||
// Generate some input
|
||||
val input = ByteArray(dataSize)
|
||||
mRand.nextBytes(input)
|
||||
|
||||
// Generate key
|
||||
val keyArray = ByteArray(32)
|
||||
mRand.nextBytes(keyArray)
|
||||
val key = SecretKeySpec(keyArray, "AES")
|
||||
|
||||
// Generate IV
|
||||
val ivArray = ByteArray(16)
|
||||
mRand.nextBytes(ivArray)
|
||||
val iv = IvParameterSpec(ivArray)
|
||||
|
||||
val android = CipherFactory.getInstance("AES/CBC/PKCS5Padding", true)
|
||||
android.init(Cipher.ENCRYPT_MODE, key, iv)
|
||||
val outAndroid = android.doFinal(input, 0, dataSize)
|
||||
|
||||
val nat = CipherFactory.getInstance("AES/CBC/PKCS5Padding")
|
||||
nat.init(Cipher.ENCRYPT_MODE, key, iv)
|
||||
val outNative = nat.doFinal(input, 0, dataSize)
|
||||
|
||||
assertArrayEquals("Arrays differ on size: $dataSize", outAndroid, outNative)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.crypto;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Random;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.CipherOutputStream;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import com.kunzisoft.keepass.crypto.CipherFactory;
|
||||
import com.kunzisoft.keepass.crypto.engine.AesEngine;
|
||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine;
|
||||
import com.kunzisoft.keepass.stream.BetterCipherInputStream;
|
||||
import com.kunzisoft.keepass.stream.LEDataInputStream;
|
||||
|
||||
public class CipherTest extends TestCase {
|
||||
private Random rand = new Random();
|
||||
|
||||
public void testCipherFactory() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
|
||||
byte[] key = new byte[32];
|
||||
byte[] iv = new byte[16];
|
||||
|
||||
byte[] plaintext = new byte[1024];
|
||||
|
||||
rand.nextBytes(key);
|
||||
rand.nextBytes(iv);
|
||||
rand.nextBytes(plaintext);
|
||||
|
||||
CipherEngine aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID);
|
||||
Cipher encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv);
|
||||
Cipher decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv);
|
||||
|
||||
byte[] secrettext = encrypt.doFinal(plaintext);
|
||||
byte[] decrypttext = decrypt.doFinal(secrettext);
|
||||
|
||||
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext);
|
||||
}
|
||||
|
||||
public void testCipherStreams() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, IOException {
|
||||
final int MESSAGE_LENGTH = 1024;
|
||||
|
||||
byte[] key = new byte[32];
|
||||
byte[] iv = new byte[16];
|
||||
|
||||
byte[] plaintext = new byte[MESSAGE_LENGTH];
|
||||
|
||||
rand.nextBytes(key);
|
||||
rand.nextBytes(iv);
|
||||
rand.nextBytes(plaintext);
|
||||
|
||||
CipherEngine aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID);
|
||||
Cipher encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv);
|
||||
Cipher decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv);
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
CipherOutputStream cos = new CipherOutputStream(bos, encrypt);
|
||||
cos.write(plaintext);
|
||||
cos.close();
|
||||
|
||||
byte[] secrettext = bos.toByteArray();
|
||||
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(secrettext);
|
||||
BetterCipherInputStream cis = new BetterCipherInputStream(bis, decrypt);
|
||||
LEDataInputStream lis = new LEDataInputStream(cis);
|
||||
|
||||
byte[] decrypttext = lis.readBytes(MESSAGE_LENGTH);
|
||||
|
||||
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.crypto
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.Random
|
||||
|
||||
import javax.crypto.BadPaddingException
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.CipherOutputStream
|
||||
import javax.crypto.IllegalBlockSizeException
|
||||
import javax.crypto.NoSuchPaddingException
|
||||
|
||||
import junit.framework.TestCase
|
||||
|
||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||
import com.kunzisoft.keepass.crypto.engine.AesEngine
|
||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
||||
import com.kunzisoft.keepass.stream.BetterCipherInputStream
|
||||
import com.kunzisoft.keepass.stream.LEDataInputStream
|
||||
|
||||
class CipherTest : TestCase() {
|
||||
private val rand = Random()
|
||||
|
||||
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidAlgorithmParameterException::class, IllegalBlockSizeException::class, BadPaddingException::class)
|
||||
fun testCipherFactory() {
|
||||
val key = ByteArray(32)
|
||||
val iv = ByteArray(16)
|
||||
|
||||
val plaintext = ByteArray(1024)
|
||||
|
||||
rand.nextBytes(key)
|
||||
rand.nextBytes(iv)
|
||||
rand.nextBytes(plaintext)
|
||||
|
||||
val aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID)
|
||||
val encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv)
|
||||
val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv)
|
||||
|
||||
val secrettext = encrypt.doFinal(plaintext)
|
||||
val decrypttext = decrypt.doFinal(secrettext)
|
||||
|
||||
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext)
|
||||
}
|
||||
|
||||
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidAlgorithmParameterException::class, IllegalBlockSizeException::class, BadPaddingException::class, IOException::class)
|
||||
fun testCipherStreams() {
|
||||
val MESSAGE_LENGTH = 1024
|
||||
|
||||
val key = ByteArray(32)
|
||||
val iv = ByteArray(16)
|
||||
|
||||
val plaintext = ByteArray(MESSAGE_LENGTH)
|
||||
|
||||
rand.nextBytes(key)
|
||||
rand.nextBytes(iv)
|
||||
rand.nextBytes(plaintext)
|
||||
|
||||
val aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID)
|
||||
val encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv)
|
||||
val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv)
|
||||
|
||||
val bos = ByteArrayOutputStream()
|
||||
val cos = CipherOutputStream(bos, encrypt)
|
||||
cos.write(plaintext)
|
||||
cos.close()
|
||||
|
||||
val secrettext = bos.toByteArray()
|
||||
|
||||
val bis = ByteArrayInputStream(secrettext)
|
||||
val cis = BetterCipherInputStream(bis, decrypt)
|
||||
val lis = LEDataInputStream(cis)
|
||||
|
||||
val decrypttext = lis.readBytes(MESSAGE_LENGTH)
|
||||
|
||||
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext)
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.crypto;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Random;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import com.kunzisoft.keepass.crypto.finalkey.AndroidFinalKey;
|
||||
import com.kunzisoft.keepass.crypto.finalkey.NativeFinalKey;
|
||||
|
||||
public class FinalKeyTest extends TestCase {
|
||||
private Random mRand;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
mRand = new Random();
|
||||
}
|
||||
|
||||
public void testNativeAndroid() throws IOException {
|
||||
// Test both an old and an even number to test my flip variable
|
||||
testNativeFinalKey(5);
|
||||
testNativeFinalKey(6);
|
||||
}
|
||||
|
||||
private void testNativeFinalKey(int rounds) throws IOException {
|
||||
byte[] seed = new byte[32];
|
||||
byte[] key = new byte[32];
|
||||
byte[] nativeKey;
|
||||
byte[] androidKey;
|
||||
|
||||
mRand.nextBytes(seed);
|
||||
mRand.nextBytes(key);
|
||||
|
||||
AndroidFinalKey aKey = new AndroidFinalKey();
|
||||
androidKey = aKey.transformMasterKey(seed, key, rounds);
|
||||
|
||||
NativeFinalKey nKey = new NativeFinalKey();
|
||||
nativeKey = nKey.transformMasterKey(seed, key, rounds);
|
||||
|
||||
assertArrayEquals("Does not match", androidKey, nativeKey);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.crypto
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
|
||||
import java.io.IOException
|
||||
import java.util.Random
|
||||
|
||||
import junit.framework.TestCase
|
||||
|
||||
import com.kunzisoft.keepass.crypto.finalkey.AndroidFinalKey
|
||||
import com.kunzisoft.keepass.crypto.finalkey.NativeFinalKey
|
||||
|
||||
class FinalKeyTest : TestCase() {
|
||||
private var mRand: Random? = null
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
|
||||
mRand = Random()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun testNativeAndroid() {
|
||||
// Test both an old and an even number to test my flip variable
|
||||
testNativeFinalKey(5)
|
||||
testNativeFinalKey(6)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun testNativeFinalKey(rounds: Int) {
|
||||
val seed = ByteArray(32)
|
||||
val key = ByteArray(32)
|
||||
val nativeKey: ByteArray
|
||||
val androidKey: ByteArray
|
||||
|
||||
mRand!!.nextBytes(seed)
|
||||
mRand!!.nextBytes(key)
|
||||
|
||||
val aKey = AndroidFinalKey()
|
||||
androidKey = aKey.transformMasterKey(seed, key, rounds.toLong())
|
||||
|
||||
val nKey = NativeFinalKey()
|
||||
nativeKey = nKey.transformMasterKey(seed, key, rounds.toLong())
|
||||
|
||||
assertArrayEquals("Does not match", androidKey, nativeKey)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.test.AndroidTestCase;
|
||||
|
||||
import com.kunzisoft.keepass.database.Database;
|
||||
import com.kunzisoft.keepass.database.PwDatabase;
|
||||
import com.kunzisoft.keepass.database.PwDatabaseV3;
|
||||
import com.kunzisoft.keepass.database.PwEntry;
|
||||
import com.kunzisoft.keepass.database.PwEntryV3;
|
||||
import com.kunzisoft.keepass.database.PwGroup;
|
||||
import com.kunzisoft.keepass.database.action.node.DeleteGroupRunnable;
|
||||
import com.kunzisoft.keepass.database.search.SearchDbHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class DeleteEntry extends AndroidTestCase {
|
||||
private static final String GROUP1_NAME = "Group1";
|
||||
private static final String ENTRY1_NAME = "Test1";
|
||||
private static final String ENTRY2_NAME = "Test2";
|
||||
private static final String KEYFILE = "";
|
||||
private static final String PASSWORD = "12345";
|
||||
private static final String ASSET = "delete.kdb";
|
||||
private static final String FILENAME = "/sdcard/delete.kdb";
|
||||
|
||||
public void testDelete() {
|
||||
|
||||
Database db;
|
||||
|
||||
Context ctx = getContext();
|
||||
|
||||
try {
|
||||
db = TestData.GetDb(ctx, ASSET, PASSWORD, KEYFILE, FILENAME);
|
||||
} catch (Exception e) {
|
||||
assertTrue("Failed to open database: " + e.getMessage(), false);
|
||||
return;
|
||||
}
|
||||
|
||||
PwDatabaseV3 pm = (PwDatabaseV3) db.getPwDatabase();
|
||||
PwGroup group1 = getGroup(pm, GROUP1_NAME);
|
||||
assertNotNull("Could not find group1", group1);
|
||||
|
||||
// Delete the group
|
||||
DeleteGroupRunnable task = new DeleteGroupRunnable(null, db, group1, null, true);
|
||||
task.run();
|
||||
|
||||
// Verify the entries were deleted
|
||||
PwEntry entry1 = getEntry(pm, ENTRY1_NAME);
|
||||
assertNull("Entry 1 was not removed", entry1);
|
||||
|
||||
PwEntry entry2 = getEntry(pm, ENTRY2_NAME);
|
||||
assertNull("Entry 2 was not removed", entry2);
|
||||
|
||||
// Verify the entries were removed from the search index
|
||||
SearchDbHelper dbHelp = new SearchDbHelper(ctx);
|
||||
PwGroup results1 = dbHelp.search(db.getPwDatabase(), ENTRY1_NAME, 100);
|
||||
PwGroup results2 = dbHelp.search(db.getPwDatabase(), ENTRY2_NAME, 100);
|
||||
|
||||
assertEquals("Entry1 was not removed from the search results", 0, results1.numbersOfChildEntries());
|
||||
assertEquals("Entry2 was not removed from the search results", 0, results2.numbersOfChildEntries());
|
||||
|
||||
// Verify the group was deleted
|
||||
group1 = getGroup(pm, GROUP1_NAME);
|
||||
assertNull("Group 1 was not removed.", group1);
|
||||
|
||||
}
|
||||
|
||||
private PwEntryV3 getEntry(PwDatabaseV3 pm, String name) {
|
||||
List<PwEntryV3> entries = pm.getEntries();
|
||||
for ( int i = 0; i < entries.size(); i++ ) {
|
||||
PwEntryV3 entry = entries.get(i);
|
||||
if ( entry.getTitle().equals(name) ) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
private PwGroup getGroup(PwDatabase pm, String name) {
|
||||
List<PwGroup> groups = pm.getGroups();
|
||||
for ( int i = 0; i < groups.size(); i++ ) {
|
||||
PwGroup group = groups.get(i);
|
||||
if ( group.getName().equals(name) ) {
|
||||
return group;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.database;
|
||||
|
||||
import com.kunzisoft.keepass.database.PwDatabaseV4;
|
||||
import com.kunzisoft.keepass.database.PwEntryV4;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
public class EntryV4 extends TestCase {
|
||||
|
||||
public void testBackup() {
|
||||
PwDatabaseV4 db = new PwDatabaseV4();
|
||||
|
||||
db.setHistoryMaxItems(2);
|
||||
|
||||
PwEntryV4 entry = new PwEntryV4();
|
||||
entry.startToManageFieldReferences(db);
|
||||
entry.setTitle("Title1");
|
||||
entry.setUsername("User1");
|
||||
entry.createBackup(db);
|
||||
|
||||
entry.setTitle("Title2");
|
||||
entry.setUsername("User2");
|
||||
entry.createBackup(db);
|
||||
|
||||
entry.setTitle("Title3");
|
||||
entry.setUsername("User3");
|
||||
entry.createBackup(db);
|
||||
|
||||
PwEntryV4 backup = entry.getHistory().get(0);
|
||||
entry.stopToManageFieldReferences();
|
||||
assertEquals("Title2", backup.getTitle());
|
||||
assertEquals("User2", backup.getUsername());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.test.AndroidTestCase;
|
||||
|
||||
import com.kunzisoft.keepass.database.load.ImporterV3;
|
||||
import com.kunzisoft.keepass.tests.TestUtil;
|
||||
import com.kunzisoft.keepass.utils.UriUtil;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.File;
|
||||
|
||||
public class Kdb3 extends AndroidTestCase {
|
||||
|
||||
private void testKeyfile(String dbAsset, String keyAsset, String password) throws Exception {
|
||||
Context ctx = getContext();
|
||||
|
||||
File sdcard = Environment.getExternalStorageDirectory();
|
||||
String keyPath = sdcard.getAbsolutePath() + "/key";
|
||||
|
||||
TestUtil.extractKey(ctx, keyAsset, keyPath);
|
||||
|
||||
AssetManager am = ctx.getAssets();
|
||||
InputStream is = am.open(dbAsset, AssetManager.ACCESS_STREAMING);
|
||||
|
||||
ImporterV3 importer = new ImporterV3();
|
||||
importer.openDatabase(is, password, TestUtil.getKeyFileInputStream(ctx, keyPath));
|
||||
|
||||
is.close();
|
||||
}
|
||||
|
||||
public void testXMLKeyFile() throws Exception {
|
||||
testKeyfile("kdb_with_xml_keyfile.kdb", "keyfile.key", "12345");
|
||||
}
|
||||
|
||||
public void testBinary64KeyFile() throws Exception {
|
||||
testKeyfile("binary-key.kdb", "binary.key", "12345");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.database;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.test.AndroidTestCase;
|
||||
|
||||
import com.kunzisoft.keepass.database.PwDatabaseV3;
|
||||
import com.kunzisoft.keepass.database.PwEncryptionAlgorithm;
|
||||
import com.kunzisoft.keepass.database.load.ImporterV3;
|
||||
|
||||
public class Kdb3Twofish extends AndroidTestCase {
|
||||
public void testReadTwofish() throws Exception {
|
||||
Context ctx = getContext();
|
||||
|
||||
AssetManager am = ctx.getAssets();
|
||||
InputStream is = am.open("twofish.kdb", AssetManager.ACCESS_STREAMING);
|
||||
|
||||
ImporterV3 importer = new ImporterV3();
|
||||
|
||||
PwDatabaseV3 db = importer.openDatabase(is, "12345", null);
|
||||
|
||||
assertTrue(db.getEncryptionAlgorithm() == PwEncryptionAlgorithm.Twofish);
|
||||
|
||||
is.close();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.database;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.test.AndroidTestCase;
|
||||
|
||||
import com.kunzisoft.keepass.database.PwDatabaseV4;
|
||||
import com.kunzisoft.keepass.database.exception.InvalidDBException;
|
||||
import com.kunzisoft.keepass.database.exception.PwDbOutputException;
|
||||
import com.kunzisoft.keepass.database.load.Importer;
|
||||
import com.kunzisoft.keepass.database.load.ImporterFactory;
|
||||
import com.kunzisoft.keepass.database.load.ImporterV4;
|
||||
import com.kunzisoft.keepass.database.save.PwDbOutput;
|
||||
import com.kunzisoft.keepass.database.save.PwDbV4Output;
|
||||
import com.kunzisoft.keepass.stream.CopyInputStream;
|
||||
import com.kunzisoft.keepass.tests.TestUtil;
|
||||
|
||||
public class Kdb4 extends AndroidTestCase {
|
||||
|
||||
public void testDetection() throws IOException, InvalidDBException {
|
||||
Context ctx = getContext();
|
||||
|
||||
AssetManager am = ctx.getAssets();
|
||||
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
|
||||
|
||||
Importer importer = ImporterFactory.createImporter(is);
|
||||
|
||||
assertTrue(importer instanceof ImporterV4);
|
||||
is.close();
|
||||
|
||||
}
|
||||
|
||||
public void testParsing() throws IOException, InvalidDBException {
|
||||
Context ctx = getContext();
|
||||
|
||||
AssetManager am = ctx.getAssets();
|
||||
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
|
||||
|
||||
ImporterV4 importer = new ImporterV4();
|
||||
importer.openDatabase(is, "12345", null);
|
||||
|
||||
is.close();
|
||||
|
||||
|
||||
}
|
||||
|
||||
public void testSavingKDBXV3() throws IOException, InvalidDBException, PwDbOutputException {
|
||||
testSaving("test.kdbx", "12345", "test-out.kdbx");
|
||||
}
|
||||
|
||||
public void testSavingKDBXV4() throws IOException, InvalidDBException, PwDbOutputException {
|
||||
testSaving("test-kdbxv4.kdbx", "1", "test-kdbxv4-out.kdbx");
|
||||
}
|
||||
|
||||
private void testSaving(String inputFile, String password, String outputFile) throws IOException, InvalidDBException, PwDbOutputException {
|
||||
Context ctx = getContext();
|
||||
|
||||
AssetManager am = ctx.getAssets();
|
||||
InputStream is = am.open(inputFile, AssetManager.ACCESS_STREAMING);
|
||||
|
||||
ImporterV4 importer = new ImporterV4();
|
||||
PwDatabaseV4 db = importer.openDatabase(is, password, null);
|
||||
is.close();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
|
||||
PwDbV4Output output = (PwDbV4Output) PwDbOutput.getInstance(db, bos);
|
||||
output.output();
|
||||
|
||||
byte[] data = bos.toByteArray();
|
||||
|
||||
FileOutputStream fos = new FileOutputStream(TestUtil.getSdPath(outputFile), false);
|
||||
|
||||
InputStream bis = new ByteArrayInputStream(data);
|
||||
bis = new CopyInputStream(bis, fos);
|
||||
importer = new ImporterV4();
|
||||
db = importer.openDatabase(bis, password, null);
|
||||
bis.close();
|
||||
|
||||
fos.close();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
TestUtil.extractKey(getContext(), "keyfile.key", TestUtil.getSdPath("key"));
|
||||
TestUtil.extractKey(getContext(), "binary.key", TestUtil.getSdPath("key-binary"));
|
||||
}
|
||||
|
||||
public void testComposite() throws IOException, InvalidDBException {
|
||||
Context ctx = getContext();
|
||||
|
||||
AssetManager am = ctx.getAssets();
|
||||
InputStream is = am.open("keyfile.kdbx", AssetManager.ACCESS_STREAMING);
|
||||
|
||||
ImporterV4 importer = new ImporterV4();
|
||||
importer.openDatabase(is, "12345", TestUtil.getKeyFileInputStream(ctx, TestUtil.getSdPath("key")));
|
||||
|
||||
is.close();
|
||||
|
||||
}
|
||||
|
||||
public void testCompositeBinary() throws IOException, InvalidDBException {
|
||||
Context ctx = getContext();
|
||||
|
||||
AssetManager am = ctx.getAssets();
|
||||
InputStream is = am.open("keyfile-binary.kdbx", AssetManager.ACCESS_STREAMING);
|
||||
|
||||
ImporterV4 importer = new ImporterV4();
|
||||
importer.openDatabase(is, "12345", TestUtil.getKeyFileInputStream(ctx,TestUtil.getSdPath("key-binary")));
|
||||
|
||||
is.close();
|
||||
|
||||
}
|
||||
|
||||
public void testKeyfile() throws IOException, InvalidDBException {
|
||||
Context ctx = getContext();
|
||||
|
||||
AssetManager am = ctx.getAssets();
|
||||
InputStream is = am.open("key-only.kdbx", AssetManager.ACCESS_STREAMING);
|
||||
|
||||
ImporterV4 importer = new ImporterV4();
|
||||
importer.openDatabase(is, "", TestUtil.getKeyFileInputStream(ctx, TestUtil.getSdPath("key")));
|
||||
|
||||
is.close();
|
||||
|
||||
|
||||
}
|
||||
|
||||
public void testNoGzip() throws IOException, InvalidDBException {
|
||||
Context ctx = getContext();
|
||||
|
||||
AssetManager am = ctx.getAssets();
|
||||
InputStream is = am.open("no-encrypt.kdbx", AssetManager.ACCESS_STREAMING);
|
||||
|
||||
ImporterV4 importer = new ImporterV4();
|
||||
importer.openDatabase(is, "12345", null);
|
||||
|
||||
is.close();
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.test.AndroidTestCase;
|
||||
|
||||
import com.kunzisoft.keepass.crypto.engine.AesEngine;
|
||||
import com.kunzisoft.keepass.database.PwDatabaseV4;
|
||||
import com.kunzisoft.keepass.database.load.ImporterV4;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public class Kdb4Header extends AndroidTestCase {
|
||||
public void testReadHeader() throws Exception {
|
||||
Context ctx = getContext();
|
||||
|
||||
AssetManager am = ctx.getAssets();
|
||||
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
|
||||
|
||||
ImporterV4 importer = new ImporterV4();
|
||||
|
||||
PwDatabaseV4 db = importer.openDatabase(is, "12345", null);
|
||||
|
||||
assertEquals(6000, db.getNumberKeyEncryptionRounds());
|
||||
|
||||
assertTrue(db.getDataCipher().equals(AesEngine.CIPHER_UUID));
|
||||
|
||||
is.close();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.database;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.UUID;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.test.AndroidTestCase;
|
||||
import biz.source_code.base64Coder.Base64Coder;
|
||||
|
||||
import com.kunzisoft.keepass.database.PwDatabase;
|
||||
import com.kunzisoft.keepass.database.PwDatabaseV4;
|
||||
import com.kunzisoft.keepass.database.PwEntryV4;
|
||||
import com.kunzisoft.keepass.database.load.ImporterV4;
|
||||
import com.kunzisoft.keepass.utils.SprEngineV4;
|
||||
import com.kunzisoft.keepass.utils.Types;
|
||||
|
||||
public class SprEngineTest extends AndroidTestCase {
|
||||
private PwDatabaseV4 db;
|
||||
private SprEngineV4 spr;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
Context ctx = getContext();
|
||||
|
||||
AssetManager am = ctx.getAssets();
|
||||
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
|
||||
|
||||
ImporterV4 importer = new ImporterV4();
|
||||
db = importer.openDatabase(is, "12345", null);
|
||||
|
||||
is.close();
|
||||
|
||||
spr = new SprEngineV4();
|
||||
}
|
||||
|
||||
private final String REF = "{REF:P@I:2B1D56590D961F48A8CE8C392CE6CD35}";
|
||||
private final String ENCODE_UUID = "IN7RkON49Ui1UZ2ddqmLcw==";
|
||||
private final String RESULT = "Password";
|
||||
public void testRefReplace() {
|
||||
UUID entryUUID = decodeUUID(ENCODE_UUID);
|
||||
|
||||
PwEntryV4 entry = (PwEntryV4) db.getEntryByUUIDId(entryUUID);
|
||||
|
||||
|
||||
assertEquals(RESULT, spr.compile(REF, entry, db));
|
||||
|
||||
}
|
||||
|
||||
private UUID decodeUUID(String encoded) {
|
||||
if (encoded == null || encoded.length() == 0 ) {
|
||||
return PwDatabase.UUID_ZERO;
|
||||
}
|
||||
|
||||
byte[] buf = Base64Coder.decode(encoded);
|
||||
return Types.bytestoUUID(buf);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.database;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.kunzisoft.keepass.database.Database;
|
||||
import com.kunzisoft.keepass.database.PwDatabaseV3Debug;
|
||||
import com.kunzisoft.keepass.database.load.Importer;
|
||||
import com.kunzisoft.keepass.tests.TestUtil;
|
||||
|
||||
public class TestData {
|
||||
private static final String TEST1_KEYFILE = "";
|
||||
private static final String TEST1_KDB = "test1.kdb";
|
||||
private static final String TEST1_PASSWORD = "12345";
|
||||
|
||||
private static Database mDb1;
|
||||
|
||||
|
||||
public static Database GetDb1(Context ctx) throws Exception {
|
||||
return GetDb1(ctx, false);
|
||||
}
|
||||
|
||||
public static Database GetDb1(Context ctx, boolean forceReload) throws Exception {
|
||||
if ( mDb1 == null || forceReload ) {
|
||||
mDb1 = GetDb(ctx, TEST1_KDB, TEST1_PASSWORD, TEST1_KEYFILE, "/sdcard/test1.kdb");
|
||||
}
|
||||
|
||||
return mDb1;
|
||||
}
|
||||
|
||||
public static Database GetDb(Context ctx, String asset, String password, String keyfile, String filename) throws Exception {
|
||||
AssetManager am = ctx.getAssets();
|
||||
InputStream is = am.open(asset, AssetManager.ACCESS_STREAMING);
|
||||
|
||||
Database Db = new Database();
|
||||
|
||||
InputStream keyIs = TestUtil.getKeyFileInputStream(ctx, keyfile);
|
||||
|
||||
Db.loadData(ctx, is, password, keyIs, Importer.DEBUG);
|
||||
Uri.Builder b = new Uri.Builder();
|
||||
|
||||
Db.setUri(b.scheme("file").path(filename).build());
|
||||
|
||||
return Db;
|
||||
|
||||
}
|
||||
|
||||
public static PwDatabaseV3Debug GetTest1(Context ctx) throws Exception {
|
||||
if ( mDb1 == null ) {
|
||||
GetDb1(ctx);
|
||||
}
|
||||
|
||||
return (PwDatabaseV3Debug) mDb1.getPwDatabase();
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.output;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.DigestOutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import android.content.res.AssetManager;
|
||||
import android.test.AndroidTestCase;
|
||||
|
||||
import com.kunzisoft.keepass.database.PwDatabaseV3Debug;
|
||||
import com.kunzisoft.keepass.database.PwDbHeader;
|
||||
import com.kunzisoft.keepass.database.PwDbHeaderV3;
|
||||
import com.kunzisoft.keepass.database.exception.PwDbOutputException;
|
||||
import com.kunzisoft.keepass.database.save.PwDbHeaderOutputV3;
|
||||
import com.kunzisoft.keepass.database.save.PwDbV3Output;
|
||||
import com.kunzisoft.keepass.database.save.PwDbV3OutputDebug;
|
||||
import com.kunzisoft.keepass.stream.NullOutputStream;
|
||||
import com.kunzisoft.keepass.tests.TestUtil;
|
||||
import com.kunzisoft.keepass.tests.database.TestData;
|
||||
|
||||
public class PwManagerOutputTest extends AndroidTestCase {
|
||||
PwDatabaseV3Debug mPM;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
mPM = TestData.GetTest1(getContext());
|
||||
}
|
||||
|
||||
public void testPlainContent() throws IOException, PwDbOutputException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
|
||||
PwDbV3Output pos = new PwDbV3OutputDebug(mPM, bos, true);
|
||||
pos.outputPlanGroupAndEntries(bos);
|
||||
|
||||
assertTrue("No output", bos.toByteArray().length > 0);
|
||||
assertArrayEquals("Group and entry output doesn't match.", mPM.getPostHeader(), bos.toByteArray());
|
||||
|
||||
}
|
||||
|
||||
public void testChecksum() throws NoSuchAlgorithmException, IOException, PwDbOutputException {
|
||||
//FileOutputStream fos = new FileOutputStream("/dev/null");
|
||||
NullOutputStream nos = new NullOutputStream();
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
|
||||
DigestOutputStream dos = new DigestOutputStream(nos, md);
|
||||
|
||||
PwDbV3Output pos = new PwDbV3OutputDebug(mPM, dos, true);
|
||||
pos.outputPlanGroupAndEntries(dos);
|
||||
dos.close();
|
||||
|
||||
byte[] digest = md.digest();
|
||||
assertTrue("No output", digest.length > 0);
|
||||
assertArrayEquals("Hash of groups and entries failed.", mPM.getDbHeader().contentsHash, digest);
|
||||
}
|
||||
|
||||
private void assertHeadersEquals(PwDbHeaderV3 expected, PwDbHeaderV3 actual) {
|
||||
assertEquals("Flags unequal", expected.flags, actual.flags);
|
||||
assertEquals("Entries unequal", expected.numEntries, actual.numEntries);
|
||||
assertEquals("Groups unequal", expected.numGroups, actual.numGroups);
|
||||
assertEquals("Key Rounds unequal", expected.numKeyEncRounds, actual.numKeyEncRounds);
|
||||
assertEquals("Signature1 unequal", expected.signature1, actual.signature1);
|
||||
assertEquals("Signature2 unequal", expected.signature2, actual.signature2);
|
||||
assertTrue("Version incompatible", PwDbHeaderV3.compatibleHeaders(expected.version, actual.version));
|
||||
assertArrayEquals("Hash unequal", expected.contentsHash, actual.contentsHash);
|
||||
assertArrayEquals("IV unequal", expected.encryptionIV, actual.encryptionIV);
|
||||
assertArrayEquals("Seed unequal", expected.masterSeed, actual.masterSeed);
|
||||
assertArrayEquals("Seed2 unequal", expected.transformSeed, actual.transformSeed);
|
||||
}
|
||||
|
||||
public void testHeader() throws PwDbOutputException, IOException {
|
||||
ByteArrayOutputStream bActual = new ByteArrayOutputStream();
|
||||
PwDbV3Output pActual = new PwDbV3OutputDebug(mPM, bActual, true);
|
||||
PwDbHeaderV3 header = pActual.outputHeader(bActual);
|
||||
|
||||
ByteArrayOutputStream bExpected = new ByteArrayOutputStream();
|
||||
PwDbHeaderOutputV3 outExpected = new PwDbHeaderOutputV3(mPM.getDbHeader(), bExpected);
|
||||
outExpected.output();
|
||||
|
||||
assertHeadersEquals(mPM.getDbHeader(), header);
|
||||
assertTrue("No output", bActual.toByteArray().length > 0);
|
||||
assertArrayEquals("Header does not match.", bExpected.toByteArray(), bActual.toByteArray());
|
||||
}
|
||||
|
||||
public void testFinalKey() throws PwDbOutputException {
|
||||
ByteArrayOutputStream bActual = new ByteArrayOutputStream();
|
||||
PwDbV3Output pActual = new PwDbV3OutputDebug(mPM, bActual, true);
|
||||
PwDbHeader hActual = pActual.outputHeader(bActual);
|
||||
byte[] finalKey = pActual.getFinalKey(hActual);
|
||||
|
||||
assertArrayEquals("Keys mismatched", mPM.getFinalKey(), finalKey);
|
||||
|
||||
}
|
||||
|
||||
public void testFullWrite() throws IOException, PwDbOutputException {
|
||||
AssetManager am = getContext().getAssets();
|
||||
InputStream is = am.open("test1.kdb");
|
||||
|
||||
// Pull file into byte array (for streaming fun)
|
||||
ByteArrayOutputStream bExpected = new ByteArrayOutputStream();
|
||||
while (true) {
|
||||
int data = is.read();
|
||||
if ( data == -1 ) {
|
||||
break;
|
||||
}
|
||||
bExpected.write(data);
|
||||
}
|
||||
|
||||
ByteArrayOutputStream bActual = new ByteArrayOutputStream();
|
||||
PwDbV3Output pActual = new PwDbV3OutputDebug(mPM, bActual, true);
|
||||
pActual.output();
|
||||
//pActual.close();
|
||||
|
||||
FileOutputStream fos = new FileOutputStream(TestUtil.getSdPath("test1_out.kdb"));
|
||||
fos.write(bActual.toByteArray());
|
||||
fos.close();
|
||||
assertArrayEquals("Databases do not match.", bExpected.toByteArray(), bActual.toByteArray());
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.search;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.test.AndroidTestCase;
|
||||
|
||||
import com.kunzisoft.keepass.database.Database;
|
||||
import com.kunzisoft.keepass.database.PwGroup;
|
||||
import com.kunzisoft.keepass.tests.database.TestData;
|
||||
|
||||
public class SearchTest extends AndroidTestCase {
|
||||
|
||||
private Database mDb;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
mDb = TestData.GetDb1(getContext(), true);
|
||||
}
|
||||
|
||||
public void testSearch() {
|
||||
PwGroup results = mDb.search("Amazon");
|
||||
assertTrue("Search result not found.", results.numbersOfChildEntries() > 0);
|
||||
|
||||
}
|
||||
|
||||
public void testBackupIncluded() {
|
||||
updateOmitSetting(false);
|
||||
PwGroup results = mDb.search("BackupOnly");
|
||||
|
||||
assertTrue("Search result not found.", results.numbersOfChildEntries() > 0);
|
||||
}
|
||||
|
||||
public void testBackupExcluded() {
|
||||
updateOmitSetting(true);
|
||||
PwGroup results = mDb.search("BackupOnly");
|
||||
|
||||
assertFalse("Search result found, but should not have been.", results.numbersOfChildEntries() > 0);
|
||||
}
|
||||
|
||||
private void updateOmitSetting(boolean setting) {
|
||||
Context ctx = getContext();
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
|
||||
editor.putBoolean("settings_omitbackup_key", setting);
|
||||
editor.commit();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.stream;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Random;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import com.kunzisoft.keepass.stream.HashedBlockInputStream;
|
||||
import com.kunzisoft.keepass.stream.HashedBlockOutputStream;
|
||||
|
||||
public class HashedBlock extends TestCase {
|
||||
|
||||
private static Random rand = new Random();
|
||||
|
||||
public void testBlockAligned() throws IOException {
|
||||
testSize(1024, 1024);
|
||||
}
|
||||
|
||||
public void testOffset() throws IOException {
|
||||
testSize(1500, 1024);
|
||||
}
|
||||
|
||||
private void testSize(int blockSize, int bufferSize) throws IOException {
|
||||
byte[] orig = new byte[blockSize];
|
||||
|
||||
rand.nextBytes(orig);
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
HashedBlockOutputStream output = new HashedBlockOutputStream(bos, bufferSize);
|
||||
output.write(orig);
|
||||
output.close();
|
||||
|
||||
byte[] encoded = bos.toByteArray();
|
||||
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(encoded);
|
||||
HashedBlockInputStream input = new HashedBlockInputStream(bis);
|
||||
|
||||
ByteArrayOutputStream decoded = new ByteArrayOutputStream();
|
||||
while ( true ) {
|
||||
byte[] buf = new byte[1024];
|
||||
int read = input.read(buf);
|
||||
if ( read == -1 ) {
|
||||
break;
|
||||
}
|
||||
|
||||
decoded.write(buf, 0, read);
|
||||
}
|
||||
|
||||
byte[] out = decoded.toByteArray();
|
||||
|
||||
assertArrayEquals(orig, out);
|
||||
|
||||
}
|
||||
|
||||
public void testGZIPStream() throws IOException {
|
||||
final int testLength = 32000;
|
||||
|
||||
byte[] orig = new byte[testLength];
|
||||
rand.nextBytes(orig);
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
HashedBlockOutputStream hos = new HashedBlockOutputStream(bos);
|
||||
GZIPOutputStream zos = new GZIPOutputStream(hos);
|
||||
|
||||
zos.write(orig);
|
||||
zos.close();
|
||||
|
||||
byte[] compressed = bos.toByteArray();
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(compressed);
|
||||
HashedBlockInputStream his = new HashedBlockInputStream(bis);
|
||||
GZIPInputStream zis = new GZIPInputStream(his);
|
||||
|
||||
byte[] uncompressed = new byte[testLength];
|
||||
|
||||
int read = 0;
|
||||
while (read != -1 && testLength - read > 0) {
|
||||
read += zis.read(uncompressed, read, testLength - read);
|
||||
|
||||
}
|
||||
|
||||
assertArrayEquals("Output not equal to input", orig, uncompressed);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.stream
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.Random
|
||||
import java.util.zip.GZIPInputStream
|
||||
import java.util.zip.GZIPOutputStream
|
||||
|
||||
import junit.framework.TestCase
|
||||
|
||||
import com.kunzisoft.keepass.stream.HashedBlockInputStream
|
||||
import com.kunzisoft.keepass.stream.HashedBlockOutputStream
|
||||
|
||||
class HashedBlock : TestCase() {
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun testBlockAligned() {
|
||||
testSize(1024, 1024)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun testOffset() {
|
||||
testSize(1500, 1024)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun testSize(blockSize: Int, bufferSize: Int) {
|
||||
val orig = ByteArray(blockSize)
|
||||
|
||||
rand.nextBytes(orig)
|
||||
|
||||
val bos = ByteArrayOutputStream()
|
||||
val output = HashedBlockOutputStream(bos, bufferSize)
|
||||
output.write(orig)
|
||||
output.close()
|
||||
|
||||
val encoded = bos.toByteArray()
|
||||
|
||||
val bis = ByteArrayInputStream(encoded)
|
||||
val input = HashedBlockInputStream(bis)
|
||||
|
||||
val decoded = ByteArrayOutputStream()
|
||||
while (true) {
|
||||
val buf = ByteArray(1024)
|
||||
val read = input.read(buf)
|
||||
if (read == -1) {
|
||||
break
|
||||
}
|
||||
|
||||
decoded.write(buf, 0, read)
|
||||
}
|
||||
|
||||
val out = decoded.toByteArray()
|
||||
|
||||
assertArrayEquals(orig, out)
|
||||
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun testGZIPStream() {
|
||||
val testLength = 32000
|
||||
|
||||
val orig = ByteArray(testLength)
|
||||
rand.nextBytes(orig)
|
||||
|
||||
val bos = ByteArrayOutputStream()
|
||||
val hos = HashedBlockOutputStream(bos)
|
||||
val zos = GZIPOutputStream(hos)
|
||||
|
||||
zos.write(orig)
|
||||
zos.close()
|
||||
|
||||
val compressed = bos.toByteArray()
|
||||
val bis = ByteArrayInputStream(compressed)
|
||||
val his = HashedBlockInputStream(bis)
|
||||
val zis = GZIPInputStream(his)
|
||||
|
||||
val uncompressed = ByteArray(testLength)
|
||||
|
||||
var read = 0
|
||||
while (read != -1 && testLength - read > 0) {
|
||||
read += zis.read(uncompressed, read, testLength - read)
|
||||
|
||||
}
|
||||
|
||||
assertArrayEquals("Output not equal to input", orig, uncompressed)
|
||||
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val rand = Random()
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.utils;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import com.kunzisoft.keepass.utils.StrUtil;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
public class StrUtilTest extends TestCase {
|
||||
private final String text = "AbCdEfGhIj";
|
||||
private final String search = "BcDe";
|
||||
private final String badSearch = "Ed";
|
||||
|
||||
public void testIndexOfIgnoreCase1() {
|
||||
assertEquals(1, StrUtil.indexOfIgnoreCase(text, search, Locale.ENGLISH));
|
||||
}
|
||||
|
||||
public void testIndexOfIgnoreCase2() {
|
||||
assertEquals(-1, StrUtil.indexOfIgnoreCase(text, search, Locale.ENGLISH), 2);
|
||||
}
|
||||
|
||||
public void testIndexOfIgnoreCase3() {
|
||||
assertEquals(-1, StrUtil.indexOfIgnoreCase(text, badSearch, Locale.ENGLISH));
|
||||
}
|
||||
|
||||
private final String repText = "AbCtestingaBc";
|
||||
private final String repSearch = "ABc";
|
||||
private final String repSearchBad = "CCCCCC";
|
||||
private final String repNew = "12345";
|
||||
private final String repResult = "12345testing12345";
|
||||
public void testReplaceAllIgnoresCase1() {
|
||||
assertEquals(repResult, StrUtil.replaceAllIgnoresCase(repText, repSearch, repNew, Locale.ENGLISH));
|
||||
}
|
||||
|
||||
public void testReplaceAllIgnoresCase2() {
|
||||
assertEquals(repText, StrUtil.replaceAllIgnoresCase(repText, repSearchBad, repNew, Locale.ENGLISH));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.tests.utils
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
import com.kunzisoft.keepass.utils.StringUtil
|
||||
|
||||
import junit.framework.TestCase
|
||||
|
||||
class StringUtilTest : TestCase() {
|
||||
private val text = "AbCdEfGhIj"
|
||||
private val search = "BcDe"
|
||||
private val badSearch = "Ed"
|
||||
|
||||
private val repText = "AbCtestingaBc"
|
||||
private val repSearch = "ABc"
|
||||
private val repSearchBad = "CCCCCC"
|
||||
private val repNew = "12345"
|
||||
private val repResult = "12345testing12345"
|
||||
|
||||
fun testIndexOfIgnoreCase1() {
|
||||
assertEquals(1, StringUtil.indexOfIgnoreCase(text, search, Locale.ENGLISH))
|
||||
}
|
||||
|
||||
fun testIndexOfIgnoreCase2() {
|
||||
assertEquals(-1f, StringUtil.indexOfIgnoreCase(text, search, Locale.ENGLISH).toFloat(), 2f)
|
||||
}
|
||||
|
||||
fun testIndexOfIgnoreCase3() {
|
||||
assertEquals(-1, StringUtil.indexOfIgnoreCase(text, badSearch, Locale.ENGLISH))
|
||||
}
|
||||
|
||||
fun testReplaceAllIgnoresCase1() {
|
||||
assertEquals(repResult, StringUtil.replaceAllIgnoresCase(repText, repSearch, repNew, Locale.ENGLISH))
|
||||
}
|
||||
|
||||
fun testReplaceAllIgnoresCase2() {
|
||||
assertEquals(repText, StringUtil.replaceAllIgnoresCase(repText, repSearchBad, repNew, Locale.ENGLISH))
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.kunzisoft.keepass"
|
||||
android:installLocation="auto">
|
||||
<supports-screens
|
||||
@@ -8,7 +7,8 @@
|
||||
android:normalScreens="true"
|
||||
android:largeScreens="true"
|
||||
android:anyDensity="true" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
|
||||
@@ -20,19 +20,18 @@
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup"
|
||||
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
|
||||
android:theme="@style/KeepassDXStyle.Night"
|
||||
tools:replace="android:theme">
|
||||
android:theme="@style/KeepassDXStyle.Night">
|
||||
<!-- TODO backup API Key -->
|
||||
<meta-data
|
||||
android:name="com.google.android.backup.api_key"
|
||||
android:value="" />
|
||||
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.fileselect.FileSelectActivity"
|
||||
android:name="com.kunzisoft.keepass.activities.FileDatabaseSelectActivity"
|
||||
android:theme="@style/KeepassDXStyle.SplashScreen"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTop"
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:windowSoftInputMode="stateHidden" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
@@ -40,12 +39,8 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/menu_about" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.password.PasswordActivity"
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:name="com.kunzisoft.keepass.activities.PasswordActivity"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
@@ -53,7 +48,7 @@
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="file" />
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="application/octet-stream" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:host="*" />
|
||||
<data android:pathPattern=".*\\.kdb" />
|
||||
<data android:pathPattern=".*\\..*\\.kdb" />
|
||||
@@ -76,36 +71,36 @@
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.kdbx" />
|
||||
</intent-filter>
|
||||
<intent-filter tools:ignore="AppLinkUrlError">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:mimeType="application/octet-stream"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Folder picker -->
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/nnf_provider_paths" />
|
||||
</provider>
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.fileselect.FilePickerStylishActivity"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.GET_CONTENT" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="file" />
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="application/octet-stream" />
|
||||
<data android:mimeType="application/x-kdb" />
|
||||
<data android:mimeType="application/x-kdbx" />
|
||||
<data android:mimeType="application/x-keepass" />
|
||||
<data android:host="*" />
|
||||
<data android:pathPattern=".*" />
|
||||
<data android:pathPattern=".*\\.*" />
|
||||
<data android:pathPattern=".*\\..*\\.*" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.*" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.*" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.*" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.*" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.*" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.*" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.*" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Main Activity -->
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.GroupActivity"
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:configChanges="keyboardHidden"
|
||||
android:windowSoftInputMode="adjustPan"
|
||||
android:launchMode="singleTop">
|
||||
android:launchMode="singleTask">
|
||||
<meta-data
|
||||
android:name="android.app.default_searchable"
|
||||
android:value="com.kunzisoft.keepass.search.SearchResults"
|
||||
@@ -120,21 +115,38 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.EntryActivity"
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:windowSoftInputMode="stateHidden" />
|
||||
android:configChanges="keyboardHidden" />
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:windowSoftInputMode="stateHidden" />
|
||||
android:configChanges="keyboardHidden"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<!-- About and Settings -->
|
||||
<activity
|
||||
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/menu_about" />
|
||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
||||
<activity android:name="com.kunzisoft.keepass.autofill.AutoFillAuthActivity"
|
||||
android:configChanges="orientation|keyboardHidden" />
|
||||
<activity android:name="com.kunzisoft.keepass.selection.EntrySelectionAuthActivity"
|
||||
android:configChanges="orientation|keyboardHidden" />
|
||||
<activity android:name="com.kunzisoft.keepass.autofill.AutofillLauncherActivity"
|
||||
android:configChanges="keyboardHidden" />
|
||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
|
||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsAutofillActivity" />
|
||||
<activity android:name="com.kunzisoft.keepass.magikeyboard.KeyboardLauncherActivity"
|
||||
android:label="@string/keyboard_name">
|
||||
</activity>
|
||||
<activity android:name="com.kunzisoft.keepass.settings.MagikIMESettings"
|
||||
android:label="@string/keyboard_setting_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.notifications.NotificationCopyingService"
|
||||
android:name="com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<!-- Receiver for Autofill -->
|
||||
@@ -149,6 +161,20 @@
|
||||
<action android:name="android.service.autofill.AutofillService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.magikeyboard.MagikIME"
|
||||
android:label="@string/keyboard_label"
|
||||
android:permission="android.permission.BIND_INPUT_METHOD" >
|
||||
<meta-data android:name="android.view.im"
|
||||
android:resource="@xml/keyboard_method"/>
|
||||
<intent-filter>
|
||||
<action android:name="android.view.InputMethod" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name="com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
||||
</application>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,2 +0,0 @@
|
||||
v7<EFBFBD><07>gx<67><78><EFBFBD>"<04>Dm<44>]tIWRP<52>g<18>y<15>/˰1<CBB0><31><13>X<0B><>fW[<5B>F%<25><1E>\<5C>up4
|
||||
<EFBFBD><EFBFBD>-t;<3B>z<EFBFBD>
|
||||
Binary file not shown.
Binary file not shown.
BIN
app/src/main/assets/fonts/FiraMono-Regular.ttf
Normal file
BIN
app/src/main/assets/fonts/FiraMono-Regular.ttf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<KeyFile>
|
||||
<Meta>
|
||||
<Version>1.00</Version>
|
||||
</Meta>
|
||||
<Key>
|
||||
<Data>zaTWphVNtRbspnwkqjy8FGTy5IqCUx9+FNb5H+VdB24=</Data>
|
||||
</Key>
|
||||
</KeyFile>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities;
|
||||
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.kunzisoft.keepass.R;
|
||||
import com.kunzisoft.keepass.stylish.StylishActivity;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
public class AboutActivity extends StylishActivity {
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.about);
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
toolbar.setTitle(getString(R.string.menu_about));
|
||||
setSupportActionBar(toolbar);
|
||||
assert getSupportActionBar() != null;
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
|
||||
String version;
|
||||
try {
|
||||
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
|
||||
version = packageInfo.versionName;
|
||||
} catch (NameNotFoundException e) {
|
||||
Log.w(getClass().getSimpleName(), "Unable to get application version", e);
|
||||
version = "Unable to get application version.";
|
||||
}
|
||||
version = getString(R.string.version_label) + " " + version;
|
||||
TextView versionText = (TextView) findViewById(R.id.activity_about_version);
|
||||
versionText.setText(version);
|
||||
|
||||
TextView disclaimerText = (TextView) findViewById(R.id.disclaimer);
|
||||
disclaimerText.setText(getString(R.string.disclaimer_formal, new DateTime().getYear()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
// Handle action bar item clicks here. The action bar will
|
||||
// automatically handle clicks on the Home/Up button, so long
|
||||
// as you specify a parent activity in AndroidManifest.xml.
|
||||
int id = item.getItemId();
|
||||
|
||||
//noinspection SimplifiableIfStatement
|
||||
switch (id) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
break;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import android.widget.TextView
|
||||
|
||||
import com.kunzisoft.keepass.BuildConfig
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
|
||||
import org.joda.time.DateTime
|
||||
|
||||
class AboutActivity : StylishActivity() {
|
||||
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_about)
|
||||
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
toolbar.title = getString(R.string.menu_about)
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
var version: String
|
||||
var build: String
|
||||
try {
|
||||
version = packageManager.getPackageInfo(packageName, 0).versionName
|
||||
build = BuildConfig.BUILD_VERSION
|
||||
} catch (e: NameNotFoundException) {
|
||||
Log.w(javaClass.simpleName, "Unable to get the app or the build version", e)
|
||||
version = "Unable to get the app version"
|
||||
build = "Unable to get the build version"
|
||||
}
|
||||
|
||||
version = getString(R.string.version_label, version)
|
||||
val versionTextView = findViewById<TextView>(R.id.activity_about_version)
|
||||
versionTextView.text = version
|
||||
|
||||
build = getString(R.string.build_label, build)
|
||||
val buildTextView = findViewById<TextView>(R.id.activity_about_build)
|
||||
buildTextView.text = build
|
||||
|
||||
|
||||
val disclaimerText = findViewById<TextView>(R.id.disclaimer)
|
||||
disclaimerText.text = getString(R.string.disclaimer_formal, DateTime().year)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
// Handle action bar item clicks here. The action bar will
|
||||
// automatically handle clicks on the Home/Up button, so long
|
||||
// as you specify a parent activity in AndroidManifest.xml.
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> finish()
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
@@ -1,522 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.getkeepsafe.taptargetview.TapTarget;
|
||||
import com.getkeepsafe.taptargetview.TapTargetView;
|
||||
import com.kunzisoft.keepass.R;
|
||||
import com.kunzisoft.keepass.app.App;
|
||||
import com.kunzisoft.keepass.database.Database;
|
||||
import com.kunzisoft.keepass.database.ExtraFields;
|
||||
import com.kunzisoft.keepass.database.PwDatabase;
|
||||
import com.kunzisoft.keepass.database.PwEntry;
|
||||
import com.kunzisoft.keepass.database.security.ProtectedString;
|
||||
import com.kunzisoft.keepass.lock.LockingActivity;
|
||||
import com.kunzisoft.keepass.lock.LockingHideActivity;
|
||||
import com.kunzisoft.keepass.notifications.NotificationCopyingService;
|
||||
import com.kunzisoft.keepass.notifications.NotificationField;
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil;
|
||||
import com.kunzisoft.keepass.settings.SettingsAutofillActivity;
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper;
|
||||
import com.kunzisoft.keepass.utils.EmptyUtils;
|
||||
import com.kunzisoft.keepass.utils.MenuUtil;
|
||||
import com.kunzisoft.keepass.utils.Types;
|
||||
import com.kunzisoft.keepass.utils.Util;
|
||||
import com.kunzisoft.keepass.view.EntryContentsView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
import static com.kunzisoft.keepass.settings.PreferencesUtil.isClipboardNotificationsEnable;
|
||||
import static com.kunzisoft.keepass.settings.PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields;
|
||||
|
||||
public class EntryActivity extends LockingHideActivity {
|
||||
private final static String TAG = EntryActivity.class.getName();
|
||||
|
||||
public static final String KEY_ENTRY = "entry";
|
||||
|
||||
private ImageView titleIconView;
|
||||
private TextView titleView;
|
||||
private EntryContentsView entryContentsView;
|
||||
private Toolbar toolbar;
|
||||
|
||||
protected PwEntry mEntry;
|
||||
private boolean mShowPassword;
|
||||
|
||||
private ClipboardHelper clipboardHelper;
|
||||
private boolean firstLaunchOfActivity;
|
||||
|
||||
private int iconColor;
|
||||
|
||||
public static void launch(Activity act, PwEntry pw, boolean readOnly) {
|
||||
if (LockingActivity.checkTimeIsAllowedOrFinish(act)) {
|
||||
Intent intent = new Intent(act, EntryActivity.class);
|
||||
intent.putExtra(KEY_ENTRY, Types.UUIDtoBytes(pw.getUUID()));
|
||||
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly);
|
||||
act.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.entry_view);
|
||||
|
||||
toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
assert getSupportActionBar() != null;
|
||||
getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_close_white_24dp);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
|
||||
Database db = App.getDB();
|
||||
// Likely the app has been killed exit the activity
|
||||
if ( ! db.getLoaded() ) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
readOnly = db.isReadOnly() || readOnly;
|
||||
|
||||
mShowPassword = !PreferencesUtil.isPasswordMask(this);
|
||||
|
||||
// Get Entry from UUID
|
||||
Intent i = getIntent();
|
||||
UUID uuid = Types.bytestoUUID(i.getByteArrayExtra(KEY_ENTRY));
|
||||
mEntry = db.getPwDatabase().getEntryByUUIDId(uuid);
|
||||
if (mEntry == null) {
|
||||
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
int[] attrs = {R.attr.textColorInverse};
|
||||
TypedArray ta = getTheme().obtainStyledAttributes(attrs);
|
||||
iconColor = ta.getColor(0, Color.WHITE);
|
||||
|
||||
// Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set
|
||||
invalidateOptionsMenu();
|
||||
|
||||
// Update last access time.
|
||||
mEntry.touch(false, false);
|
||||
|
||||
// Get views
|
||||
titleIconView = findViewById(R.id.entry_icon);
|
||||
titleView = findViewById(R.id.entry_title);
|
||||
entryContentsView = findViewById(R.id.entry_contents);
|
||||
entryContentsView.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this));
|
||||
|
||||
// Init the clipboard helper
|
||||
clipboardHelper = new ClipboardHelper(this);
|
||||
firstLaunchOfActivity = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
// Fill data in resume to update from EntryEditActivity
|
||||
fillData();
|
||||
invalidateOptionsMenu();
|
||||
|
||||
// Start to manage field reference to copy a value from ref
|
||||
mEntry.startToManageFieldReferences(App.getDB().getPwDatabase());
|
||||
|
||||
boolean containsUsernameToCopy =
|
||||
mEntry.getUsername().length() > 0;
|
||||
boolean containsPasswordToCopy =
|
||||
(mEntry.getPassword().length() > 0
|
||||
&& PreferencesUtil.allowCopyPasswordAndProtectedFields(this));
|
||||
boolean containsExtraFieldToCopy =
|
||||
(mEntry.allowExtraFields()
|
||||
&& ((mEntry.containsCustomFields()
|
||||
&& mEntry.containsCustomFieldsNotProtected())
|
||||
|| (mEntry.containsCustomFields()
|
||||
&& mEntry.containsCustomFieldsProtected()
|
||||
&& PreferencesUtil.allowCopyPasswordAndProtectedFields(this))
|
||||
)
|
||||
);
|
||||
|
||||
// If notifications enabled in settings
|
||||
// Don't if application timeout
|
||||
if (firstLaunchOfActivity && !App.isShutdown() && isClipboardNotificationsEnable(getApplicationContext())) {
|
||||
if (containsUsernameToCopy
|
||||
|| containsPasswordToCopy
|
||||
|| containsExtraFieldToCopy
|
||||
) {
|
||||
// username already copied, waiting for user's action before copy password.
|
||||
Intent intent = new Intent(this, NotificationCopyingService.class);
|
||||
intent.setAction(NotificationCopyingService.ACTION_NEW_NOTIFICATION);
|
||||
if (mEntry.getTitle() != null)
|
||||
intent.putExtra(NotificationCopyingService.EXTRA_ENTRY_TITLE, mEntry.getTitle());
|
||||
// Construct notification fields
|
||||
ArrayList<NotificationField> notificationFields = new ArrayList<>();
|
||||
// Add username if exists to notifications
|
||||
if (containsUsernameToCopy)
|
||||
notificationFields.add(
|
||||
new NotificationField(
|
||||
NotificationField.NotificationFieldId.USERNAME,
|
||||
mEntry.getUsername(),
|
||||
getResources()));
|
||||
// Add password to notifications
|
||||
if (containsPasswordToCopy) {
|
||||
notificationFields.add(
|
||||
new NotificationField(
|
||||
NotificationField.NotificationFieldId.PASSWORD,
|
||||
mEntry.getPassword(),
|
||||
getResources()));
|
||||
}
|
||||
// Add extra fields
|
||||
if (containsExtraFieldToCopy) {
|
||||
try {
|
||||
mEntry.getFields().doActionToAllCustomProtectedField(new ExtraFields.ActionProtected() {
|
||||
private int anonymousFieldNumber = 0;
|
||||
@Override
|
||||
public void doAction(String key, ProtectedString value) {
|
||||
//If value is not protected or allowed
|
||||
if (!value.isProtected() || PreferencesUtil.allowCopyPasswordAndProtectedFields(EntryActivity.this)) {
|
||||
notificationFields.add(
|
||||
new NotificationField(
|
||||
NotificationField.NotificationFieldId.getAnonymousFieldId()[anonymousFieldNumber],
|
||||
value.toString(),
|
||||
key,
|
||||
getResources()));
|
||||
anonymousFieldNumber++;
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
Log.w(TAG, "Only " + NotificationField.NotificationFieldId.getAnonymousFieldId().length +
|
||||
" anonymous notifications are available");
|
||||
}
|
||||
}
|
||||
// Add notifications
|
||||
intent.putParcelableArrayListExtra(NotificationCopyingService.EXTRA_FIELDS, notificationFields);
|
||||
|
||||
startService(intent);
|
||||
}
|
||||
mEntry.stopToManageFieldReferences();
|
||||
}
|
||||
firstLaunchOfActivity = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and display learning views
|
||||
* Displays the explanation for copying a field and editing an entry
|
||||
*/
|
||||
private void checkAndPerformedEducation(Menu menu) {
|
||||
if (PreferencesUtil.isEducationScreensEnabled(this)) {
|
||||
|
||||
if (entryContentsView != null && entryContentsView.isUserNamePresent()
|
||||
&& !PreferencesUtil.isEducationCopyUsernamePerformed(this)) {
|
||||
TapTargetView.showFor(this,
|
||||
TapTarget.forView(findViewById(R.id.entry_user_name_action_image),
|
||||
getString(R.string.education_field_copy_title),
|
||||
getString(R.string.education_field_copy_summary))
|
||||
.textColorInt(Color.WHITE)
|
||||
.tintTarget(false)
|
||||
.cancelable(true),
|
||||
new TapTargetView.Listener() {
|
||||
@Override
|
||||
public void onTargetClick(TapTargetView view) {
|
||||
super.onTargetClick(view);
|
||||
clipboardHelper.timeoutCopyToClipboard(mEntry.getUsername(),
|
||||
getString(R.string.copy_field, getString(R.string.entry_user_name)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOuterCircleClick(TapTargetView view) {
|
||||
super.onOuterCircleClick(view);
|
||||
view.dismiss(false);
|
||||
// Launch autofill settings
|
||||
startActivity(new Intent(EntryActivity.this, SettingsAutofillActivity.class));
|
||||
}
|
||||
});
|
||||
PreferencesUtil.saveEducationPreference(this,
|
||||
R.string.education_copy_username_key);
|
||||
|
||||
} else if (!PreferencesUtil.isEducationEntryEditPerformed(this)) {
|
||||
|
||||
try {
|
||||
TapTargetView.showFor(this,
|
||||
TapTarget.forToolbarMenuItem(toolbar, R.id.menu_edit,
|
||||
getString(R.string.education_entry_edit_title),
|
||||
getString(R.string.education_entry_edit_summary))
|
||||
.textColorInt(Color.WHITE)
|
||||
.tintTarget(true)
|
||||
.cancelable(true),
|
||||
new TapTargetView.Listener() {
|
||||
@Override
|
||||
public void onTargetClick(TapTargetView view) {
|
||||
super.onTargetClick(view);
|
||||
MenuItem editItem = menu.findItem(R.id.menu_edit);
|
||||
onOptionsItemSelected(editItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOuterCircleClick(TapTargetView view) {
|
||||
super.onOuterCircleClick(view);
|
||||
view.dismiss(false);
|
||||
// Open Keepass doc to create field references
|
||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.field_references_url)));
|
||||
startActivity(browserIntent);
|
||||
}
|
||||
});
|
||||
PreferencesUtil.saveEducationPreference(this,
|
||||
R.string.education_entry_edit_key);
|
||||
} catch (Exception e) {
|
||||
// If icon not visible
|
||||
Log.w(TAG, "Can't performed education for entry's edition");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void fillData() {
|
||||
Database db = App.getDB();
|
||||
PwDatabase pm = db.getPwDatabase();
|
||||
|
||||
mEntry.startToManageFieldReferences(pm);
|
||||
|
||||
// Assign title icon
|
||||
db.getDrawFactory().assignDatabaseIconTo(this, titleIconView, mEntry.getIcon(), iconColor);
|
||||
|
||||
// Assign title text
|
||||
titleView.setText(mEntry.getVisualTitle());
|
||||
|
||||
// Assign basic fields
|
||||
entryContentsView.assignUserName(mEntry.getUsername());
|
||||
entryContentsView.assignUserNameCopyListener(view ->
|
||||
clipboardHelper.timeoutCopyToClipboard(mEntry.getUsername(),
|
||||
getString(R.string.copy_field, getString(R.string.entry_user_name)))
|
||||
);
|
||||
|
||||
boolean allowCopyPassword = PreferencesUtil.allowCopyPasswordAndProtectedFields(this);
|
||||
entryContentsView.assignPassword(mEntry.getPassword(), allowCopyPassword);
|
||||
if (allowCopyPassword) {
|
||||
entryContentsView.assignPasswordCopyListener(view ->
|
||||
clipboardHelper.timeoutCopyToClipboard(mEntry.getPassword(),
|
||||
getString(R.string.copy_field, getString(R.string.entry_password)))
|
||||
);
|
||||
} else {
|
||||
// If dialog not already shown
|
||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)) {
|
||||
entryContentsView.assignPasswordCopyListener(v -> {
|
||||
String message = getString(R.string.allow_copy_password_warning) +
|
||||
"\n\n" +
|
||||
getString(R.string.clipboard_warning);
|
||||
AlertDialog warningDialog = new AlertDialog.Builder(EntryActivity.this)
|
||||
.setMessage(message).create();
|
||||
warningDialog.setButton(AlertDialog.BUTTON1, getText(android.R.string.ok),
|
||||
(dialog, which) -> {
|
||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(EntryActivity.this, true);
|
||||
dialog.dismiss();
|
||||
fillData();
|
||||
});
|
||||
warningDialog.setButton(AlertDialog.BUTTON2, getText(android.R.string.cancel),
|
||||
(dialog, which) -> {
|
||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(EntryActivity.this, false);
|
||||
dialog.dismiss();
|
||||
fillData();
|
||||
});
|
||||
warningDialog.show();
|
||||
});
|
||||
} else {
|
||||
entryContentsView.assignPasswordCopyListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
entryContentsView.assignURL(mEntry.getUrl());
|
||||
|
||||
entryContentsView.setHiddenPasswordStyle(!mShowPassword);
|
||||
entryContentsView.assignComment(mEntry.getNotes());
|
||||
|
||||
// Assign custom fields
|
||||
if (mEntry.allowExtraFields()) {
|
||||
entryContentsView.clearExtraFields();
|
||||
|
||||
mEntry.getFields().doActionToAllCustomProtectedField((label, value) -> {
|
||||
boolean showAction = (!value.isProtected() || PreferencesUtil.allowCopyPasswordAndProtectedFields(EntryActivity.this));
|
||||
entryContentsView.addExtraField(label, value, showAction, view ->
|
||||
clipboardHelper.timeoutCopyToClipboard(
|
||||
value.toString(),
|
||||
getString(R.string.copy_field, label)
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Assign dates
|
||||
entryContentsView.assignCreationDate(mEntry.getCreationTime().getDate());
|
||||
entryContentsView.assignModificationDate(mEntry.getLastModificationTime().getDate());
|
||||
entryContentsView.assignLastAccessDate(mEntry.getLastAccessTime().getDate());
|
||||
Date expires = mEntry.getExpiryTime().getDate();
|
||||
if ( mEntry.isExpires() ) {
|
||||
entryContentsView.assignExpiresDate(expires);
|
||||
} else {
|
||||
entryContentsView.assignExpiresDate(getString(R.string.never));
|
||||
}
|
||||
|
||||
mEntry.stopToManageFieldReferences();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
switch (requestCode) {
|
||||
case EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE:
|
||||
fillData();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void changeShowPasswordIcon(MenuItem togglePassword) {
|
||||
if ( mShowPassword ) {
|
||||
togglePassword.setTitle(R.string.menu_hide_password);
|
||||
togglePassword.setIcon(R.drawable.ic_visibility_off_white_24dp);
|
||||
} else {
|
||||
togglePassword.setTitle(R.string.menu_showpass);
|
||||
togglePassword.setIcon(R.drawable.ic_visibility_white_24dp);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
MenuUtil.contributionMenuInflater(inflater, menu);
|
||||
inflater.inflate(R.menu.entry, menu);
|
||||
inflater.inflate(R.menu.database_lock, menu);
|
||||
|
||||
if (readOnly) {
|
||||
MenuItem edit = menu.findItem(R.id.menu_edit);
|
||||
if (edit != null)
|
||||
edit.setVisible(false);
|
||||
}
|
||||
|
||||
MenuItem togglePassword = menu.findItem(R.id.menu_toggle_pass);
|
||||
if (entryContentsView != null && togglePassword != null) {
|
||||
if (entryContentsView.isPasswordPresent() || entryContentsView.atLeastOneFieldProtectedPresent()) {
|
||||
changeShowPasswordIcon(togglePassword);
|
||||
} else {
|
||||
togglePassword.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem gotoUrl = menu.findItem(R.id.menu_goto_url);
|
||||
if (gotoUrl != null) {
|
||||
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
|
||||
// so mEntry may not be set
|
||||
if (mEntry == null) {
|
||||
gotoUrl.setVisible(false);
|
||||
} else {
|
||||
String url = mEntry.getUrl();
|
||||
if (EmptyUtils.isNullOrEmpty(url)) {
|
||||
// disable button if url is not available
|
||||
gotoUrl.setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show education views
|
||||
new Handler().post(() -> checkAndPerformedEducation(menu));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch ( item.getItemId() ) {
|
||||
case R.id.menu_contribute:
|
||||
return MenuUtil.onContributionItemSelected(this);
|
||||
|
||||
case R.id.menu_toggle_pass:
|
||||
mShowPassword = !mShowPassword;
|
||||
changeShowPasswordIcon(item);
|
||||
entryContentsView.setHiddenPasswordStyle(!mShowPassword);
|
||||
return true;
|
||||
|
||||
case R.id.menu_edit:
|
||||
EntryEditActivity.launch(EntryActivity.this, mEntry);
|
||||
return true;
|
||||
|
||||
case R.id.menu_goto_url:
|
||||
String url;
|
||||
url = mEntry.getUrl();
|
||||
|
||||
// Default http:// if no protocol specified
|
||||
if ( ! url.contains("://") ) {
|
||||
url = "http://" + url;
|
||||
}
|
||||
|
||||
try {
|
||||
Util.gotoUrl(this, url);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Toast.makeText(this, R.string.no_url_handler, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
return true;
|
||||
|
||||
case R.id.menu_lock:
|
||||
lockAndExit();
|
||||
return true;
|
||||
|
||||
case android.R.id.home :
|
||||
finish(); // close this activity and return to preview activity (if there is any)
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
// Transit data in previous Activity after an update
|
||||
/*
|
||||
TODO Slowdown when add entry as result
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry);
|
||||
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent);
|
||||
*/
|
||||
super.finish();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,426 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.EntryVersioned
|
||||
import com.kunzisoft.keepass.database.element.PwNodeId
|
||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.settings.SettingsAutofillActivity
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.EntryContentsView
|
||||
|
||||
class EntryActivity : LockingHideActivity() {
|
||||
|
||||
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
||||
private var titleIconView: ImageView? = null
|
||||
private var entryContentsView: EntryContentsView? = null
|
||||
private var toolbar: Toolbar? = null
|
||||
|
||||
private var mEntry: EntryVersioned? = null
|
||||
private var mShowPassword: Boolean = false
|
||||
|
||||
private var clipboardHelper: ClipboardHelper? = null
|
||||
private var firstLaunchOfActivity: Boolean = false
|
||||
|
||||
private var iconColor: Int = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_entry)
|
||||
|
||||
toolbar = findViewById(R.id.toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
val currentDatabase = Database.getInstance()
|
||||
mReadOnly = currentDatabase.isReadOnly || mReadOnly
|
||||
|
||||
mShowPassword = !PreferencesUtil.isPasswordMask(this)
|
||||
|
||||
// Get Entry from UUID
|
||||
try {
|
||||
val keyEntry: PwNodeId<*> = intent.getParcelableExtra(KEY_ENTRY)
|
||||
mEntry = currentDatabase.getEntryById(keyEntry)
|
||||
} catch (e: ClassCastException) {
|
||||
Log.e(TAG, "Unable to retrieve the entry key")
|
||||
}
|
||||
|
||||
if (mEntry == null) {
|
||||
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
// Update last access time.
|
||||
mEntry?.touch(modified = false, touchParents = false)
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
iconColor = taIconColor.getColor(0, Color.BLACK)
|
||||
taIconColor.recycle()
|
||||
|
||||
// Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set
|
||||
invalidateOptionsMenu()
|
||||
|
||||
// Get views
|
||||
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
||||
titleIconView = findViewById(R.id.entry_icon)
|
||||
entryContentsView = findViewById(R.id.entry_contents)
|
||||
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
||||
|
||||
// Init the clipboard helper
|
||||
clipboardHelper = ClipboardHelper(this)
|
||||
firstLaunchOfActivity = true
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
mEntry?.let { entry ->
|
||||
// Fill data in resume to update from EntryEditActivity
|
||||
fillEntryDataInContentsView(entry)
|
||||
// Refresh Menu
|
||||
invalidateOptionsMenu()
|
||||
|
||||
val entryInfo = entry.getEntryInfo(Database.getInstance())
|
||||
|
||||
// Manage entry copy to start notification if allowed
|
||||
if (firstLaunchOfActivity) {
|
||||
// Manage entry to launch copying notification if allowed
|
||||
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
||||
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
||||
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
|
||||
MagikIME.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
firstLaunchOfActivity = false
|
||||
}
|
||||
|
||||
private fun fillEntryDataInContentsView(entry: EntryVersioned) {
|
||||
|
||||
val database = Database.getInstance()
|
||||
database.startManageEntry(entry)
|
||||
// Assign title icon
|
||||
titleIconView?.assignDatabaseIcon(database.drawFactory, entry.icon, iconColor)
|
||||
|
||||
// Assign title text
|
||||
val entryTitle = entry.title
|
||||
collapsingToolbarLayout?.title = entryTitle
|
||||
toolbar?.title = entryTitle
|
||||
|
||||
// Assign basic fields
|
||||
entryContentsView?.assignUserName(entry.username)
|
||||
entryContentsView?.assignUserNameCopyListener(View.OnClickListener {
|
||||
database.startManageEntry(entry)
|
||||
clipboardHelper?.timeoutCopyToClipboard(entry.username,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_user_name)))
|
||||
database.stopManageEntry(entry)
|
||||
})
|
||||
|
||||
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
||||
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
|
||||
val allowCopyPasswordAndProtectedFields =
|
||||
PreferencesUtil.allowCopyPasswordAndProtectedFields(this)
|
||||
|
||||
val showWarningClipboardDialogOnClickListener = View.OnClickListener {
|
||||
AlertDialog.Builder(this@EntryActivity)
|
||||
.setMessage(getString(R.string.allow_copy_password_warning) +
|
||||
"\n\n" +
|
||||
getString(R.string.clipboard_warning))
|
||||
.create().apply {
|
||||
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) {dialog, _ ->
|
||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true)
|
||||
dialog.dismiss()
|
||||
fillEntryDataInContentsView(entry)
|
||||
}
|
||||
setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ ->
|
||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, false)
|
||||
dialog.dismiss()
|
||||
fillEntryDataInContentsView(entry)
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
entryContentsView?.assignPassword(entry.password, allowCopyPasswordAndProtectedFields)
|
||||
if (allowCopyPasswordAndProtectedFields) {
|
||||
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
|
||||
database.startManageEntry(entry)
|
||||
clipboardHelper?.timeoutCopyToClipboard(entry.password,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_password)))
|
||||
database.stopManageEntry(entry)
|
||||
})
|
||||
} else {
|
||||
// If dialog not already shown
|
||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||
entryContentsView?.assignPasswordCopyListener(showWarningClipboardDialogOnClickListener)
|
||||
} else {
|
||||
entryContentsView?.assignPasswordCopyListener(null)
|
||||
}
|
||||
}
|
||||
|
||||
entryContentsView?.assignURL(entry.url)
|
||||
entryContentsView?.assignComment(entry.notes)
|
||||
|
||||
// Assign custom fields
|
||||
if (entry.allowCustomFields()) {
|
||||
entryContentsView?.clearExtraFields()
|
||||
|
||||
for (element in entry.customFields.entries) {
|
||||
val label = element.key
|
||||
val value = element.value
|
||||
|
||||
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
||||
if (allowCopyProtectedField) {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, View.OnClickListener {
|
||||
clipboardHelper?.timeoutCopyToClipboard(
|
||||
value.toString(),
|
||||
getString(R.string.copy_field, label)
|
||||
)
|
||||
})
|
||||
} else {
|
||||
// If dialog not already shown
|
||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
|
||||
} else {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
|
||||
|
||||
// Assign dates
|
||||
entry.creationTime.date?.let {
|
||||
entryContentsView?.assignCreationDate(it)
|
||||
}
|
||||
entry.lastModificationTime.date?.let {
|
||||
entryContentsView?.assignModificationDate(it)
|
||||
}
|
||||
entry.lastAccessTime.date?.let {
|
||||
entryContentsView?.assignLastAccessDate(it)
|
||||
}
|
||||
val expires = entry.expiryTime.date
|
||||
if (entry.isExpires && expires != null) {
|
||||
entryContentsView?.assignExpiresDate(expires)
|
||||
} else {
|
||||
entryContentsView?.assignExpiresDate(getString(R.string.never))
|
||||
}
|
||||
|
||||
// Assign special data
|
||||
entryContentsView?.assignUUID(entry.nodeId.id)
|
||||
|
||||
database.stopManageEntry(entry)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
when (requestCode) {
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE ->
|
||||
// Not directly get the entry from intent data but from database
|
||||
mEntry?.let {
|
||||
fillEntryDataInContentsView(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeShowPasswordIcon(togglePassword: MenuItem?) {
|
||||
if (mShowPassword) {
|
||||
togglePassword?.setTitle(R.string.menu_hide_password)
|
||||
togglePassword?.setIcon(R.drawable.ic_visibility_off_white_24dp)
|
||||
} else {
|
||||
togglePassword?.setTitle(R.string.menu_showpass)
|
||||
togglePassword?.setIcon(R.drawable.ic_visibility_white_24dp)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
val inflater = menuInflater
|
||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||
inflater.inflate(R.menu.entry, menu)
|
||||
inflater.inflate(R.menu.database_lock, menu)
|
||||
|
||||
if (mReadOnly) {
|
||||
menu.findItem(R.id.menu_edit)?.isVisible = false
|
||||
}
|
||||
|
||||
val togglePassword = menu.findItem(R.id.menu_toggle_pass)
|
||||
entryContentsView?.let {
|
||||
if (it.isPasswordPresent || it.atLeastOneFieldProtectedPresent()) {
|
||||
changeShowPasswordIcon(togglePassword)
|
||||
} else {
|
||||
togglePassword?.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
val gotoUrl = menu.findItem(R.id.menu_goto_url)
|
||||
gotoUrl?.apply {
|
||||
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
|
||||
// so mEntry may not be set
|
||||
if (mEntry == null) {
|
||||
isVisible = false
|
||||
} else {
|
||||
if (mEntry?.url?.isEmpty() != false) {
|
||||
// disable button if url is not available
|
||||
isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show education views
|
||||
Handler().post { performedNextEducation(EntryActivityEducation(this), menu) }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
||||
menu: Menu) {
|
||||
val entryCopyEducationPerformed = entryContentsView?.isUserNamePresent == true
|
||||
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
||||
findViewById(R.id.entry_user_name_action_image),
|
||||
{
|
||||
clipboardHelper?.timeoutCopyToClipboard(mEntry!!.username,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_user_name)))
|
||||
},
|
||||
{
|
||||
// Launch autofill settings
|
||||
startActivity(Intent(this@EntryActivity, SettingsAutofillActivity::class.java))
|
||||
})
|
||||
|
||||
if (!entryCopyEducationPerformed) {
|
||||
// entryEditEducationPerformed
|
||||
toolbar?.findViewById<View>(R.id.menu_edit) != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
|
||||
toolbar!!.findViewById(R.id.menu_edit),
|
||||
{
|
||||
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
||||
},
|
||||
{
|
||||
// Open Keepass doc to create field references
|
||||
startActivity(Intent(Intent.ACTION_VIEW,
|
||||
UriUtil.parse(getString(R.string.field_references_url))))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_contribute -> {
|
||||
MenuUtil.onContributionItemSelected(this)
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_toggle_pass -> {
|
||||
mShowPassword = !mShowPassword
|
||||
changeShowPasswordIcon(item)
|
||||
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_edit -> {
|
||||
mEntry?.let {
|
||||
EntryEditActivity.launch(this@EntryActivity, it)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_goto_url -> {
|
||||
var url: String = mEntry?.url ?: ""
|
||||
|
||||
// Default http:// if no protocol specified
|
||||
if (!url.contains("://")) {
|
||||
url = "http://$url"
|
||||
}
|
||||
|
||||
UriUtil.gotoUrl(this, url)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_lock -> {
|
||||
lockAndExit()
|
||||
return true
|
||||
}
|
||||
|
||||
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
|
||||
override fun finish() {
|
||||
// Transit data in previous Activity after an update
|
||||
/*
|
||||
TODO Slowdown when add entry as result
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry);
|
||||
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent);
|
||||
*/
|
||||
super.finish()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = EntryActivity::class.java.name
|
||||
|
||||
const val KEY_ENTRY = "entry"
|
||||
|
||||
fun launch(activity: Activity, pw: EntryVersioned, readOnly: Boolean) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, pw.nodeId)
|
||||
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
||||
activity.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,590 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.getkeepsafe.taptargetview.TapTarget;
|
||||
import com.getkeepsafe.taptargetview.TapTargetView;
|
||||
import com.kunzisoft.keepass.R;
|
||||
import com.kunzisoft.keepass.app.App;
|
||||
import com.kunzisoft.keepass.database.Database;
|
||||
import com.kunzisoft.keepass.database.PwDatabase;
|
||||
import com.kunzisoft.keepass.database.PwDate;
|
||||
import com.kunzisoft.keepass.database.PwEntry;
|
||||
import com.kunzisoft.keepass.database.PwGroup;
|
||||
import com.kunzisoft.keepass.database.PwGroupId;
|
||||
import com.kunzisoft.keepass.database.PwIconStandard;
|
||||
import com.kunzisoft.keepass.database.PwNode;
|
||||
import com.kunzisoft.keepass.database.action.RunnableOnFinish;
|
||||
import com.kunzisoft.keepass.database.action.node.AddEntryRunnable;
|
||||
import com.kunzisoft.keepass.database.action.node.AfterActionNodeOnFinish;
|
||||
import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable;
|
||||
import com.kunzisoft.keepass.database.security.ProtectedString;
|
||||
import com.kunzisoft.keepass.dialogs.GeneratePasswordDialogFragment;
|
||||
import com.kunzisoft.keepass.dialogs.IconPickerDialogFragment;
|
||||
import com.kunzisoft.keepass.lock.LockingActivity;
|
||||
import com.kunzisoft.keepass.lock.LockingHideActivity;
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil;
|
||||
import com.kunzisoft.keepass.tasks.SaveDatabaseProgressTaskDialogFragment;
|
||||
import com.kunzisoft.keepass.tasks.UpdateProgressTaskStatus;
|
||||
import com.kunzisoft.keepass.utils.MenuUtil;
|
||||
import com.kunzisoft.keepass.utils.Types;
|
||||
import com.kunzisoft.keepass.utils.Util;
|
||||
import com.kunzisoft.keepass.view.EntryEditCustomField;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static com.kunzisoft.keepass.dialogs.IconPickerDialogFragment.KEY_ICON_STANDARD;
|
||||
|
||||
public class EntryEditActivity extends LockingHideActivity
|
||||
implements IconPickerDialogFragment.IconPickerListener,
|
||||
GeneratePasswordDialogFragment.GeneratePasswordListener {
|
||||
|
||||
private static final String TAG = EntryEditActivity.class.getName();
|
||||
|
||||
// Keys for current Activity
|
||||
public static final String KEY_ENTRY = "entry";
|
||||
public static final String KEY_PARENT = "parent";
|
||||
|
||||
// Keys for callback
|
||||
public static final int ADD_ENTRY_RESULT_CODE = 31;
|
||||
public static final int UPDATE_ENTRY_RESULT_CODE = 32;
|
||||
public static final int ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129;
|
||||
public static final String ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY";
|
||||
|
||||
private Database database;
|
||||
|
||||
protected PwEntry mEntry;
|
||||
protected PwEntry mCallbackNewEntry;
|
||||
protected boolean mIsNew;
|
||||
protected PwIconStandard mSelectedIconStandard;
|
||||
|
||||
// Views
|
||||
private ScrollView scrollView;
|
||||
private EditText entryTitleView;
|
||||
private ImageView entryIconView;
|
||||
private EditText entryUserNameView;
|
||||
private EditText entryUrlView;
|
||||
private EditText entryPasswordView;
|
||||
private EditText entryConfirmationPasswordView;
|
||||
private View generatePasswordView;
|
||||
private EditText entryCommentView;
|
||||
private ViewGroup entryExtraFieldsContainer;
|
||||
private View addNewFieldView;
|
||||
private View saveView;
|
||||
private int iconColor;
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to update an existing entry
|
||||
*
|
||||
* @param act from activity
|
||||
* @param pw Entry to update
|
||||
*/
|
||||
public static void launch(Activity act, PwEntry pw) {
|
||||
if (LockingActivity.checkTimeIsAllowedOrFinish(act)) {
|
||||
Intent intent = new Intent(act, EntryEditActivity.class);
|
||||
intent.putExtra(KEY_ENTRY, Types.UUIDtoBytes(pw.getUUID()));
|
||||
act.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to add a new entry
|
||||
*
|
||||
* @param act from activity
|
||||
* @param pwGroup Group who will contains new entry
|
||||
*/
|
||||
public static void launch(Activity act, PwGroup pwGroup) {
|
||||
if (LockingActivity.checkTimeIsAllowedOrFinish(act)) {
|
||||
Intent intent = new Intent(act, EntryEditActivity.class);
|
||||
intent.putExtra(KEY_PARENT, pwGroup.getId());
|
||||
act.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.entry_edit);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
assert getSupportActionBar() != null;
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setDisplayShowHomeEnabled(true);
|
||||
|
||||
scrollView = findViewById(R.id.entry_scroll);
|
||||
scrollView.setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET);
|
||||
|
||||
entryTitleView = findViewById(R.id.entry_title);
|
||||
entryIconView = findViewById(R.id.icon_button);
|
||||
entryUserNameView = findViewById(R.id.entry_user_name);
|
||||
entryUrlView = findViewById(R.id.entry_url);
|
||||
entryPasswordView = findViewById(R.id.entry_password);
|
||||
entryConfirmationPasswordView = findViewById(R.id.entry_confpassword);
|
||||
entryCommentView = findViewById(R.id.entry_comment);
|
||||
entryExtraFieldsContainer = findViewById(R.id.advanced_container);
|
||||
|
||||
// Likely the app has been killed exit the activity
|
||||
database = App.getDB();
|
||||
if ( ! database.getLoaded() ) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = getIntent();
|
||||
byte[] uuidBytes = intent.getByteArrayExtra(KEY_ENTRY);
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
int[] attrs = {android.R.attr.textColorPrimary};
|
||||
TypedArray ta = getTheme().obtainStyledAttributes(attrs);
|
||||
iconColor = ta.getColor(0, Color.WHITE);
|
||||
|
||||
mSelectedIconStandard = database.getPwDatabase().getIconFactory().getUnknownIcon();
|
||||
|
||||
PwDatabase pm = database.getPwDatabase();
|
||||
if ( uuidBytes == null ) {
|
||||
PwGroupId parentId = intent.getParcelableExtra(KEY_PARENT);
|
||||
PwGroup parent = pm.getGroupByGroupId(parentId);
|
||||
mEntry = database.createEntry(parent);
|
||||
mIsNew = true;
|
||||
// Add the default icon
|
||||
database.getDrawFactory().assignDefaultDatabaseIconTo(this, entryIconView, iconColor);
|
||||
} else {
|
||||
UUID uuid = Types.bytestoUUID(uuidBytes);
|
||||
mEntry = pm.getEntryByUUIDId(uuid);
|
||||
mIsNew = false;
|
||||
fillData();
|
||||
}
|
||||
|
||||
// Assign title
|
||||
setTitle((mIsNew) ? getString(R.string.add_entry) : getString(R.string.edit_entry));
|
||||
|
||||
// Retrieve the icon after an orientation change
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(KEY_ICON_STANDARD)) {
|
||||
iconPicked(savedInstanceState);
|
||||
}
|
||||
|
||||
// Add listener to the icon
|
||||
entryIconView.setOnClickListener(v ->
|
||||
IconPickerDialogFragment.launch(EntryEditActivity.this));
|
||||
|
||||
// Generate password button
|
||||
generatePasswordView = findViewById(R.id.generate_button);
|
||||
generatePasswordView.setOnClickListener(v -> openPasswordGenerator());
|
||||
|
||||
// Save button
|
||||
saveView = findViewById(R.id.entry_save);
|
||||
saveView.setOnClickListener(v -> saveEntry());
|
||||
|
||||
|
||||
if (mEntry.allowExtraFields()) {
|
||||
addNewFieldView = findViewById(R.id.add_new_field);
|
||||
addNewFieldView.setVisibility(View.VISIBLE);
|
||||
addNewFieldView.setOnClickListener(v -> addNewCustomField());
|
||||
}
|
||||
|
||||
// Verify the education views
|
||||
checkAndPerformedEducation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the password generator fragment
|
||||
*/
|
||||
private void openPasswordGenerator() {
|
||||
GeneratePasswordDialogFragment generatePasswordDialogFragment = new GeneratePasswordDialogFragment();
|
||||
generatePasswordDialogFragment.show(getSupportFragmentManager(), "PasswordGeneratorFragment");
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new view to fill in the information of the customized field
|
||||
*/
|
||||
private void addNewCustomField() {
|
||||
EntryEditCustomField entryEditCustomField = new EntryEditCustomField(EntryEditActivity.this);
|
||||
entryEditCustomField.setData("", new ProtectedString(false, ""));
|
||||
boolean visibilityFontActivated = PreferencesUtil.fieldFontIsInVisibility(this);
|
||||
entryEditCustomField.setFontVisibility(visibilityFontActivated);
|
||||
entryExtraFieldsContainer.addView(entryEditCustomField);
|
||||
|
||||
// Scroll bottom
|
||||
scrollView.post(() -> scrollView.fullScroll(ScrollView.FOCUS_DOWN));
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the new entry or update an existing entry in the database
|
||||
*/
|
||||
private void saveEntry() {
|
||||
if (!validateBeforeSaving()) {
|
||||
return;
|
||||
}
|
||||
mCallbackNewEntry = populateNewEntry();
|
||||
|
||||
// Open a progress dialog and save entry
|
||||
AfterActionNodeOnFinish onFinish = new AfterSave();
|
||||
EntryEditActivity act = EntryEditActivity.this;
|
||||
RunnableOnFinish task;
|
||||
if ( mIsNew ) {
|
||||
task = new AddEntryRunnable(act, database, mCallbackNewEntry, onFinish);
|
||||
} else {
|
||||
task = new UpdateEntryRunnable(act, database, mEntry, mCallbackNewEntry, onFinish);
|
||||
}
|
||||
task.setUpdateProgressTaskStatus(
|
||||
new UpdateProgressTaskStatus(this,
|
||||
SaveDatabaseProgressTaskDialogFragment.start(
|
||||
getSupportFragmentManager())
|
||||
));
|
||||
new Thread(task).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and display learning views
|
||||
* Displays the explanation for the icon selection, the password generator and for a new field
|
||||
*/
|
||||
private void checkAndPerformedEducation() {
|
||||
if (PreferencesUtil.isEducationScreensEnabled(this)) {
|
||||
// TODO Show icon
|
||||
|
||||
if (!PreferencesUtil.isEducationPasswordGeneratorPerformed(this)) {
|
||||
TapTargetView.showFor(this,
|
||||
TapTarget.forView(generatePasswordView,
|
||||
getString(R.string.education_generate_password_title),
|
||||
getString(R.string.education_generate_password_summary))
|
||||
.textColorInt(Color.WHITE)
|
||||
.tintTarget(false)
|
||||
.cancelable(true),
|
||||
new TapTargetView.Listener() {
|
||||
@Override
|
||||
public void onTargetClick(TapTargetView view) {
|
||||
super.onTargetClick(view);
|
||||
openPasswordGenerator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOuterCircleClick(TapTargetView view) {
|
||||
super.onOuterCircleClick(view);
|
||||
view.dismiss(false);
|
||||
}
|
||||
});
|
||||
PreferencesUtil.saveEducationPreference(this,
|
||||
R.string.education_password_generator_key);
|
||||
} else if (mEntry.allowExtraFields()
|
||||
&& !mEntry.containsCustomFields()
|
||||
&& !PreferencesUtil.isEducationEntryNewFieldPerformed(this)) {
|
||||
TapTargetView.showFor(this,
|
||||
TapTarget.forView(addNewFieldView,
|
||||
getString(R.string.education_entry_new_field_title),
|
||||
getString(R.string.education_entry_new_field_summary))
|
||||
.textColorInt(Color.WHITE)
|
||||
.tintTarget(false)
|
||||
.cancelable(true),
|
||||
new TapTargetView.Listener() {
|
||||
@Override
|
||||
public void onTargetClick(TapTargetView view) {
|
||||
super.onTargetClick(view);
|
||||
addNewCustomField();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOuterCircleClick(TapTargetView view) {
|
||||
super.onOuterCircleClick(view);
|
||||
view.dismiss(false);
|
||||
}
|
||||
});
|
||||
PreferencesUtil.saveEducationPreference(this,
|
||||
R.string.education_entry_new_field_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility class to retrieve a validation or an error with a message
|
||||
*/
|
||||
private class ErrorValidation {
|
||||
static final int unknownMessage = -1;
|
||||
|
||||
boolean isValidate = false;
|
||||
int messageId = unknownMessage;
|
||||
|
||||
void showValidationErrorIfNeeded() {
|
||||
if (!isValidate && messageId != unknownMessage)
|
||||
Toast.makeText(EntryEditActivity.this, messageId, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate or not the entry form
|
||||
*
|
||||
* @return ErrorValidation An error with a message or a validation without message
|
||||
*/
|
||||
protected ErrorValidation validate() {
|
||||
ErrorValidation errorValidation = new ErrorValidation();
|
||||
|
||||
// Require title
|
||||
String title = entryTitleView.getText().toString();
|
||||
if ( title.length() == 0 ) {
|
||||
errorValidation.messageId = R.string.error_title_required;
|
||||
return errorValidation;
|
||||
}
|
||||
|
||||
// Validate password
|
||||
String pass = entryPasswordView.getText().toString();
|
||||
String conf = entryConfirmationPasswordView.getText().toString();
|
||||
if ( ! pass.equals(conf) ) {
|
||||
errorValidation.messageId = R.string.error_pass_match;
|
||||
return errorValidation;
|
||||
}
|
||||
|
||||
// Validate extra fields
|
||||
if (mEntry.allowExtraFields()) {
|
||||
for (int i = 0; i < entryExtraFieldsContainer.getChildCount(); i++) {
|
||||
EntryEditCustomField entryEditCustomField = (EntryEditCustomField) entryExtraFieldsContainer.getChildAt(i);
|
||||
String key = entryEditCustomField.getLabel();
|
||||
if (key == null || key.length() == 0) {
|
||||
errorValidation.messageId = R.string.error_string_key;
|
||||
return errorValidation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errorValidation.isValidate = true;
|
||||
return errorValidation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch a validation with {@link #validate()} and show the error if present
|
||||
*
|
||||
* @return true if the form was validate or false if not
|
||||
*/
|
||||
protected boolean validateBeforeSaving() {
|
||||
ErrorValidation errorValidation = validate();
|
||||
errorValidation.showValidationErrorIfNeeded();
|
||||
return errorValidation.isValidate;
|
||||
}
|
||||
|
||||
protected PwEntry populateNewEntry() {
|
||||
PwDatabase db = App.getDB().getPwDatabase();
|
||||
|
||||
PwEntry newEntry = mEntry.clone();
|
||||
|
||||
newEntry.startToManageFieldReferences(db);
|
||||
|
||||
newEntry.createBackup(db);
|
||||
|
||||
newEntry.setLastAccessTime(new PwDate());
|
||||
newEntry.setLastModificationTime(new PwDate());
|
||||
|
||||
newEntry.setTitle(entryTitleView.getText().toString());
|
||||
newEntry.setIconStandard(retrieveIcon());
|
||||
|
||||
newEntry.setUrl(entryUrlView.getText().toString());
|
||||
newEntry.setUsername(entryUserNameView.getText().toString());
|
||||
newEntry.setNotes(entryCommentView.getText().toString());
|
||||
newEntry.setPassword(entryPasswordView.getText().toString());
|
||||
|
||||
if (newEntry.allowExtraFields()) {
|
||||
// Delete all extra strings
|
||||
newEntry.removeAllCustomFields();
|
||||
// Add extra fields from views
|
||||
for (int i = 0; i < entryExtraFieldsContainer.getChildCount(); i++) {
|
||||
EntryEditCustomField view = (EntryEditCustomField) entryExtraFieldsContainer.getChildAt(i);
|
||||
String key = view.getLabel();
|
||||
String value = view.getValue();
|
||||
boolean protect = view.isProtected();
|
||||
newEntry.addExtraField(key, new ProtectedString(protect, value));
|
||||
}
|
||||
}
|
||||
|
||||
newEntry.stopToManageFieldReferences();
|
||||
|
||||
return newEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the icon by the selection, or the first icon in the list if the entry is new or the last one
|
||||
*/
|
||||
private PwIconStandard retrieveIcon() {
|
||||
|
||||
if (!mSelectedIconStandard.isUnknown())
|
||||
return mSelectedIconStandard;
|
||||
else {
|
||||
if (mIsNew) {
|
||||
return database.getPwDatabase().getIconFactory().getKeyIcon();
|
||||
}
|
||||
else {
|
||||
// Keep previous icon, if no new one was selected
|
||||
return mEntry.getIconStandard();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
MenuUtil.contributionMenuInflater(inflater, menu);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch ( item.getItemId() ) {
|
||||
case R.id.menu_contribute:
|
||||
return MenuUtil.onContributionItemSelected(this);
|
||||
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void assignIconView() {
|
||||
database.getDrawFactory()
|
||||
.assignDatabaseIconTo(
|
||||
this,
|
||||
entryIconView,
|
||||
mEntry.getIcon(),
|
||||
iconColor);
|
||||
}
|
||||
|
||||
protected void fillData() {
|
||||
|
||||
assignIconView();
|
||||
|
||||
// Don't start the field reference manager, we want to see the raw ref
|
||||
mEntry.stopToManageFieldReferences();
|
||||
|
||||
entryTitleView.setText(mEntry.getTitle());
|
||||
entryUserNameView.setText(mEntry.getUsername());
|
||||
entryUrlView.setText(mEntry.getUrl());
|
||||
String password = mEntry.getPassword();
|
||||
entryPasswordView.setText(password);
|
||||
entryConfirmationPasswordView.setText(password);
|
||||
entryCommentView.setText(mEntry.getNotes());
|
||||
|
||||
boolean visibilityFontActivated = PreferencesUtil.fieldFontIsInVisibility(this);
|
||||
if (visibilityFontActivated) {
|
||||
Util.applyFontVisibilityTo(this, entryUserNameView);
|
||||
Util.applyFontVisibilityTo(this, entryPasswordView);
|
||||
Util.applyFontVisibilityTo(this, entryConfirmationPasswordView);
|
||||
Util.applyFontVisibilityTo(this, entryCommentView);
|
||||
}
|
||||
|
||||
if (mEntry.allowExtraFields()) {
|
||||
LinearLayout container = findViewById(R.id.advanced_container);
|
||||
mEntry.getFields().doActionToAllCustomProtectedField((key, value) -> {
|
||||
EntryEditCustomField entryEditCustomField = new EntryEditCustomField(EntryEditActivity.this);
|
||||
entryEditCustomField.setData(key, value);
|
||||
entryEditCustomField.setFontVisibility(visibilityFontActivated);
|
||||
container.addView(entryEditCustomField);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void iconPicked(Bundle bundle) {
|
||||
mSelectedIconStandard = bundle.getParcelable(KEY_ICON_STANDARD);
|
||||
mEntry.setIconStandard(mSelectedIconStandard);
|
||||
assignIconView();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
if (!mSelectedIconStandard.isUnknown()) {
|
||||
outState.putParcelable(KEY_ICON_STANDARD, mSelectedIconStandard);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void acceptPassword(Bundle bundle) {
|
||||
String generatedPassword = bundle.getString(GeneratePasswordDialogFragment.KEY_PASSWORD_ID);
|
||||
entryPasswordView.setText(generatedPassword);
|
||||
entryConfirmationPasswordView.setText(generatedPassword);
|
||||
|
||||
checkAndPerformedEducation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelPassword(Bundle bundle) {
|
||||
// Do nothing here
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
// Assign entry callback as a result in all case
|
||||
try {
|
||||
if (mCallbackNewEntry != null) {
|
||||
Bundle bundle = new Bundle();
|
||||
Intent intentEntry = new Intent();
|
||||
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, mCallbackNewEntry);
|
||||
intentEntry.putExtras(bundle);
|
||||
if (mIsNew) {
|
||||
setResult(ADD_ENTRY_RESULT_CODE, intentEntry);
|
||||
} else {
|
||||
setResult(UPDATE_ENTRY_RESULT_CODE, intentEntry);
|
||||
}
|
||||
}
|
||||
super.finish();
|
||||
} catch (Exception e) {
|
||||
// Exception when parcelable can't be done
|
||||
Log.e(TAG, "Cant add entry as result", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final class AfterSave extends AfterActionNodeOnFinish {
|
||||
|
||||
@Override
|
||||
public void run(@Nullable PwNode oldNode, @Nullable PwNode newNode) {
|
||||
runOnUiThread(() -> {
|
||||
if ( mSuccess ) {
|
||||
finish();
|
||||
} else {
|
||||
displayMessage(EntryEditActivity.this);
|
||||
}
|
||||
|
||||
SaveDatabaseProgressTaskDialogFragment.stop(EntryEditActivity.this);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ScrollView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
|
||||
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
|
||||
import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread
|
||||
import com.kunzisoft.keepass.database.action.node.ActionNodeValues
|
||||
import com.kunzisoft.keepass.database.action.node.AddEntryRunnable
|
||||
import com.kunzisoft.keepass.database.action.node.AfterActionNodeFinishRunnable
|
||||
import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.view.EntryEditContentsView
|
||||
|
||||
class EntryEditActivity : LockingHideActivity(),
|
||||
IconPickerDialogFragment.IconPickerListener,
|
||||
GeneratePasswordDialogFragment.GeneratePasswordListener {
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
|
||||
// Refs of an entry and group in database, are not modifiable
|
||||
private var mEntry: EntryVersioned? = null
|
||||
private var mParent: GroupVersioned? = null
|
||||
// New or copy of mEntry in the database to be modifiable
|
||||
private var mNewEntry: EntryVersioned? = null
|
||||
private var mIsNew: Boolean = false
|
||||
|
||||
// Views
|
||||
private var scrollView: ScrollView? = null
|
||||
|
||||
private var entryEditContentsView: EntryEditContentsView? = null
|
||||
|
||||
private var saveView: View? = null
|
||||
|
||||
// Education
|
||||
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_entry_edit)
|
||||
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
scrollView = findViewById(R.id.entry_edit_scroll)
|
||||
scrollView?.scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
||||
|
||||
entryEditContentsView = findViewById(R.id.entry_edit_contents)
|
||||
entryEditContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
||||
// Focus view to reinitialize timeout
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(entryEditContentsView)
|
||||
|
||||
// Likely the app has been killed exit the activity
|
||||
mDatabase = Database.getInstance()
|
||||
|
||||
// Entry is retrieve, it's an entry to update
|
||||
intent.getParcelableExtra<PwNodeId<*>>(KEY_ENTRY)?.let {
|
||||
mIsNew = false
|
||||
// Create an Entry copy to modify from the database entry
|
||||
mEntry = mDatabase?.getEntryById(it)
|
||||
|
||||
// Retrieve the parent
|
||||
mEntry?.let { entry ->
|
||||
mParent = entry.parent
|
||||
// If no parent, add root group as parent
|
||||
if (mParent == null) {
|
||||
mParent = mDatabase?.rootGroup
|
||||
entry.parent = mParent
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve the icon after an orientation change
|
||||
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
||||
mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY) as EntryVersioned
|
||||
} else {
|
||||
mEntry?.let { entry ->
|
||||
// Create a copy to modify
|
||||
mNewEntry = EntryVersioned(entry).also { newEntry ->
|
||||
|
||||
// WARNING Remove the parent to keep memory with parcelable
|
||||
newEntry.parent = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parent is retrieve, it's a new entry to create
|
||||
intent.getParcelableExtra<PwNodeId<*>>(KEY_PARENT)?.let {
|
||||
mIsNew = true
|
||||
mNewEntry = mDatabase?.createEntry()
|
||||
mParent = mDatabase?.getGroupById(it)
|
||||
// Add the default icon
|
||||
mDatabase?.drawFactory?.let { iconFactory ->
|
||||
entryEditContentsView?.setDefaultIcon(iconFactory)
|
||||
}
|
||||
}
|
||||
|
||||
// Close the activity if entry or parent can't be retrieve
|
||||
if (mNewEntry == null || mParent == null) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
populateViewsWithEntry(mNewEntry!!)
|
||||
|
||||
// Assign title
|
||||
title = if (mIsNew) getString(R.string.add_entry) else getString(R.string.edit_entry)
|
||||
|
||||
// Add listener to the icon
|
||||
entryEditContentsView?.setOnIconViewClickListener { IconPickerDialogFragment.launch(this@EntryEditActivity) }
|
||||
|
||||
// Generate password button
|
||||
entryEditContentsView?.setOnPasswordGeneratorClickListener { openPasswordGenerator() }
|
||||
|
||||
// Save button
|
||||
saveView = findViewById(R.id.entry_edit_save)
|
||||
saveView?.setOnClickListener { saveEntry() }
|
||||
|
||||
entryEditContentsView?.allowCustomField(mNewEntry?.allowCustomFields() == true) { addNewCustomField() }
|
||||
|
||||
// Verify the education views
|
||||
entryEditActivityEducation = EntryEditActivityEducation(this)
|
||||
}
|
||||
|
||||
private fun populateViewsWithEntry(newEntry: EntryVersioned) {
|
||||
// Don't start the field reference manager, we want to see the raw ref
|
||||
mDatabase?.stopManageEntry(newEntry)
|
||||
|
||||
// Set info in temp parameters
|
||||
temporarilySaveAndShowSelectedIcon(newEntry.icon)
|
||||
|
||||
// Set info in view
|
||||
entryEditContentsView?.apply {
|
||||
title = newEntry.title
|
||||
username = newEntry.username
|
||||
url = newEntry.url
|
||||
password = newEntry.password
|
||||
notes = newEntry.notes
|
||||
for (entry in newEntry.customFields.entries) {
|
||||
addNewCustomField(entry.key, entry.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateEntryWithViews(newEntry: EntryVersioned) {
|
||||
|
||||
mDatabase?.startManageEntry(newEntry)
|
||||
|
||||
newEntry.apply {
|
||||
// Build info from view
|
||||
entryEditContentsView?.let { entryView ->
|
||||
title = entryView.title
|
||||
username = entryView.username
|
||||
url = entryView.url
|
||||
password = entryView.password
|
||||
notes = entryView.notes
|
||||
entryView.customFields.forEach { customField ->
|
||||
addExtraField(customField.name, customField.protectedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mDatabase?.stopManageEntry(newEntry)
|
||||
}
|
||||
|
||||
private fun temporarilySaveAndShowSelectedIcon(icon: PwIcon) {
|
||||
mNewEntry?.icon = icon
|
||||
mDatabase?.drawFactory?.let { iconDrawFactory ->
|
||||
entryEditContentsView?.setIcon(iconDrawFactory, icon)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the password generator fragment
|
||||
*/
|
||||
private fun openPasswordGenerator() {
|
||||
GeneratePasswordDialogFragment().show(supportFragmentManager, "PasswordGeneratorFragment")
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new customized field view and scroll to bottom
|
||||
*/
|
||||
private fun addNewCustomField() {
|
||||
entryEditContentsView?.addNewCustomField()
|
||||
// Scroll bottom
|
||||
scrollView?.post { scrollView?.fullScroll(ScrollView.FOCUS_DOWN) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the new entry or update an existing entry in the database
|
||||
*/
|
||||
private fun saveEntry() {
|
||||
|
||||
// Launch a validation and show the error if present
|
||||
if (entryEditContentsView?.isValid() == true) {
|
||||
// Clone the entry
|
||||
mDatabase?.let { database ->
|
||||
mNewEntry?.let { newEntry ->
|
||||
|
||||
// WARNING Add the parent previously deleted
|
||||
newEntry.parent = mEntry?.parent
|
||||
// Build info
|
||||
newEntry.lastAccessTime = PwDate()
|
||||
newEntry.lastModificationTime = PwDate()
|
||||
|
||||
populateEntryWithViews(newEntry)
|
||||
|
||||
// Open a progress dialog and save entry
|
||||
var actionRunnable: ActionRunnable? = null
|
||||
val afterActionNodeFinishRunnable = object : AfterActionNodeFinishRunnable() {
|
||||
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
||||
if (actionNodeValues.result.isSuccess)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
if (mIsNew) {
|
||||
mParent?.let { parent ->
|
||||
actionRunnable = AddEntryRunnable(this@EntryEditActivity,
|
||||
database,
|
||||
newEntry,
|
||||
parent,
|
||||
afterActionNodeFinishRunnable,
|
||||
!mReadOnly)
|
||||
}
|
||||
|
||||
} else {
|
||||
mEntry?.let { oldEntry ->
|
||||
actionRunnable = UpdateEntryRunnable(this@EntryEditActivity,
|
||||
database,
|
||||
oldEntry,
|
||||
newEntry,
|
||||
afterActionNodeFinishRunnable,
|
||||
!mReadOnly)
|
||||
}
|
||||
}
|
||||
actionRunnable?.let { runnable ->
|
||||
ProgressDialogSaveDatabaseThread(this@EntryEditActivity) { runnable }.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
val inflater = menuInflater
|
||||
inflater.inflate(R.menu.database_lock, menu)
|
||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||
|
||||
entryEditActivityEducation?.let {
|
||||
Handler().post { performedNextEducation(it) }
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
||||
val passwordView = entryEditContentsView?.generatePasswordView
|
||||
val addNewFieldView = entryEditContentsView?.addNewFieldView
|
||||
|
||||
val generatePasswordEducationPerformed = passwordView != null
|
||||
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
||||
passwordView,
|
||||
{
|
||||
openPasswordGenerator()
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryEditActivityEducation)
|
||||
}
|
||||
)
|
||||
if (!generatePasswordEducationPerformed) {
|
||||
// entryNewFieldEducationPerformed
|
||||
mNewEntry != null && mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty()
|
||||
&& addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE
|
||||
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
|
||||
addNewFieldView,
|
||||
{
|
||||
addNewCustomField()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_lock -> {
|
||||
lockAndExit()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_contribute -> {
|
||||
MenuUtil.onContributionItemSelected(this)
|
||||
return true
|
||||
}
|
||||
|
||||
android.R.id.home -> finish()
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun iconPicked(bundle: Bundle) {
|
||||
IconPickerDialogFragment.getIconStandardFromBundle(bundle)?.let { icon ->
|
||||
temporarilySaveAndShowSelectedIcon(icon)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putParcelable(KEY_NEW_ENTRY, mNewEntry)
|
||||
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun acceptPassword(bundle: Bundle) {
|
||||
bundle.getString(GeneratePasswordDialogFragment.KEY_PASSWORD_ID)?.let {
|
||||
entryEditContentsView?.password = it
|
||||
}
|
||||
|
||||
entryEditActivityEducation?.let {
|
||||
Handler().post { performedNextEducation(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancelPassword(bundle: Bundle) {
|
||||
// Do nothing here
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
// Assign entry callback as a result in all case
|
||||
try {
|
||||
mNewEntry?.let {
|
||||
val bundle = Bundle()
|
||||
val intentEntry = Intent()
|
||||
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, mNewEntry)
|
||||
intentEntry.putExtras(bundle)
|
||||
if (mIsNew) {
|
||||
setResult(ADD_ENTRY_RESULT_CODE, intentEntry)
|
||||
} else {
|
||||
setResult(UPDATE_ENTRY_RESULT_CODE, intentEntry)
|
||||
}
|
||||
}
|
||||
super.finish()
|
||||
} catch (e: Exception) {
|
||||
// Exception when parcelable can't be done
|
||||
Log.e(TAG, "Cant add entry as result", e)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = EntryEditActivity::class.java.name
|
||||
|
||||
// Keys for current Activity
|
||||
const val KEY_ENTRY = "entry"
|
||||
const val KEY_PARENT = "parent"
|
||||
|
||||
// SaveInstanceState
|
||||
const val KEY_NEW_ENTRY = "new_entry"
|
||||
|
||||
// Keys for callback
|
||||
const val ADD_ENTRY_RESULT_CODE = 31
|
||||
const val UPDATE_ENTRY_RESULT_CODE = 32
|
||||
const val ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129
|
||||
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to update an existing entry
|
||||
*
|
||||
* @param activity from activity
|
||||
* @param pwEntry Entry to update
|
||||
*/
|
||||
fun launch(activity: Activity, pwEntry: EntryVersioned) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_ENTRY, pwEntry.nodeId)
|
||||
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch EntryEditActivity to add a new entry
|
||||
*
|
||||
* @param activity from activity
|
||||
* @param pwGroup Group who will contains new entry
|
||||
*/
|
||||
fun launch(activity: Activity, pwGroup: GroupVersioned) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||
intent.putExtra(KEY_PARENT, pwGroup.nodeId)
|
||||
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.assist.AssistStructure
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.preference.PreferenceManager
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.database.action.CreateDatabaseRunnable
|
||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import kotlinx.android.synthetic.main.activity_file_selection.*
|
||||
import net.cachapa.expandablelayout.ExpandableLayout
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
class FileDatabaseSelectActivity : StylishActivity(),
|
||||
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
||||
|
||||
// Views
|
||||
private var fileListContainer: View? = null
|
||||
private var createButtonView: View? = null
|
||||
private var browseButtonView: View? = null
|
||||
private var openButtonView: View? = null
|
||||
private var fileSelectExpandableButtonView: View? = null
|
||||
private var fileSelectExpandableLayout: ExpandableLayout? = null
|
||||
private var openFileNameView: EditText? = null
|
||||
|
||||
// Adapter to manage database history list
|
||||
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
|
||||
|
||||
private var mFileDatabaseHistoryAction: FileDatabaseHistoryAction? = null
|
||||
|
||||
private var mDatabaseFileUri: Uri? = null
|
||||
|
||||
private var mOpenFileHelper: OpenFileHelper? = null
|
||||
|
||||
private var mDefaultPath: String? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
|
||||
|
||||
setContentView(R.layout.activity_file_selection)
|
||||
fileListContainer = findViewById(R.id.container_file_list)
|
||||
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
toolbar.title = ""
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
openFileNameView = findViewById(R.id.file_filename)
|
||||
|
||||
// Set the initial value of the filename
|
||||
mDefaultPath = (Environment.getExternalStorageDirectory().absolutePath
|
||||
+ getString(R.string.database_file_path_default)
|
||||
+ getString(R.string.database_file_name_default)
|
||||
+ getString(R.string.database_file_extension_default))
|
||||
openFileNameView?.setHint(R.string.open_link_database)
|
||||
|
||||
// Button to expand file selection
|
||||
fileSelectExpandableButtonView = findViewById(R.id.file_select_expandable_button)
|
||||
fileSelectExpandableLayout = findViewById(R.id.file_select_expandable)
|
||||
fileSelectExpandableButtonView?.setOnClickListener { _ ->
|
||||
if (fileSelectExpandableLayout?.isExpanded == true)
|
||||
fileSelectExpandableLayout?.collapse()
|
||||
else
|
||||
fileSelectExpandableLayout?.expand()
|
||||
}
|
||||
|
||||
// Open button
|
||||
openButtonView = findViewById(R.id.open_database)
|
||||
openButtonView?.setOnClickListener { _ ->
|
||||
var fileName = openFileNameView?.text?.toString() ?: ""
|
||||
mDefaultPath?.let {
|
||||
if (fileName.isEmpty())
|
||||
fileName = it
|
||||
}
|
||||
UriUtil.parse(fileName)?.let { fileNameUri ->
|
||||
launchPasswordActivityWithPath(fileNameUri)
|
||||
} ?: run {
|
||||
Log.e(TAG, "Unable to open the database link")
|
||||
Snackbar.make(activity_file_selection_coordinator_layout, getString(R.string.error_can_not_handle_uri), Snackbar.LENGTH_LONG).asError().show()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// Create button
|
||||
createButtonView = findViewById(R.id.create_database)
|
||||
if (Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/x-keepass"
|
||||
}.resolveActivity(packageManager) == null) {
|
||||
// No Activity found that can handle this intent.
|
||||
createButtonView?.visibility = View.GONE
|
||||
}
|
||||
else{
|
||||
// There is an activity which can handle this intent.
|
||||
createButtonView?.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
createButtonView?.setOnClickListener { createNewFile() }
|
||||
|
||||
mOpenFileHelper = OpenFileHelper(this)
|
||||
browseButtonView = findViewById(R.id.browse_button)
|
||||
browseButtonView?.setOnClickListener(mOpenFileHelper!!.getOpenFileOnClickViewListener {
|
||||
UriUtil.parse(openFileNameView?.text?.toString())
|
||||
})
|
||||
|
||||
// History list
|
||||
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
||||
fileDatabaseHistoryRecyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
|
||||
// Removes blinks
|
||||
(fileDatabaseHistoryRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
// Construct adapter with listeners
|
||||
mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this)
|
||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
||||
UriUtil.parse(fileDatabaseHistoryEntityToOpen.databaseUri)?.let { databaseFileUri ->
|
||||
launchPasswordActivity(
|
||||
databaseFileUri,
|
||||
UriUtil.parse(fileDatabaseHistoryEntityToOpen.keyFileUri))
|
||||
}
|
||||
updateFileListVisibility()
|
||||
}
|
||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
||||
// Remove from app database
|
||||
mFileDatabaseHistoryAction?.deleteFileDatabaseHistory(fileDatabaseHistoryToDelete) { fileHistoryDeleted ->
|
||||
// Remove from adapter
|
||||
fileHistoryDeleted?.let { databaseFileHistoryDeleted ->
|
||||
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileHistoryDeleted)
|
||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
updateFileListVisibility()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
mAdapterDatabaseHistory?.setOnSaveAliasListener { fileDatabaseHistoryWithNewAlias ->
|
||||
mFileDatabaseHistoryAction?.addOrUpdateFileDatabaseHistory(fileDatabaseHistoryWithNewAlias)
|
||||
}
|
||||
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
|
||||
|
||||
// Load default database if not an orientation change
|
||||
if (!(savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(EXTRA_STAY)
|
||||
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val databasePath = prefs.getString(PasswordActivity.KEY_DEFAULT_DATABASE_PATH, "")
|
||||
|
||||
UriUtil.parse(databasePath)?.let { databaseFileUri ->
|
||||
launchPasswordActivityWithPath(databaseFileUri)
|
||||
} ?: run {
|
||||
Log.i(TAG, "Unable to launch Password Activity")
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve the database URI provided by file manager after an orientation change
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(EXTRA_DATABASE_URI)) {
|
||||
mDatabaseFileUri = savedInstanceState.getParcelable(EXTRA_DATABASE_URI)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new file by calling the content provider
|
||||
*/
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun createNewFile() {
|
||||
try {
|
||||
startActivityForResult(Intent(
|
||||
Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/x-keepass"
|
||||
putExtra(Intent.EXTRA_TITLE, getString(R.string.database_file_name_default) +
|
||||
getString(R.string.database_file_extension_default))
|
||||
},
|
||||
CREATE_FILE_REQUEST_CODE)
|
||||
} catch (e: Exception) {
|
||||
BrowserDialogFragment().show(supportFragmentManager, "browserDialog")
|
||||
}
|
||||
}
|
||||
|
||||
private fun fileNoFoundAction(e: FileNotFoundException) {
|
||||
val error = getString(R.string.file_not_found_content)
|
||||
Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||
Log.e(TAG, error, e)
|
||||
}
|
||||
|
||||
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||
{
|
||||
try {
|
||||
PasswordActivity.launch(this@FileDatabaseSelectActivity,
|
||||
databaseUri, keyFile)
|
||||
} catch (e: FileNotFoundException) {
|
||||
fileNoFoundAction(e)
|
||||
}
|
||||
},
|
||||
{
|
||||
try {
|
||||
PasswordActivity.launchForKeyboardResult(this@FileDatabaseSelectActivity,
|
||||
databaseUri, keyFile)
|
||||
finish()
|
||||
} catch (e: FileNotFoundException) {
|
||||
fileNoFoundAction(e)
|
||||
}
|
||||
},
|
||||
{ assistStructure ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
try {
|
||||
PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
||||
databaseUri, keyFile,
|
||||
assistStructure)
|
||||
} catch (e: FileNotFoundException) {
|
||||
fileNoFoundAction(e)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
||||
launchPasswordActivity(databaseUri, null)
|
||||
// Delete flickering for kitkat <=
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
||||
overridePendingTransition(0, 0)
|
||||
}
|
||||
|
||||
private fun updateExternalStorageWarning() {
|
||||
// To show errors
|
||||
var warning = -1
|
||||
val state = Environment.getExternalStorageState()
|
||||
if (state == Environment.MEDIA_MOUNTED_READ_ONLY) {
|
||||
warning = R.string.read_only_warning
|
||||
} else if (state != Environment.MEDIA_MOUNTED) {
|
||||
warning = R.string.warning_unmounted
|
||||
}
|
||||
|
||||
val labelWarningView = findViewById<TextView>(R.id.label_warning)
|
||||
if (warning != -1) {
|
||||
labelWarningView.setText(warning)
|
||||
labelWarningView.visibility = View.VISIBLE
|
||||
} else {
|
||||
labelWarningView.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
updateExternalStorageWarning()
|
||||
|
||||
// Construct adapter with listeners
|
||||
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
|
||||
databaseFileHistoryList?.let {
|
||||
mAdapterDatabaseHistory?.addDatabaseFileHistoryList(it)
|
||||
updateFileListVisibility()
|
||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
// only to keep the current activity
|
||||
outState.putBoolean(EXTRA_STAY, true)
|
||||
// to retrieve the URI of a created database after an orientation change
|
||||
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
||||
}
|
||||
|
||||
private fun updateFileListVisibility() {
|
||||
if (mAdapterDatabaseHistory?.itemCount == 0)
|
||||
fileListContainer?.visibility = View.INVISIBLE
|
||||
else
|
||||
fileListContainer?.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
override fun onAssignKeyDialogPositiveClick(
|
||||
masterPasswordChecked: Boolean, masterPassword: String?,
|
||||
keyFileChecked: Boolean, keyFile: Uri?) {
|
||||
|
||||
try {
|
||||
mDatabaseFileUri?.let { databaseUri ->
|
||||
|
||||
// Create the new database
|
||||
ProgressDialogThread(this@FileDatabaseSelectActivity,
|
||||
{
|
||||
CreateDatabaseRunnable(this@FileDatabaseSelectActivity,
|
||||
databaseUri,
|
||||
Database.getInstance(),
|
||||
masterPasswordChecked,
|
||||
masterPassword,
|
||||
keyFileChecked,
|
||||
keyFile,
|
||||
true, // TODO get readonly
|
||||
LaunchGroupActivityFinish(databaseUri, keyFile)
|
||||
)
|
||||
},
|
||||
R.string.progress_create)
|
||||
.start()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
val error = getString(R.string.error_create_database_file)
|
||||
Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||
Log.e(TAG, error, e)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class LaunchGroupActivityFinish(private val databaseFileUri: Uri,
|
||||
private val keyFileUri: Uri?) : ActionRunnable() {
|
||||
|
||||
override fun run() {
|
||||
finishRun(true, null)
|
||||
}
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
runOnUiThread {
|
||||
if (result.isSuccess) {
|
||||
// Add database to recent files
|
||||
mFileDatabaseHistoryAction?.addOrUpdateDatabaseUri(databaseFileUri, keyFileUri)
|
||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||
updateFileListVisibility()
|
||||
GroupActivity.launch(this@FileDatabaseSelectActivity)
|
||||
} else {
|
||||
Log.e(TAG, "Unable to open the database")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAssignKeyDialogNegativeClick(
|
||||
masterPasswordChecked: Boolean, masterPassword: String?,
|
||||
keyFileChecked: Boolean, keyFile: Uri?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
if (PreferencesUtil.autoOpenSelectedFile(this@FileDatabaseSelectActivity)) {
|
||||
launchPasswordActivityWithPath(uri)
|
||||
} else {
|
||||
fileSelectExpandableLayout?.expand(false)
|
||||
openFileNameView?.setText(uri.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve the created URI from the file manager
|
||||
if (requestCode == CREATE_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
|
||||
mDatabaseFileUri = data?.data
|
||||
if (mDatabaseFileUri != null) {
|
||||
AssignMasterKeyDialogFragment().show(supportFragmentManager, "passwordDialog")
|
||||
}
|
||||
// else {
|
||||
// TODO Show error
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
MenuUtil.defaultMenuInflater(menuInflater, menu)
|
||||
|
||||
Handler().post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
|
||||
// If no recent files
|
||||
val createDatabaseEducationPerformed = createButtonView != null && createButtonView!!.visibility == View.VISIBLE
|
||||
&& mAdapterDatabaseHistory != null
|
||||
&& mAdapterDatabaseHistory!!.itemCount > 0
|
||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
||||
createButtonView!!,
|
||||
{
|
||||
createNewFile()
|
||||
},
|
||||
{
|
||||
// But if the user cancel, it can also select a database
|
||||
performedNextEducation(fileDatabaseSelectActivityEducation)
|
||||
})
|
||||
if (!createDatabaseEducationPerformed) {
|
||||
// selectDatabaseEducationPerformed
|
||||
browseButtonView != null
|
||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
||||
browseButtonView!!,
|
||||
{tapTargetView ->
|
||||
tapTargetView?.let {
|
||||
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it)
|
||||
}
|
||||
},
|
||||
{
|
||||
fileSelectExpandableButtonView?.let {
|
||||
fileDatabaseSelectActivityEducation
|
||||
.checkAndPerformedOpenLinkDatabaseEducation(it)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "FileDbSelectActivity"
|
||||
private const val EXTRA_STAY = "EXTRA_STAY"
|
||||
private const val EXTRA_DATABASE_URI = "EXTRA_DATABASE_URI"
|
||||
|
||||
private const val CREATE_FILE_REQUEST_CODE = 3853
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* No Standard Launch, pass by PasswordActivity
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Keyboard Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
fun launchForKeyboardSelection(activity: Activity) {
|
||||
EntrySelectionHelper.startActivityForEntrySelection(activity, Intent(activity, FileDatabaseSelectActivity::class.java))
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Autofill Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
fun launchForAutofillResult(activity: Activity, assistStructure: AssistStructure) {
|
||||
AutofillHelper.startActivityForAutofillResult(activity,
|
||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||
assistStructure)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,992 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.SearchManager
|
||||
import android.app.assist.AssistStructure
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
|
||||
import com.kunzisoft.keepass.activities.dialogs.ReadOnlyDialog
|
||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.adapters.NodeAdapter
|
||||
import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.database.SortNodeEnum
|
||||
import com.kunzisoft.keepass.database.action.ProgressDialogSaveDatabaseThread
|
||||
import com.kunzisoft.keepass.database.action.node.*
|
||||
import com.kunzisoft.keepass.database.action.node.ActionNodeDatabaseRunnable.Companion.NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.education.GroupActivityEducation
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.view.AddNodeButtonView
|
||||
import net.cachapa.expandablelayout.ExpandableLayout
|
||||
|
||||
class GroupActivity : LockingActivity(),
|
||||
GroupEditDialogFragment.EditGroupListener,
|
||||
IconPickerDialogFragment.IconPickerListener,
|
||||
NodeAdapter.NodeMenuListener,
|
||||
ListNodesFragment.OnScrollListener,
|
||||
NodeAdapter.NodeClickCallback,
|
||||
SortDialogFragment.SortSelectionListener {
|
||||
|
||||
// Views
|
||||
private var toolbar: Toolbar? = null
|
||||
private var searchTitleView: View? = null
|
||||
private var toolbarPasteExpandableLayout: ExpandableLayout? = null
|
||||
private var toolbarPaste: Toolbar? = null
|
||||
private var iconView: ImageView? = null
|
||||
private var modeTitleView: TextView? = null
|
||||
private var addNodeButtonView: AddNodeButtonView? = null
|
||||
private var groupNameView: TextView? = null
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
|
||||
private var mListNodesFragment: ListNodesFragment? = null
|
||||
private var mCurrentGroupIsASearch: Boolean = false
|
||||
|
||||
// Nodes
|
||||
private var mRootGroup: GroupVersioned? = null
|
||||
private var mCurrentGroup: GroupVersioned? = null
|
||||
private var mOldGroupToUpdate: GroupVersioned? = null
|
||||
private var mNodeToCopy: NodeVersioned? = null
|
||||
private var mNodeToMove: NodeVersioned? = null
|
||||
|
||||
private var mSearchSuggestionAdapter: SearchEntryCursorAdapter? = null
|
||||
|
||||
private var mIconColor: Int = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (isFinishing) {
|
||||
return
|
||||
}
|
||||
mDatabase = Database.getInstance()
|
||||
|
||||
// Construct main view
|
||||
setContentView(layoutInflater.inflate(R.layout.activity_group, null))
|
||||
|
||||
// Initialize views
|
||||
iconView = findViewById(R.id.icon)
|
||||
addNodeButtonView = findViewById(R.id.add_node_button)
|
||||
toolbar = findViewById(R.id.toolbar)
|
||||
searchTitleView = findViewById(R.id.search_title)
|
||||
groupNameView = findViewById(R.id.group_name)
|
||||
toolbarPasteExpandableLayout = findViewById(R.id.expandable_toolbar_paste_layout)
|
||||
toolbarPaste = findViewById(R.id.toolbar_paste)
|
||||
modeTitleView = findViewById(R.id.mode_title_view)
|
||||
|
||||
// Focus view to reinitialize timeout
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(addNodeButtonView)
|
||||
|
||||
// Retrieve elements after an orientation change
|
||||
if (savedInstanceState != null) {
|
||||
if (savedInstanceState.containsKey(OLD_GROUP_TO_UPDATE_KEY))
|
||||
mOldGroupToUpdate = savedInstanceState.getParcelable(OLD_GROUP_TO_UPDATE_KEY)
|
||||
if (savedInstanceState.containsKey(NODE_TO_COPY_KEY)) {
|
||||
mNodeToCopy = savedInstanceState.getParcelable(NODE_TO_COPY_KEY)
|
||||
toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener())
|
||||
} else if (savedInstanceState.containsKey(NODE_TO_MOVE_KEY)) {
|
||||
mNodeToMove = savedInstanceState.getParcelable(NODE_TO_MOVE_KEY)
|
||||
toolbarPaste?.setOnMenuItemClickListener(OnMoveMenuItemClickListener())
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
mRootGroup = mDatabase?.rootGroup
|
||||
} catch (e: NullPointerException) {
|
||||
Log.e(TAG, "Unable to get rootGroup")
|
||||
}
|
||||
|
||||
mCurrentGroup = retrieveCurrentGroup(intent, savedInstanceState)
|
||||
mCurrentGroupIsASearch = Intent.ACTION_SEARCH == intent.action
|
||||
|
||||
Log.i(TAG, "Started creating tree")
|
||||
if (mCurrentGroup == null) {
|
||||
Log.w(TAG, "Group was null")
|
||||
return
|
||||
}
|
||||
|
||||
// Update last access time.
|
||||
mCurrentGroup?.touch(modified = false, touchParents = false)
|
||||
|
||||
toolbar?.title = ""
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
toolbarPaste?.inflateMenu(R.menu.node_paste_menu)
|
||||
toolbarPaste?.setNavigationIcon(R.drawable.ic_arrow_left_white_24dp)
|
||||
toolbarPaste?.setNavigationOnClickListener {
|
||||
toolbarPasteExpandableLayout?.collapse()
|
||||
mNodeToCopy = null
|
||||
mNodeToMove = null
|
||||
}
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val taTextColor = theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
||||
mIconColor = taTextColor.getColor(0, Color.WHITE)
|
||||
taTextColor.recycle()
|
||||
|
||||
var fragmentTag = LIST_NODES_FRAGMENT_TAG
|
||||
if (mCurrentGroupIsASearch)
|
||||
fragmentTag = SEARCH_FRAGMENT_TAG
|
||||
|
||||
// Initialize the fragment with the list
|
||||
mListNodesFragment = supportFragmentManager.findFragmentByTag(fragmentTag) as ListNodesFragment?
|
||||
if (mListNodesFragment == null)
|
||||
mListNodesFragment = ListNodesFragment.newInstance(mCurrentGroup, mReadOnly, mCurrentGroupIsASearch)
|
||||
|
||||
// Attach fragment to content view
|
||||
supportFragmentManager.beginTransaction().replace(
|
||||
R.id.nodes_list_fragment_container,
|
||||
mListNodesFragment!!,
|
||||
fragmentTag)
|
||||
.commit()
|
||||
|
||||
// Add listeners to the add buttons
|
||||
addNodeButtonView?.setAddGroupClickListener(View.OnClickListener {
|
||||
GroupEditDialogFragment.build()
|
||||
.show(supportFragmentManager,
|
||||
GroupEditDialogFragment.TAG_CREATE_GROUP)
|
||||
})
|
||||
addNodeButtonView?.setAddEntryClickListener(View.OnClickListener {
|
||||
mCurrentGroup?.let { currentGroup ->
|
||||
EntryEditActivity.launch(this@GroupActivity, currentGroup)
|
||||
}
|
||||
})
|
||||
|
||||
// Search suggestion
|
||||
mDatabase?.let { database ->
|
||||
mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, database)
|
||||
}
|
||||
|
||||
Log.i(TAG, "Finished creating tree")
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
|
||||
intent?.let { intentNotNull ->
|
||||
Log.d(TAG, "setNewIntent: $intentNotNull")
|
||||
setIntent(intentNotNull)
|
||||
mCurrentGroupIsASearch = if (Intent.ACTION_SEARCH == intentNotNull.action) {
|
||||
// only one instance of search in backstack
|
||||
openSearchGroup(retrieveCurrentGroup(intentNotNull, null))
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openSearchGroup(group: GroupVersioned?) {
|
||||
// Delete the previous search fragment
|
||||
val searchFragment = supportFragmentManager.findFragmentByTag(SEARCH_FRAGMENT_TAG)
|
||||
if (searchFragment != null) {
|
||||
if (supportFragmentManager
|
||||
.popBackStackImmediate(SEARCH_FRAGMENT_TAG,
|
||||
FragmentManager.POP_BACK_STACK_INCLUSIVE))
|
||||
supportFragmentManager.beginTransaction().remove(searchFragment).commit()
|
||||
}
|
||||
openGroup(group, true)
|
||||
}
|
||||
|
||||
private fun openChildGroup(group: GroupVersioned) {
|
||||
openGroup(group, false)
|
||||
}
|
||||
|
||||
private fun openGroup(group: GroupVersioned?, isASearch: Boolean) {
|
||||
// Check TimeoutHelper
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
|
||||
// Open a group in a new fragment
|
||||
val newListNodeFragment = ListNodesFragment.newInstance(group, mReadOnly, isASearch)
|
||||
val fragmentTransaction = supportFragmentManager.beginTransaction()
|
||||
// Different animation
|
||||
val fragmentTag: String
|
||||
fragmentTag = if (isASearch) {
|
||||
fragmentTransaction.setCustomAnimations(R.anim.slide_in_top, R.anim.slide_out_bottom,
|
||||
R.anim.slide_in_bottom, R.anim.slide_out_top)
|
||||
SEARCH_FRAGMENT_TAG
|
||||
} else {
|
||||
fragmentTransaction.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left,
|
||||
R.anim.slide_in_left, R.anim.slide_out_right)
|
||||
LIST_NODES_FRAGMENT_TAG
|
||||
}
|
||||
|
||||
fragmentTransaction.replace(R.id.nodes_list_fragment_container,
|
||||
newListNodeFragment,
|
||||
fragmentTag)
|
||||
fragmentTransaction.addToBackStack(fragmentTag)
|
||||
fragmentTransaction.commit()
|
||||
|
||||
mListNodesFragment = newListNodeFragment
|
||||
mCurrentGroup = group
|
||||
assignGroupViewElements()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
mCurrentGroup?.let {
|
||||
outState.putParcelable(GROUP_ID_KEY, it.nodeId)
|
||||
}
|
||||
mOldGroupToUpdate?.let {
|
||||
outState.putParcelable(OLD_GROUP_TO_UPDATE_KEY, it)
|
||||
}
|
||||
mNodeToCopy?.let {
|
||||
outState.putParcelable(NODE_TO_COPY_KEY, it)
|
||||
}
|
||||
mNodeToMove?.let {
|
||||
outState.putParcelable(NODE_TO_MOVE_KEY, it)
|
||||
}
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
private fun retrieveCurrentGroup(intent: Intent, savedInstanceState: Bundle?): GroupVersioned? {
|
||||
|
||||
// Force read only if the database is like that
|
||||
mReadOnly = mDatabase?.isReadOnly == true || mReadOnly
|
||||
|
||||
// If it's a search
|
||||
if (Intent.ACTION_SEARCH == intent.action) {
|
||||
return mDatabase?.search(intent.getStringExtra(SearchManager.QUERY).trim { it <= ' ' })
|
||||
}
|
||||
// else a real group
|
||||
else {
|
||||
var pwGroupId: PwNodeId<*>? = null
|
||||
if (savedInstanceState != null && savedInstanceState.containsKey(GROUP_ID_KEY)) {
|
||||
pwGroupId = savedInstanceState.getParcelable(GROUP_ID_KEY)
|
||||
} else {
|
||||
if (getIntent() != null)
|
||||
pwGroupId = intent.getParcelableExtra(GROUP_ID_KEY)
|
||||
}
|
||||
|
||||
Log.w(TAG, "Creating tree view")
|
||||
val currentGroup: GroupVersioned?
|
||||
currentGroup = if (pwGroupId == null) {
|
||||
mRootGroup
|
||||
} else {
|
||||
mDatabase?.getGroupById(pwGroupId)
|
||||
}
|
||||
|
||||
return currentGroup
|
||||
}
|
||||
}
|
||||
|
||||
private fun assignGroupViewElements() {
|
||||
// Assign title
|
||||
if (mCurrentGroup != null) {
|
||||
val title = mCurrentGroup?.title
|
||||
if (title != null && title.isNotEmpty()) {
|
||||
if (groupNameView != null) {
|
||||
groupNameView?.text = title
|
||||
groupNameView?.invalidate()
|
||||
}
|
||||
} else {
|
||||
if (groupNameView != null) {
|
||||
groupNameView?.text = getText(R.string.root)
|
||||
groupNameView?.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mCurrentGroupIsASearch) {
|
||||
searchTitleView?.visibility = View.VISIBLE
|
||||
} else {
|
||||
searchTitleView?.visibility = View.GONE
|
||||
}
|
||||
|
||||
// Assign icon
|
||||
if (mCurrentGroupIsASearch) {
|
||||
if (toolbar != null) {
|
||||
toolbar?.navigationIcon = null
|
||||
}
|
||||
iconView?.visibility = View.GONE
|
||||
} else {
|
||||
// Assign the group icon depending of IconPack or custom icon
|
||||
iconView?.visibility = View.VISIBLE
|
||||
mCurrentGroup?.let {
|
||||
if (mDatabase?.drawFactory != null)
|
||||
iconView?.assignDatabaseIcon(mDatabase?.drawFactory!!, it.icon, mIconColor)
|
||||
|
||||
if (toolbar != null) {
|
||||
if (mCurrentGroup?.containsParent() == true)
|
||||
toolbar?.setNavigationIcon(R.drawable.ic_arrow_up_white_24dp)
|
||||
else {
|
||||
toolbar?.navigationIcon = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show selection mode message if needed
|
||||
if (mSelectionMode) {
|
||||
modeTitleView?.visibility = View.VISIBLE
|
||||
} else {
|
||||
modeTitleView?.visibility = View.GONE
|
||||
}
|
||||
|
||||
// Show button if allowed
|
||||
addNodeButtonView?.apply {
|
||||
|
||||
// To enable add button
|
||||
val addGroupEnabled = !mReadOnly && !mCurrentGroupIsASearch
|
||||
var addEntryEnabled = !mReadOnly && !mCurrentGroupIsASearch
|
||||
mCurrentGroup?.let {
|
||||
val isRoot = it == mRootGroup
|
||||
if (!it.allowAddEntryIfIsRoot())
|
||||
addEntryEnabled = !isRoot && addEntryEnabled
|
||||
if (isRoot) {
|
||||
showWarnings()
|
||||
}
|
||||
}
|
||||
enableAddGroup(addGroupEnabled)
|
||||
enableAddEntry(addEntryEnabled)
|
||||
|
||||
if (isEnable)
|
||||
showButton()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrolled(dy: Int) {
|
||||
addNodeButtonView?.hideButtonOnScrollListener(dy)
|
||||
}
|
||||
|
||||
override fun onNodeClick(node: NodeVersioned) {
|
||||
when (node.type) {
|
||||
Type.GROUP -> try {
|
||||
openChildGroup(node as GroupVersioned)
|
||||
} catch (e: ClassCastException) {
|
||||
Log.e(TAG, "Node can't be cast in Group")
|
||||
}
|
||||
|
||||
Type.ENTRY -> try {
|
||||
val entryVersioned = node as EntryVersioned
|
||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||
{
|
||||
EntryActivity.launch(this@GroupActivity, entryVersioned, mReadOnly)
|
||||
},
|
||||
{
|
||||
// Populate Magikeyboard with entry
|
||||
mDatabase?.let { database ->
|
||||
MagikIME.addEntryAndLaunchNotificationIfAllowed(this@GroupActivity,
|
||||
entryVersioned.getEntryInfo(database))
|
||||
}
|
||||
// Consume the selection mode
|
||||
EntrySelectionHelper.removeEntrySelectionModeFromIntent(intent)
|
||||
moveTaskToBack(true)
|
||||
},
|
||||
{
|
||||
// Build response with the entry selected
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) {
|
||||
AutofillHelper.buildResponseWhenEntrySelected(this@GroupActivity,
|
||||
entryVersioned.getEntryInfo(mDatabase!!))
|
||||
}
|
||||
finish()
|
||||
})
|
||||
} catch (e: ClassCastException) {
|
||||
Log.e(TAG, "Node can't be cast in Entry")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOpenMenuClick(node: NodeVersioned): Boolean {
|
||||
onNodeClick(node)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onEditMenuClick(node: NodeVersioned): Boolean {
|
||||
when (node.type) {
|
||||
Type.GROUP -> {
|
||||
mOldGroupToUpdate = node as GroupVersioned
|
||||
GroupEditDialogFragment.build(mOldGroupToUpdate!!)
|
||||
.show(supportFragmentManager,
|
||||
GroupEditDialogFragment.TAG_CREATE_GROUP)
|
||||
}
|
||||
Type.ENTRY -> EntryEditActivity.launch(this@GroupActivity, node as EntryVersioned)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onCopyMenuClick(node: NodeVersioned): Boolean {
|
||||
toolbarPasteExpandableLayout?.expand()
|
||||
mNodeToCopy = node
|
||||
toolbarPaste?.setOnMenuItemClickListener(OnCopyMenuItemClickListener())
|
||||
return false
|
||||
}
|
||||
|
||||
private inner class OnCopyMenuItemClickListener : Toolbar.OnMenuItemClickListener {
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
toolbarPasteExpandableLayout?.collapse()
|
||||
|
||||
when (item.itemId) {
|
||||
R.id.menu_paste -> {
|
||||
when (mNodeToCopy?.type) {
|
||||
Type.GROUP -> Log.e(TAG, "Copy not allowed for group")
|
||||
Type.ENTRY -> {
|
||||
mCurrentGroup?.let { currentGroup ->
|
||||
copyEntry(mNodeToCopy as EntryVersioned, currentGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
mNodeToCopy = null
|
||||
return true
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyEntry(entryToCopy: EntryVersioned, newParent: GroupVersioned) {
|
||||
ProgressDialogSaveDatabaseThread(this) {
|
||||
CopyEntryRunnable(this,
|
||||
Database.getInstance(),
|
||||
entryToCopy,
|
||||
newParent,
|
||||
AfterAddNodeRunnable(),
|
||||
!mReadOnly)
|
||||
}.start()
|
||||
}
|
||||
|
||||
override fun onMoveMenuClick(node: NodeVersioned): Boolean {
|
||||
toolbarPasteExpandableLayout?.expand()
|
||||
mNodeToMove = node
|
||||
toolbarPaste?.setOnMenuItemClickListener(OnMoveMenuItemClickListener())
|
||||
return false
|
||||
}
|
||||
|
||||
private inner class OnMoveMenuItemClickListener : Toolbar.OnMenuItemClickListener {
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
toolbarPasteExpandableLayout?.collapse()
|
||||
|
||||
when (item.itemId) {
|
||||
R.id.menu_paste -> {
|
||||
when (mNodeToMove?.type) {
|
||||
Type.GROUP -> {
|
||||
mCurrentGroup?.let { currentGroup ->
|
||||
moveGroup(mNodeToMove as GroupVersioned, currentGroup)
|
||||
}
|
||||
}
|
||||
Type.ENTRY -> {
|
||||
mCurrentGroup?.let { currentGroup ->
|
||||
moveEntry(mNodeToMove as EntryVersioned, currentGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
mNodeToMove = null
|
||||
return true
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private fun moveGroup(groupToMove: GroupVersioned, newParent: GroupVersioned) {
|
||||
ProgressDialogSaveDatabaseThread(this) {
|
||||
MoveGroupRunnable(
|
||||
this,
|
||||
Database.getInstance(),
|
||||
groupToMove,
|
||||
newParent,
|
||||
AfterAddNodeRunnable(),
|
||||
!mReadOnly)
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun moveEntry(entryToMove: EntryVersioned, newParent: GroupVersioned) {
|
||||
ProgressDialogSaveDatabaseThread(this) {
|
||||
MoveEntryRunnable(
|
||||
this,
|
||||
Database.getInstance(),
|
||||
entryToMove,
|
||||
newParent,
|
||||
AfterAddNodeRunnable(),
|
||||
!mReadOnly)
|
||||
}.start()
|
||||
}
|
||||
|
||||
override fun onDeleteMenuClick(node: NodeVersioned): Boolean {
|
||||
when (node.type) {
|
||||
Type.GROUP -> deleteGroup(node as GroupVersioned)
|
||||
Type.ENTRY -> deleteEntry(node as EntryVersioned)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun deleteGroup(group: GroupVersioned) {
|
||||
//TODO Verify trash recycle bin
|
||||
ProgressDialogSaveDatabaseThread(this) {
|
||||
DeleteGroupRunnable(
|
||||
this,
|
||||
Database.getInstance(),
|
||||
group,
|
||||
AfterDeleteNodeRunnable(),
|
||||
!mReadOnly)
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun deleteEntry(entry: EntryVersioned) {
|
||||
ProgressDialogSaveDatabaseThread(this) {
|
||||
DeleteEntryRunnable(
|
||||
this,
|
||||
Database.getInstance(),
|
||||
entry,
|
||||
AfterDeleteNodeRunnable(),
|
||||
!mReadOnly)
|
||||
}.start()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// Refresh the elements
|
||||
assignGroupViewElements()
|
||||
// Refresh suggestions to change preferences
|
||||
mSearchSuggestionAdapter?.reInit(this)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
|
||||
val inflater = menuInflater
|
||||
inflater.inflate(R.menu.search, menu)
|
||||
inflater.inflate(R.menu.database_lock, menu)
|
||||
if (!mSelectionMode) {
|
||||
inflater.inflate(R.menu.default_menu, menu)
|
||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||
}
|
||||
|
||||
// Get the SearchView and set the searchable configuration
|
||||
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
|
||||
|
||||
menu.findItem(R.id.menu_search)?.let {
|
||||
val searchView = it.actionView as SearchView?
|
||||
searchView?.apply {
|
||||
setSearchableInfo(searchManager.getSearchableInfo(
|
||||
ComponentName(this@GroupActivity, GroupActivity::class.java)))
|
||||
setIconifiedByDefault(false) // Do not iconify the widget; expand it by default
|
||||
suggestionsAdapter = mSearchSuggestionAdapter
|
||||
setOnSuggestionListener(object : SearchView.OnSuggestionListener {
|
||||
override fun onSuggestionClick(position: Int): Boolean {
|
||||
mSearchSuggestionAdapter?.let { searchAdapter ->
|
||||
searchAdapter.getEntryFromPosition(position)?.let { entry ->
|
||||
onNodeClick(entry)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSuggestionSelect(position: Int): Boolean {
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
// Launch education screen
|
||||
Handler().post { performedNextEducation(GroupActivityEducation(this), menu) }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun performedNextEducation(groupActivityEducation: GroupActivityEducation,
|
||||
menu: Menu) {
|
||||
|
||||
// If no node, show education to add new one
|
||||
val addNodeButtonEducationPerformed = mListNodesFragment != null
|
||||
&& mListNodesFragment!!.isEmpty
|
||||
&& addNodeButtonView?.addButtonView != null
|
||||
&& addNodeButtonView!!.isEnable
|
||||
&& groupActivityEducation.checkAndPerformedAddNodeButtonEducation(
|
||||
addNodeButtonView?.addButtonView!!,
|
||||
{
|
||||
addNodeButtonView?.openButtonIfClose()
|
||||
},
|
||||
{
|
||||
performedNextEducation(groupActivityEducation, menu)
|
||||
}
|
||||
)
|
||||
if (!addNodeButtonEducationPerformed) {
|
||||
|
||||
val searchMenuEducationPerformed = toolbar != null
|
||||
&& toolbar!!.findViewById<View>(R.id.menu_search) != null
|
||||
&& groupActivityEducation.checkAndPerformedSearchMenuEducation(
|
||||
toolbar!!.findViewById(R.id.menu_search),
|
||||
{
|
||||
menu.findItem(R.id.menu_search).expandActionView()
|
||||
},
|
||||
{
|
||||
performedNextEducation(groupActivityEducation, menu)
|
||||
})
|
||||
|
||||
if (!searchMenuEducationPerformed) {
|
||||
|
||||
val sortMenuEducationPerformed = toolbar != null
|
||||
&& toolbar!!.findViewById<View>(R.id.menu_sort) != null
|
||||
&& groupActivityEducation.checkAndPerformedSortMenuEducation(
|
||||
toolbar!!.findViewById(R.id.menu_sort),
|
||||
{
|
||||
onOptionsItemSelected(menu.findItem(R.id.menu_sort))
|
||||
},
|
||||
{
|
||||
performedNextEducation(groupActivityEducation, menu)
|
||||
})
|
||||
|
||||
if (!sortMenuEducationPerformed) {
|
||||
// lockMenuEducationPerformed
|
||||
toolbar != null
|
||||
&& toolbar!!.findViewById<View>(R.id.menu_lock) != null
|
||||
&& groupActivityEducation.checkAndPerformedLockMenuEducation(
|
||||
toolbar!!.findViewById(R.id.menu_lock),
|
||||
{
|
||||
onOptionsItemSelected(menu.findItem(R.id.menu_lock))
|
||||
},
|
||||
{
|
||||
performedNextEducation(groupActivityEducation, menu)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
R.id.menu_search ->
|
||||
//onSearchRequested();
|
||||
return true
|
||||
R.id.menu_lock -> {
|
||||
lockAndExit()
|
||||
return true
|
||||
}
|
||||
else -> {
|
||||
// Check the time lock before launching settings
|
||||
MenuUtil.onDefaultMenuOptionsItemSelected(this, item, mReadOnly, true)
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?,
|
||||
name: String?,
|
||||
icon: PwIcon?) {
|
||||
val database = Database.getInstance()
|
||||
|
||||
if (name != null && name.isNotEmpty() && icon != null) {
|
||||
when (action) {
|
||||
GroupEditDialogFragment.EditGroupDialogAction.CREATION -> {
|
||||
// If group creation
|
||||
mCurrentGroup?.let { currentGroup ->
|
||||
// Build the group
|
||||
database.createGroup()?.let { newGroup ->
|
||||
newGroup.title = name
|
||||
newGroup.icon = icon
|
||||
// Not really needed here because added in runnable but safe
|
||||
newGroup.parent = currentGroup
|
||||
|
||||
// If group created save it in the database
|
||||
ProgressDialogSaveDatabaseThread(this) {
|
||||
AddGroupRunnable(this,
|
||||
Database.getInstance(),
|
||||
newGroup,
|
||||
currentGroup,
|
||||
AfterAddNodeRunnable(),
|
||||
!mReadOnly)
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
GroupEditDialogFragment.EditGroupDialogAction.UPDATE ->
|
||||
// If update add new elements
|
||||
mOldGroupToUpdate?.let { oldGroupToUpdate ->
|
||||
GroupVersioned(oldGroupToUpdate).let { updateGroup ->
|
||||
updateGroup.title = name
|
||||
// TODO custom icon
|
||||
updateGroup.icon = icon
|
||||
|
||||
mListNodesFragment?.removeNode(oldGroupToUpdate)
|
||||
|
||||
// If group updated save it in the database
|
||||
ProgressDialogSaveDatabaseThread(this) {
|
||||
UpdateGroupRunnable(this,
|
||||
Database.getInstance(),
|
||||
oldGroupToUpdate,
|
||||
updateGroup,
|
||||
AfterUpdateNodeRunnable(),
|
||||
!mReadOnly)
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal inner class AfterAddNodeRunnable : AfterActionNodeFinishRunnable() {
|
||||
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
||||
runOnUiThread {
|
||||
if (actionNodeValues.result.isSuccess) {
|
||||
if (actionNodeValues.newNode != null)
|
||||
mListNodesFragment?.addNode(actionNodeValues.newNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal inner class AfterUpdateNodeRunnable : AfterActionNodeFinishRunnable() {
|
||||
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
||||
runOnUiThread {
|
||||
if (actionNodeValues.result.isSuccess) {
|
||||
if (actionNodeValues.oldNode!= null && actionNodeValues.newNode != null)
|
||||
mListNodesFragment?.updateNode(actionNodeValues.oldNode, actionNodeValues.newNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal inner class AfterDeleteNodeRunnable : AfterActionNodeFinishRunnable() {
|
||||
override fun onActionNodeFinish(actionNodeValues: ActionNodeValues) {
|
||||
runOnUiThread {
|
||||
if (actionNodeValues.result.isSuccess) {
|
||||
|
||||
// If the action register the position, use it to remove the entry view
|
||||
val positionNode = actionNodeValues.result.data?.getInt(NODE_POSITION_FOR_ACTION_NATURAL_ORDER_KEY)
|
||||
if (PreferencesUtil.getListSort(this@GroupActivity) == SortNodeEnum.DB
|
||||
&& positionNode != null) {
|
||||
mListNodesFragment?.removeNodeAt(positionNode)
|
||||
} else {
|
||||
// else use the old Node that was the entry unchanged with the old parent
|
||||
actionNodeValues.oldNode?.let { oldNode ->
|
||||
mListNodesFragment?.removeNode(oldNode)
|
||||
}
|
||||
}
|
||||
|
||||
// Add trash in views list if it doesn't exists
|
||||
val database = Database.getInstance()
|
||||
if (database.isRecycleBinEnabled) {
|
||||
val recycleBin = database.recycleBin
|
||||
if (mCurrentGroup != null && recycleBin != null
|
||||
&& mCurrentGroup!!.parent == null
|
||||
&& mCurrentGroup != recycleBin) {
|
||||
if (mListNodesFragment?.contains(recycleBin) == true)
|
||||
mListNodesFragment?.updateNode(recycleBin)
|
||||
else
|
||||
mListNodesFragment?.addNode(recycleBin)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancelEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction?,
|
||||
name: String?,
|
||||
icon: PwIcon?) {
|
||||
// Do nothing here
|
||||
}
|
||||
|
||||
override// For icon in create tree dialog
|
||||
fun iconPicked(bundle: Bundle) {
|
||||
(supportFragmentManager
|
||||
.findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as GroupEditDialogFragment)
|
||||
.iconPicked(bundle)
|
||||
}
|
||||
|
||||
private fun showWarnings() {
|
||||
if (Database.getInstance().isReadOnly) {
|
||||
if (PreferencesUtil.showReadOnlyWarning(this)) {
|
||||
ReadOnlyDialog().show(supportFragmentManager, "readOnlyDialog")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSortSelected(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) {
|
||||
mListNodesFragment?.onSortSelected(sortNodeEnum, ascending, groupsBefore, recycleBinBottom)
|
||||
}
|
||||
|
||||
override fun startActivity(intent: Intent) {
|
||||
|
||||
// Get the intent, verify the action and get the query
|
||||
if (Intent.ACTION_SEARCH == intent.action) {
|
||||
// manually launch the real search activity
|
||||
val searchIntent = Intent(applicationContext, GroupActivity::class.java).apply {
|
||||
// Add bundle of current intent
|
||||
putExtras(this@GroupActivity.intent)
|
||||
// add query to the Intent Extras
|
||||
action = Intent.ACTION_SEARCH
|
||||
putExtra(SearchManager.QUERY, intent.getStringExtra(SearchManager.QUERY))
|
||||
}
|
||||
|
||||
super.startActivity(searchIntent)
|
||||
} else {
|
||||
super.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun startActivityForResult(intent: Intent, requestCode: Int, options: Bundle?) {
|
||||
/*
|
||||
* ACTION_SEARCH automatically forces a new task. This occurs when you open a kdb file in
|
||||
* another app such as Files or GoogleDrive and then Search for an entry. Here we remove the
|
||||
* FLAG_ACTIVITY_NEW_TASK flag bit allowing search to open it's activity in the current task.
|
||||
*/
|
||||
if (Intent.ACTION_SEARCH == intent.action) {
|
||||
var flags = intent.flags
|
||||
flags = flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv()
|
||||
intent.flags = flags
|
||||
}
|
||||
|
||||
super.startActivityForResult(intent, requestCode, options)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
// Not directly get the entry from intent data but from database
|
||||
mListNodesFragment?.rebuildList()
|
||||
}
|
||||
|
||||
private fun removeSearchInIntent(intent: Intent) {
|
||||
if (Intent.ACTION_SEARCH == intent.action) {
|
||||
mCurrentGroupIsASearch = false
|
||||
intent.action = Intent.ACTION_DEFAULT
|
||||
intent.removeExtra(SearchManager.QUERY)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
// Normal way when we are not in root
|
||||
if (mRootGroup != null && mRootGroup != mCurrentGroup)
|
||||
super.onBackPressed()
|
||||
// Else lock if needed
|
||||
else {
|
||||
if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) {
|
||||
lockAndExit()
|
||||
super.onBackPressed()
|
||||
} else {
|
||||
moveTaskToBack(true)
|
||||
}
|
||||
}
|
||||
|
||||
mListNodesFragment = supportFragmentManager.findFragmentByTag(LIST_NODES_FRAGMENT_TAG) as ListNodesFragment
|
||||
// to refresh fragment
|
||||
mListNodesFragment?.rebuildList()
|
||||
mCurrentGroup = mListNodesFragment?.mainGroup
|
||||
removeSearchInIntent(intent)
|
||||
assignGroupViewElements()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = GroupActivity::class.java.name
|
||||
|
||||
private const val GROUP_ID_KEY = "GROUP_ID_KEY"
|
||||
private const val LIST_NODES_FRAGMENT_TAG = "LIST_NODES_FRAGMENT_TAG"
|
||||
private const val SEARCH_FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG"
|
||||
private const val OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY"
|
||||
private const val NODE_TO_COPY_KEY = "NODE_TO_COPY_KEY"
|
||||
private const val NODE_TO_MOVE_KEY = "NODE_TO_MOVE_KEY"
|
||||
|
||||
private fun buildAndLaunchIntent(activity: Activity, group: GroupVersioned?, readOnly: Boolean,
|
||||
intentBuildLauncher: (Intent) -> Unit) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, GroupActivity::class.java)
|
||||
if (group != null) {
|
||||
intent.putExtra(GROUP_ID_KEY, group.nodeId)
|
||||
}
|
||||
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
||||
intentBuildLauncher.invoke(intent)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Standard Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@JvmOverloads
|
||||
fun launch(activity: Activity, readOnly: Boolean = PreferencesUtil.enableReadOnlyDatabase(activity)) {
|
||||
TimeoutHelper.recordTime(activity)
|
||||
buildAndLaunchIntent(activity, null, readOnly) { intent ->
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Keyboard Launch
|
||||
* -------------------------
|
||||
*/
|
||||
// TODO implement pre search to directly open the direct group
|
||||
|
||||
fun launchForKeyboarSelection(activity: Activity, readOnly: Boolean) {
|
||||
TimeoutHelper.recordTime(activity)
|
||||
buildAndLaunchIntent(activity, null, readOnly) { intent ->
|
||||
EntrySelectionHelper.startActivityForEntrySelection(activity, intent)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Autofill Launch
|
||||
* -------------------------
|
||||
*/
|
||||
// TODO implement pre search to directly open the direct group
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
fun launchForAutofillResult(activity: Activity, assistStructure: AssistStructure, readOnly: Boolean) {
|
||||
TimeoutHelper.recordTime(activity)
|
||||
buildAndLaunchIntent(activity, null, readOnly) { intent ->
|
||||
AutofillHelper.startActivityForAutofillResult(activity, intent, assistStructure)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.kunzisoft.keepass.activities;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
public interface IntentBuildLauncher {
|
||||
void startActivityForResult(Intent intent);
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
package com.kunzisoft.keepass.activities;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.kunzisoft.keepass.R;
|
||||
import com.kunzisoft.keepass.adapters.NodeAdapter;
|
||||
import com.kunzisoft.keepass.app.App;
|
||||
import com.kunzisoft.keepass.database.PwDatabase;
|
||||
import com.kunzisoft.keepass.database.PwGroup;
|
||||
import com.kunzisoft.keepass.database.PwNode;
|
||||
import com.kunzisoft.keepass.database.SortNodeEnum;
|
||||
import com.kunzisoft.keepass.dialogs.SortDialogFragment;
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil;
|
||||
import com.kunzisoft.keepass.stylish.StylishFragment;
|
||||
|
||||
public class ListNodesFragment extends StylishFragment implements
|
||||
SortDialogFragment.SortSelectionListener {
|
||||
|
||||
private static final String TAG = ListNodesFragment.class.getName();
|
||||
|
||||
private static final String GROUP_KEY = "GROUP_KEY";
|
||||
private static final String IS_SEARCH = "IS_SEARCH";
|
||||
|
||||
private NodeAdapter.NodeClickCallback nodeClickCallback;
|
||||
private NodeAdapter.NodeMenuListener nodeMenuListener;
|
||||
private OnScrollListener onScrollListener;
|
||||
|
||||
private RecyclerView listView;
|
||||
private PwGroup currentGroup;
|
||||
private NodeAdapter mAdapter;
|
||||
|
||||
private View notFoundView;
|
||||
private boolean isASearchResult;
|
||||
|
||||
// Preferences for sorting
|
||||
private SharedPreferences prefs;
|
||||
|
||||
private boolean readOnly;
|
||||
|
||||
public static ListNodesFragment newInstance(PwGroup group, boolean readOnly, boolean isASearch) {
|
||||
Bundle bundle = new Bundle();
|
||||
if (group != null) {
|
||||
bundle.putParcelable(GROUP_KEY, group);
|
||||
}
|
||||
bundle.putBoolean(IS_SEARCH, isASearch);
|
||||
ReadOnlyHelper.putReadOnlyInBundle(bundle, readOnly);
|
||||
ListNodesFragment listNodesFragment = new ListNodesFragment();
|
||||
listNodesFragment.setArguments(bundle);
|
||||
return listNodesFragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
try {
|
||||
nodeClickCallback = (NodeAdapter.NodeClickCallback) context;
|
||||
} catch (ClassCastException e) {
|
||||
// The activity doesn't implement the interface, throw exception
|
||||
throw new ClassCastException(context.toString()
|
||||
+ " must implement " + NodeAdapter.NodeClickCallback.class.getName());
|
||||
}
|
||||
try {
|
||||
nodeMenuListener = (NodeAdapter.NodeMenuListener) context;
|
||||
} catch (ClassCastException e) {
|
||||
nodeMenuListener = null;
|
||||
// Context menu can be omit
|
||||
Log.w(TAG, context.toString()
|
||||
+ " must implement " + NodeAdapter.NodeMenuListener.class.getName());
|
||||
}
|
||||
try {
|
||||
onScrollListener = (OnScrollListener) context;
|
||||
} catch (ClassCastException e) {
|
||||
onScrollListener = null;
|
||||
// Context menu can be omit
|
||||
Log.w(TAG, context.toString()
|
||||
+ " must implement " + RecyclerView.OnScrollListener.class.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if ( getActivity() != null ) {
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, getArguments());
|
||||
|
||||
if (getArguments() != null) {
|
||||
// Contains all the group in element
|
||||
if (getArguments().containsKey(GROUP_KEY)) {
|
||||
currentGroup = getArguments().getParcelable(GROUP_KEY);
|
||||
}
|
||||
|
||||
if (getArguments().containsKey(IS_SEARCH)) {
|
||||
isASearchResult = getArguments().getBoolean(IS_SEARCH);
|
||||
}
|
||||
}
|
||||
|
||||
mAdapter = new NodeAdapter(getContextThemed(), getActivity().getMenuInflater());
|
||||
mAdapter.setReadOnly(readOnly);
|
||||
mAdapter.setIsASearchResult(isASearchResult);
|
||||
mAdapter.setOnNodeClickListener(nodeClickCallback);
|
||||
|
||||
if (nodeMenuListener != null) {
|
||||
mAdapter.setActivateContextMenu(true);
|
||||
mAdapter.setNodeMenuListener(nodeMenuListener);
|
||||
}
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
ReadOnlyHelper.onSaveInstanceState(outState, readOnly);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
super.onCreateView(inflater, container, savedInstanceState);
|
||||
|
||||
// To apply theme
|
||||
View rootView = inflater.cloneInContext(getContextThemed())
|
||||
.inflate(R.layout.list_nodes_fragment, container, false);
|
||||
listView = rootView.findViewById(R.id.nodes_list);
|
||||
notFoundView = rootView.findViewById(R.id.not_found_container);
|
||||
|
||||
if (onScrollListener != null) {
|
||||
listView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
onScrollListener.onScrolled(dy);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
rebuildList();
|
||||
|
||||
if (isASearchResult && mAdapter.isEmpty()) {
|
||||
// To show the " no search entry found "
|
||||
listView.setVisibility(View.GONE);
|
||||
notFoundView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
listView.setVisibility(View.VISIBLE);
|
||||
notFoundView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public void rebuildList() {
|
||||
// Add elements to the list
|
||||
mAdapter.rebuildList(currentGroup);
|
||||
assignListToNodeAdapter(listView);
|
||||
}
|
||||
|
||||
protected void assignListToNodeAdapter(RecyclerView recyclerView) {
|
||||
recyclerView.setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
recyclerView.setAdapter(mAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSortSelected(SortNodeEnum sortNodeEnum, boolean ascending, boolean groupsBefore, boolean recycleBinBottom) {
|
||||
// Toggle setting
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putString(getString(R.string.sort_node_key), sortNodeEnum.name());
|
||||
editor.putBoolean(getString(R.string.sort_ascending_key), ascending);
|
||||
editor.putBoolean(getString(R.string.sort_group_before_key), groupsBefore);
|
||||
editor.putBoolean(getString(R.string.sort_recycle_bin_bottom_key), recycleBinBottom);
|
||||
editor.apply();
|
||||
|
||||
// Tell the adapter to refresh it's list
|
||||
mAdapter.notifyChangeSort(sortNodeEnum, ascending, groupsBefore);
|
||||
mAdapter.rebuildList(currentGroup);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.tree, menu);
|
||||
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch ( item.getItemId() ) {
|
||||
|
||||
case R.id.menu_sort:
|
||||
SortDialogFragment sortDialogFragment;
|
||||
|
||||
PwDatabase database = App.getDB().getPwDatabase();
|
||||
/*
|
||||
// TODO Recycle bin bottom
|
||||
if (database.isRecycleBinAvailable() && database.isRecycleBinEnabled()) {
|
||||
sortDialogFragment =
|
||||
SortDialogFragment.getInstance(
|
||||
PrefsUtil.getListSort(this),
|
||||
PrefsUtil.getAscendingSort(this),
|
||||
PrefsUtil.getGroupsBeforeSort(this),
|
||||
PrefsUtil.getRecycleBinBottomSort(this));
|
||||
} else {
|
||||
*/
|
||||
sortDialogFragment =
|
||||
SortDialogFragment.getInstance(
|
||||
PreferencesUtil.getListSort(getContext()),
|
||||
PreferencesUtil.getAscendingSort(getContext()),
|
||||
PreferencesUtil.getGroupsBeforeSort(getContext()));
|
||||
//}
|
||||
|
||||
sortDialogFragment.show(getChildFragmentManager(), "sortDialog");
|
||||
return true;
|
||||
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
switch (requestCode) {
|
||||
case EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE:
|
||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE ||
|
||||
resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
|
||||
PwNode newNode = data.getParcelableExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY);
|
||||
if (newNode != null) {
|
||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
|
||||
mAdapter.addNode(newNode);
|
||||
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
|
||||
//mAdapter.updateLastNodeRegister(newNode);
|
||||
mAdapter.rebuildList(currentGroup);
|
||||
}
|
||||
} else {
|
||||
Log.e(this.getClass().getName(), "New node can be retrieve in Activity Result");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return mAdapter == null || mAdapter.getItemCount() <= 0;
|
||||
}
|
||||
|
||||
public void addNode(PwNode newNode) {
|
||||
mAdapter.addNode(newNode);
|
||||
}
|
||||
|
||||
public void updateNode(PwNode oldNode, PwNode newNode) {
|
||||
mAdapter.updateNode(oldNode, newNode);
|
||||
}
|
||||
|
||||
public void removeNode(PwNode pwNode) {
|
||||
mAdapter.removeNode(pwNode);
|
||||
}
|
||||
|
||||
public PwGroup getMainGroup() {
|
||||
return currentGroup;
|
||||
}
|
||||
|
||||
public interface OnScrollListener {
|
||||
|
||||
/**
|
||||
* Callback method to be invoked when the RecyclerView has been scrolled. This will be
|
||||
* called after the scroll has completed.
|
||||
*
|
||||
* @param dy The amount of vertical scroll.
|
||||
*/
|
||||
void onScrolled(int dy);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.adapters.NodeAdapter
|
||||
import com.kunzisoft.keepass.database.SortNodeEnum
|
||||
import com.kunzisoft.keepass.database.element.GroupVersioned
|
||||
import com.kunzisoft.keepass.database.element.NodeVersioned
|
||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
|
||||
class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener {
|
||||
|
||||
private var nodeClickCallback: NodeAdapter.NodeClickCallback? = null
|
||||
private var nodeMenuListener: NodeAdapter.NodeMenuListener? = null
|
||||
private var onScrollListener: OnScrollListener? = null
|
||||
|
||||
private var listView: RecyclerView? = null
|
||||
var mainGroup: GroupVersioned? = null
|
||||
private set
|
||||
private var mAdapter: NodeAdapter? = null
|
||||
|
||||
private var notFoundView: View? = null
|
||||
private var isASearchResult: Boolean = false
|
||||
|
||||
// Preferences for sorting
|
||||
private var prefs: SharedPreferences? = null
|
||||
|
||||
private var readOnly: Boolean = false
|
||||
get() {
|
||||
return field || selectionMode
|
||||
}
|
||||
private var selectionMode: Boolean = false
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
try {
|
||||
nodeClickCallback = context as NodeAdapter.NodeClickCallback
|
||||
} catch (e: ClassCastException) {
|
||||
// The activity doesn't implement the interface, throw exception
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + NodeAdapter.NodeClickCallback::class.java.name)
|
||||
}
|
||||
|
||||
try {
|
||||
nodeMenuListener = context as NodeAdapter.NodeMenuListener
|
||||
} catch (e: ClassCastException) {
|
||||
nodeMenuListener = null
|
||||
// Context menu can be omit
|
||||
Log.w(TAG, context.toString()
|
||||
+ " must implement " + NodeAdapter.NodeMenuListener::class.java.name)
|
||||
}
|
||||
|
||||
try {
|
||||
onScrollListener = context as OnScrollListener
|
||||
} catch (e: ClassCastException) {
|
||||
onScrollListener = null
|
||||
// Context menu can be omit
|
||||
Log.w(TAG, context.toString()
|
||||
+ " must implement " + RecyclerView.OnScrollListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
activity?.let { currentActivity ->
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, arguments)
|
||||
|
||||
arguments?.let { args ->
|
||||
// Contains all the group in element
|
||||
if (args.containsKey(GROUP_KEY)) {
|
||||
mainGroup = args.getParcelable(GROUP_KEY)
|
||||
}
|
||||
if (args.containsKey(IS_SEARCH)) {
|
||||
isASearchResult = args.getBoolean(IS_SEARCH)
|
||||
}
|
||||
}
|
||||
|
||||
contextThemed?.let { context ->
|
||||
mAdapter = NodeAdapter(context, currentActivity.menuInflater)
|
||||
mAdapter?.apply {
|
||||
setReadOnly(readOnly)
|
||||
setIsASearchResult(isASearchResult)
|
||||
setOnNodeClickListener(nodeClickCallback)
|
||||
setActivateContextMenu(true)
|
||||
setNodeMenuListener(nodeMenuListener)
|
||||
}
|
||||
}
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
// To apply theme
|
||||
val rootView = inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.fragment_list_nodes, container, false)
|
||||
listView = rootView.findViewById(R.id.nodes_list)
|
||||
notFoundView = rootView.findViewById(R.id.not_found_container)
|
||||
|
||||
onScrollListener?.let { onScrollListener ->
|
||||
listView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
onScrollListener.onScrolled(dy)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
rebuildList()
|
||||
|
||||
return rootView
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
activity?.intent?.let {
|
||||
selectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(it)
|
||||
}
|
||||
// Force read only mode if selection mode
|
||||
mAdapter?.apply {
|
||||
setReadOnly(readOnly)
|
||||
}
|
||||
|
||||
// Refresh data
|
||||
mAdapter?.notifyDataSetChanged()
|
||||
|
||||
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
|
||||
// To show the " no search entry found "
|
||||
listView?.visibility = View.GONE
|
||||
notFoundView?.visibility = View.VISIBLE
|
||||
} else {
|
||||
listView?.visibility = View.VISIBLE
|
||||
notFoundView?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun rebuildList() {
|
||||
// Add elements to the list
|
||||
mainGroup?.let { mainGroup ->
|
||||
mAdapter?.rebuildList(mainGroup)
|
||||
}
|
||||
listView?.apply {
|
||||
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = mAdapter
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSortSelected(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) {
|
||||
// Toggle setting
|
||||
prefs?.edit()?.apply {
|
||||
putString(getString(R.string.sort_node_key), sortNodeEnum.name)
|
||||
putBoolean(getString(R.string.sort_ascending_key), ascending)
|
||||
putBoolean(getString(R.string.sort_group_before_key), groupsBefore)
|
||||
putBoolean(getString(R.string.sort_recycle_bin_bottom_key), recycleBinBottom)
|
||||
apply()
|
||||
}
|
||||
|
||||
// Tell the adapter to refresh it's list
|
||||
mAdapter?.notifyChangeSort(sortNodeEnum, ascending, groupsBefore)
|
||||
mainGroup?.let { mainGroup ->
|
||||
mAdapter?.rebuildList(mainGroup)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.tree, menu)
|
||||
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
|
||||
R.id.menu_sort -> {
|
||||
context?.let { context ->
|
||||
val sortDialogFragment: SortDialogFragment =
|
||||
if (Database.getInstance().isRecycleBinAvailable
|
||||
&& Database.getInstance().isRecycleBinEnabled) {
|
||||
SortDialogFragment.getInstance(
|
||||
PreferencesUtil.getListSort(context),
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context),
|
||||
PreferencesUtil.getRecycleBinBottomSort(context))
|
||||
} else {
|
||||
SortDialogFragment.getInstance(
|
||||
PreferencesUtil.getListSort(context),
|
||||
PreferencesUtil.getAscendingSort(context),
|
||||
PreferencesUtil.getGroupsBeforeSort(context))
|
||||
}
|
||||
|
||||
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
when (requestCode) {
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE
|
||||
|| resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
|
||||
data?.getParcelableExtra<NodeVersioned>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { newNode ->
|
||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
|
||||
mAdapter?.addNode(newNode)
|
||||
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
|
||||
//mAdapter.updateLastNodeRegister(newNode);
|
||||
mainGroup?.let { mainGroup ->
|
||||
mAdapter?.rebuildList(mainGroup)
|
||||
}
|
||||
}
|
||||
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun contains(node: NodeVersioned): Boolean {
|
||||
return mAdapter?.contains(node) ?: false
|
||||
}
|
||||
|
||||
fun addNode(newNode: NodeVersioned) {
|
||||
mAdapter?.addNode(newNode)
|
||||
}
|
||||
|
||||
fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned? = null) {
|
||||
mAdapter?.updateNode(oldNode, newNode ?: oldNode)
|
||||
}
|
||||
|
||||
fun removeNode(pwNode: NodeVersioned) {
|
||||
mAdapter?.removeNode(pwNode)
|
||||
}
|
||||
|
||||
fun removeNodeAt(position: Int) {
|
||||
mAdapter?.removeNodeAt(position)
|
||||
}
|
||||
|
||||
interface OnScrollListener {
|
||||
|
||||
/**
|
||||
* Callback method to be invoked when the RecyclerView has been scrolled. This will be
|
||||
* called after the scroll has completed.
|
||||
*
|
||||
* @param dy The amount of vertical scroll.
|
||||
*/
|
||||
fun onScrolled(dy: Int)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = ListNodesFragment::class.java.name
|
||||
|
||||
private const val GROUP_KEY = "GROUP_KEY"
|
||||
private const val IS_SEARCH = "IS_SEARCH"
|
||||
|
||||
fun newInstance(group: GroupVersioned?, readOnly: Boolean, isASearch: Boolean): ListNodesFragment {
|
||||
val bundle = Bundle()
|
||||
if (group != null) {
|
||||
bundle.putParcelable(GROUP_KEY, group)
|
||||
}
|
||||
bundle.putBoolean(IS_SEARCH, isASearch)
|
||||
ReadOnlyHelper.putReadOnlyInBundle(bundle, readOnly)
|
||||
val listNodesFragment = ListNodesFragment()
|
||||
listNodesFragment.arguments = bundle
|
||||
return listNodesFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,705 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.assist.AssistStructure
|
||||
import android.app.backup.BackupManager
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.preference.PreferenceManager
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
||||
import android.widget.*
|
||||
import androidx.biometric.BiometricManager
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.FingerPrintExplanationDialog
|
||||
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||
import com.kunzisoft.keepass.utils.FileDatabaseInfo
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.database.action.LoadDatabaseRunnable
|
||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||
import com.kunzisoft.keepass.view.asError
|
||||
import kotlinx.android.synthetic.main.activity_password.*
|
||||
import java.io.FileNotFoundException
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class PasswordActivity : StylishActivity() {
|
||||
|
||||
// Views
|
||||
private var toolbar: Toolbar? = null
|
||||
|
||||
private var filenameView: TextView? = null
|
||||
private var passwordView: EditText? = null
|
||||
private var keyFileView: EditText? = null
|
||||
private var confirmButtonView: Button? = null
|
||||
private var checkboxPasswordView: CompoundButton? = null
|
||||
private var checkboxKeyFileView: CompoundButton? = null
|
||||
private var checkboxDefaultDatabaseView: CompoundButton? = null
|
||||
private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null
|
||||
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
|
||||
|
||||
private var mDatabaseFileUri: Uri? = null
|
||||
private var prefs: SharedPreferences? = null
|
||||
|
||||
private var mRememberKeyFile: Boolean = false
|
||||
private var mOpenFileHelper: OpenFileHelper? = null
|
||||
|
||||
private var readOnly: Boolean = false
|
||||
|
||||
private var advancedUnlockedManager: AdvancedUnlockedManager? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
mRememberKeyFile = prefs!!.getBoolean(getString(R.string.keyfile_key),
|
||||
resources.getBoolean(R.bool.keyfile_default))
|
||||
|
||||
setContentView(R.layout.activity_password)
|
||||
|
||||
toolbar = findViewById(R.id.toolbar)
|
||||
toolbar?.title = getString(R.string.app_name)
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
confirmButtonView = findViewById(R.id.pass_ok)
|
||||
filenameView = findViewById(R.id.filename)
|
||||
passwordView = findViewById(R.id.password)
|
||||
keyFileView = findViewById(R.id.pass_keyfile)
|
||||
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
||||
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
||||
checkboxDefaultDatabaseView = findViewById(R.id.default_database)
|
||||
advancedUnlockInfoView = findViewById(R.id.fingerprint_info)
|
||||
|
||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
||||
|
||||
val browseView = findViewById<View>(R.id.browse_button)
|
||||
mOpenFileHelper = OpenFileHelper(this@PasswordActivity)
|
||||
browseView.setOnClickListener(mOpenFileHelper!!.openFileOnClickViewListener)
|
||||
|
||||
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
||||
passwordView?.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun afterTextChanged(editable: Editable) {
|
||||
if (editable.toString().isNotEmpty() && checkboxPasswordView?.isChecked != true)
|
||||
checkboxPasswordView?.isChecked = true
|
||||
}
|
||||
})
|
||||
keyFileView?.setOnEditorActionListener(onEditorActionListener)
|
||||
keyFileView?.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun afterTextChanged(editable: Editable) {
|
||||
if (editable.toString().isNotEmpty() && checkboxKeyFileView?.isChecked != true)
|
||||
checkboxKeyFileView?.isChecked = true
|
||||
}
|
||||
})
|
||||
|
||||
enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ ->
|
||||
enableOrNotTheConfirmationButton()
|
||||
}
|
||||
}
|
||||
|
||||
private val onEditorActionListener = object : TextView.OnEditorActionListener {
|
||||
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
|
||||
if (actionId == IME_ACTION_DONE) {
|
||||
verifyCheckboxesAndLoadDatabase()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
// If the database isn't accessible make sure to clear the password field, if it
|
||||
// was saved in the instance state
|
||||
if (Database.getInstance().loaded) {
|
||||
setEmptyViews()
|
||||
}
|
||||
|
||||
// For check shutdown
|
||||
super.onResume()
|
||||
|
||||
initUriFromIntent()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
private fun initUriFromIntent() {
|
||||
|
||||
val databaseUri: Uri?
|
||||
val keyFileUri: Uri?
|
||||
|
||||
// If is a view intent
|
||||
val action = intent.action
|
||||
if (action != null && action == VIEW_INTENT) {
|
||||
|
||||
val databaseUriRetrieve = intent.data
|
||||
// Stop activity here if we can't verify database URI
|
||||
if (!UriUtil.verifyFileUri(databaseUriRetrieve)) {
|
||||
Log.e(TAG, "File URI not validate")
|
||||
finish()
|
||||
}
|
||||
databaseUri = databaseUriRetrieve
|
||||
keyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
|
||||
|
||||
} else {
|
||||
databaseUri = intent.getParcelableExtra(KEY_FILENAME)
|
||||
keyFileUri = intent.getParcelableExtra(KEY_KEYFILE)
|
||||
}
|
||||
|
||||
// Post init uri with KeyFile if needed
|
||||
if (mRememberKeyFile && (keyFileUri == null || keyFileUri.toString().isEmpty())) {
|
||||
// Retrieve KeyFile in a thread
|
||||
databaseUri?.let { databaseUriNotNull ->
|
||||
FileDatabaseHistoryAction.getInstance(applicationContext)
|
||||
.getKeyFileUriByDatabaseUri(databaseUriNotNull) {
|
||||
onPostInitUri(databaseUri, it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onPostInitUri(databaseUri, keyFileUri)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
||||
mDatabaseFileUri = databaseFileUri
|
||||
|
||||
// Define title
|
||||
databaseFileUri?.let {
|
||||
FileDatabaseInfo(this, it).retrieveDatabaseTitle { title ->
|
||||
filenameView?.text = title
|
||||
}
|
||||
}
|
||||
|
||||
// Define Key File text
|
||||
val keyUriString = keyFileUri?.toString() ?: ""
|
||||
if (keyUriString.isNotEmpty() && mRememberKeyFile) { // Bug KeepassDX #18
|
||||
populateKeyFileTextView(keyUriString)
|
||||
}
|
||||
|
||||
// Define listeners for default database checkbox and validate button
|
||||
checkboxDefaultDatabaseView?.setOnCheckedChangeListener { _, isChecked ->
|
||||
var newDefaultFileName: Uri? = null
|
||||
if (isChecked) {
|
||||
newDefaultFileName = databaseFileUri ?: newDefaultFileName
|
||||
}
|
||||
|
||||
newDefaultFileName?.let {
|
||||
prefs?.edit()?.apply {
|
||||
putString(KEY_DEFAULT_DATABASE_PATH, newDefaultFileName.toString())
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
val backupManager = BackupManager(this@PasswordActivity)
|
||||
backupManager.dataChanged()
|
||||
}
|
||||
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
|
||||
|
||||
// Retrieve settings for default database
|
||||
val defaultFilename = prefs?.getString(KEY_DEFAULT_DATABASE_PATH, "")
|
||||
if (databaseFileUri != null
|
||||
&& databaseFileUri.path != null && databaseFileUri.path!!.isNotEmpty()
|
||||
&& databaseFileUri == UriUtil.parse(defaultFilename)) {
|
||||
checkboxDefaultDatabaseView?.isChecked = true
|
||||
}
|
||||
|
||||
// If Activity is launch with a password and want to open directly
|
||||
val intent = intent
|
||||
val password = intent.getStringExtra(KEY_PASSWORD)
|
||||
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
|
||||
if (password != null) {
|
||||
populatePasswordTextView(password)
|
||||
}
|
||||
if (launchImmediately) {
|
||||
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
|
||||
} else {
|
||||
// Init FingerPrint elements
|
||||
var fingerPrintInit = false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (PreferencesUtil.isBiometricUnlockEnable(this)) {
|
||||
|
||||
advancedUnlockInfoView?.setOnClickListener {
|
||||
FingerPrintExplanationDialog().show(supportFragmentManager, "fingerPrintExplanationDialog")
|
||||
}
|
||||
|
||||
if (advancedUnlockedManager == null && databaseFileUri != null) {
|
||||
advancedUnlockedManager = AdvancedUnlockedManager(this,
|
||||
databaseFileUri,
|
||||
advancedUnlockInfoView,
|
||||
checkboxPasswordView,
|
||||
enableButtonOnCheckedChangeListener,
|
||||
passwordView,
|
||||
{ passwordEncrypted, ivSpec ->
|
||||
// Load the database if password is registered with biometric
|
||||
if (passwordEncrypted != null && ivSpec != null) {
|
||||
verifyCheckboxesAndLoadDatabase(
|
||||
CipherDatabaseEntity(
|
||||
databaseFileUri.toString(),
|
||||
passwordEncrypted,
|
||||
ivSpec)
|
||||
)
|
||||
}
|
||||
},
|
||||
{ passwordDecrypted ->
|
||||
// Load the database if password is retrieve from biometric
|
||||
passwordDecrypted?.let {
|
||||
// Retrieve from fingerprint
|
||||
verifyKeyFileCheckboxAndLoadDatabase(it)
|
||||
}
|
||||
})
|
||||
}
|
||||
advancedUnlockedManager?.initBiometric()
|
||||
fingerPrintInit = true
|
||||
} else {
|
||||
advancedUnlockedManager?.destroy()
|
||||
}
|
||||
}
|
||||
if (!fingerPrintInit) {
|
||||
checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
||||
}
|
||||
checkboxKeyFileView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
||||
}
|
||||
|
||||
enableOrNotTheConfirmationButton()
|
||||
}
|
||||
|
||||
private fun enableOrNotTheConfirmationButton() {
|
||||
// Enable or not the open button if setting is checked
|
||||
if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) {
|
||||
checkboxPasswordView?.let {
|
||||
confirmButtonView?.isEnabled = (checkboxPasswordView?.isChecked == true
|
||||
|| checkboxKeyFileView?.isChecked == true)
|
||||
}
|
||||
} else {
|
||||
confirmButtonView?.isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun setEmptyViews() {
|
||||
populatePasswordTextView(null)
|
||||
// Bug KeepassDX #18
|
||||
if (!mRememberKeyFile) {
|
||||
populateKeyFileTextView(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun populatePasswordTextView(text: String?) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
passwordView?.setText("")
|
||||
if (checkboxPasswordView?.isChecked == true)
|
||||
checkboxPasswordView?.isChecked = false
|
||||
} else {
|
||||
passwordView?.setText(text)
|
||||
if (checkboxPasswordView?.isChecked != true)
|
||||
checkboxPasswordView?.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateKeyFileTextView(text: String?) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
keyFileView?.setText("")
|
||||
if (checkboxKeyFileView?.isChecked == true)
|
||||
checkboxKeyFileView?.isChecked = false
|
||||
} else {
|
||||
keyFileView?.setText(text)
|
||||
if (checkboxKeyFileView?.isChecked != true)
|
||||
checkboxKeyFileView?.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
advancedUnlockedManager?.pause()
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
advancedUnlockedManager?.destroy()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||
val password: String? = passwordView?.text?.toString()
|
||||
val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString())
|
||||
verifyCheckboxesAndLoadDatabase(password, keyFile, cipherDatabaseEntity)
|
||||
}
|
||||
|
||||
private fun verifyCheckboxesAndLoadDatabase(password: String?,
|
||||
keyFile: Uri?,
|
||||
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||
val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password
|
||||
val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
|
||||
loadDatabase(keyPassword, keyFileUri, cipherDatabaseEntity)
|
||||
}
|
||||
|
||||
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) {
|
||||
val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString())
|
||||
val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
|
||||
loadDatabase(password, keyFileUri)
|
||||
}
|
||||
|
||||
private fun removePassword() {
|
||||
passwordView?.setText("")
|
||||
checkboxPasswordView?.isChecked = false
|
||||
}
|
||||
|
||||
private fun loadDatabase(password: String?, keyFile: Uri?, cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||
|
||||
runOnUiThread {
|
||||
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
|
||||
removePassword()
|
||||
}
|
||||
}
|
||||
|
||||
// Clear before we load
|
||||
val database = Database.getInstance()
|
||||
database.closeAndClear(applicationContext.filesDir)
|
||||
|
||||
mDatabaseFileUri?.let { databaseUri ->
|
||||
// Show the progress dialog and load the database
|
||||
ProgressDialogThread(this,
|
||||
{ progressTaskUpdater ->
|
||||
LoadDatabaseRunnable(
|
||||
WeakReference(this@PasswordActivity),
|
||||
database,
|
||||
databaseUri,
|
||||
password,
|
||||
keyFile,
|
||||
progressTaskUpdater,
|
||||
AfterLoadingDatabase(database, password, cipherDatabaseEntity))
|
||||
},
|
||||
R.string.loading_database).start()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after verify and try to opening the database
|
||||
*/
|
||||
private inner class AfterLoadingDatabase(val database: Database, val password: String?,
|
||||
val cipherDatabaseEntity: CipherDatabaseEntity? = null)
|
||||
: ActionRunnable() {
|
||||
|
||||
override fun onFinishRun(result: Result) {
|
||||
runOnUiThread {
|
||||
// Recheck fingerprint if error
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (PreferencesUtil.isBiometricUnlockEnable(this@PasswordActivity)) {
|
||||
// Stay with the same mode and init it
|
||||
advancedUnlockedManager?.initBiometricMode()
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isSuccess) {
|
||||
// Remove the password in view in all cases
|
||||
removePassword()
|
||||
|
||||
// Register the biometric
|
||||
if (cipherDatabaseEntity != null) {
|
||||
CipherDatabaseAction.getInstance(this@PasswordActivity)
|
||||
.addOrUpdateCipherDatabase(cipherDatabaseEntity) {
|
||||
checkAndLaunchGroupActivity(database, password)
|
||||
}
|
||||
} else {
|
||||
checkAndLaunchGroupActivity(database, password)
|
||||
}
|
||||
|
||||
} else {
|
||||
if (result.message != null && result.message!!.isNotEmpty()) {
|
||||
Snackbar.make(activity_password_coordinator_layout, result.message!!, Snackbar.LENGTH_LONG).asError().show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkAndLaunchGroupActivity(database: Database, password: String?) {
|
||||
if (database.validatePasswordEncoding(password)) {
|
||||
launchGroupActivity()
|
||||
} else {
|
||||
PasswordEncodingDialogFragment().apply {
|
||||
positiveButtonClickListener = DialogInterface.OnClickListener { _, _ ->
|
||||
launchGroupActivity()
|
||||
}
|
||||
show(supportFragmentManager, "passwordEncodingTag")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchGroupActivity() {
|
||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||
{
|
||||
GroupActivity.launch(this@PasswordActivity, readOnly)
|
||||
},
|
||||
{
|
||||
GroupActivity.launchForKeyboarSelection(this@PasswordActivity, readOnly)
|
||||
// Do not keep history
|
||||
finish()
|
||||
},
|
||||
{ assistStructure ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
GroupActivity.launchForAutofillResult(this@PasswordActivity, assistStructure, readOnly)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
val inflater = menuInflater
|
||||
// Read menu
|
||||
inflater.inflate(R.menu.open_file, menu)
|
||||
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
|
||||
|
||||
MenuUtil.defaultMenuInflater(inflater, menu)
|
||||
|
||||
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// Fingerprint menu
|
||||
advancedUnlockedManager?.inflateOptionsMenu(inflater, menu)
|
||||
}
|
||||
|
||||
super.onCreateOptionsMenu(menu)
|
||||
|
||||
// Show education views
|
||||
Handler().post { performedNextEducation(PasswordActivityEducation(this), menu) }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
|
||||
menu: Menu) {
|
||||
val unlockEducationPerformed = toolbar != null
|
||||
&& passwordActivityEducation.checkAndPerformedUnlockEducation(
|
||||
toolbar!!,
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
},
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
})
|
||||
if (!unlockEducationPerformed) {
|
||||
|
||||
val readOnlyEducationPerformed = toolbar != null
|
||||
&& toolbar!!.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
|
||||
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
|
||||
toolbar!!.findViewById(R.id.menu_open_file_read_mode_key),
|
||||
{
|
||||
onOptionsItemSelected(menu.findItem(R.id.menu_open_file_read_mode_key))
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
},
|
||||
{
|
||||
performedNextEducation(passwordActivityEducation, menu)
|
||||
})
|
||||
|
||||
if (!readOnlyEducationPerformed) {
|
||||
|
||||
val biometricCanAuthenticate = BiometricManager.from(this).canAuthenticate()
|
||||
// fingerprintEducationPerformed
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& PreferencesUtil.isBiometricUnlockEnable(applicationContext)
|
||||
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
||||
&& advancedUnlockInfoView != null && advancedUnlockInfoView?.unlockIconImageView != null
|
||||
&& passwordActivityEducation.checkAndPerformedFingerprintEducation(advancedUnlockInfoView?.unlockIconImageView!!)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeOpenFileReadIcon(togglePassword: MenuItem) {
|
||||
if (readOnly) {
|
||||
togglePassword.setTitle(R.string.menu_file_selection_read_only)
|
||||
togglePassword.setIcon(R.drawable.ic_read_only_white_24dp)
|
||||
} else {
|
||||
togglePassword.setTitle(R.string.menu_open_file_read_and_write)
|
||||
togglePassword.setIcon(R.drawable.ic_read_write_white_24dp)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> finish()
|
||||
R.id.menu_open_file_read_mode_key -> {
|
||||
readOnly = !readOnly
|
||||
changeOpenFileReadIcon(item)
|
||||
}
|
||||
R.id.menu_fingerprint_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
advancedUnlockedManager?.deleteEntryKey()
|
||||
}
|
||||
else -> return MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
// To get entry in result
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
var keyFileResult = false
|
||||
mOpenFileHelper?.let {
|
||||
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
populateKeyFileTextView(uri.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!keyFileResult) {
|
||||
// this block if not a key file response
|
||||
when (resultCode) {
|
||||
LockingActivity.RESULT_EXIT_LOCK, Activity.RESULT_CANCELED -> {
|
||||
setEmptyViews()
|
||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = PasswordActivity::class.java.name
|
||||
|
||||
const val KEY_DEFAULT_DATABASE_PATH = "KEY_DEFAULT_DATABASE_PATH"
|
||||
|
||||
private const val KEY_FILENAME = "fileName"
|
||||
private const val KEY_KEYFILE = "keyFile"
|
||||
private const val VIEW_INTENT = "android.intent.action.VIEW"
|
||||
|
||||
private const val KEY_PASSWORD = "password"
|
||||
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||
|
||||
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
|
||||
intentBuildLauncher: (Intent) -> Unit) {
|
||||
val intent = Intent(activity, PasswordActivity::class.java)
|
||||
intent.putExtra(KEY_FILENAME, databaseFile)
|
||||
if (keyFile != null)
|
||||
intent.putExtra(KEY_KEYFILE, keyFile)
|
||||
intentBuildLauncher.invoke(intent)
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Standard Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launch(
|
||||
activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Keyboard Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForKeyboardResult(
|
||||
activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
EntrySelectionHelper.startActivityForEntrySelection(activity, intent)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------
|
||||
* Autofill Launch
|
||||
* -------------------------
|
||||
*/
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launchForAutofillResult(
|
||||
activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
assistStructure: AssistStructure?) {
|
||||
if (assistStructure != null) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
AutofillHelper.startActivityForAutofillResult(
|
||||
activity,
|
||||
intent,
|
||||
assistStructure)
|
||||
}
|
||||
} else {
|
||||
launch(activity, databaseFile, keyFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package com.kunzisoft.keepass.activities;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil;
|
||||
|
||||
public class ReadOnlyHelper {
|
||||
|
||||
public static final String READ_ONLY_KEY = "READ_ONLY_KEY";
|
||||
|
||||
public static final boolean READ_ONLY_DEFAULT = false;
|
||||
|
||||
public static boolean retrieveReadOnlyFromInstanceStateOrPreference(Context context, Bundle savedInstanceState) {
|
||||
boolean readOnly;
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(READ_ONLY_KEY)) {
|
||||
readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY);
|
||||
} else {
|
||||
readOnly = PreferencesUtil.enableReadOnlyDatabase(context);
|
||||
}
|
||||
return readOnly;
|
||||
}
|
||||
|
||||
public static boolean retrieveReadOnlyFromInstanceStateOrArguments(Bundle savedInstanceState, Bundle arguments) {
|
||||
boolean readOnly = READ_ONLY_DEFAULT;
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(READ_ONLY_KEY)) {
|
||||
readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY);
|
||||
} else if (arguments != null
|
||||
&& arguments.containsKey(READ_ONLY_KEY)) {
|
||||
readOnly = arguments.getBoolean(READ_ONLY_KEY);
|
||||
}
|
||||
return readOnly;
|
||||
}
|
||||
|
||||
public static boolean retrieveReadOnlyFromInstanceStateOrIntent(Bundle savedInstanceState, Intent intent) {
|
||||
boolean readOnly = READ_ONLY_DEFAULT;
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(READ_ONLY_KEY)) {
|
||||
readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY);
|
||||
} else {
|
||||
if (intent != null)
|
||||
readOnly = intent.getBooleanExtra(READ_ONLY_KEY, READ_ONLY_DEFAULT);
|
||||
}
|
||||
return readOnly;
|
||||
}
|
||||
|
||||
public static void putReadOnlyInIntent(Intent intent, boolean readOnly) {
|
||||
intent.putExtra(READ_ONLY_KEY, readOnly);
|
||||
}
|
||||
|
||||
public static void putReadOnlyInBundle(Bundle bundle, boolean readOnly) {
|
||||
bundle.putBoolean(READ_ONLY_KEY, readOnly);
|
||||
}
|
||||
|
||||
public static void onSaveInstanceState(Bundle outState, boolean readOnly) {
|
||||
outState.putBoolean(READ_ONLY_KEY, readOnly);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.TextView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||
|
||||
private var mMasterPassword: String? = null
|
||||
private var mKeyFile: Uri? = null
|
||||
|
||||
private var rootView: View? = null
|
||||
|
||||
private var passwordCheckBox: CompoundButton? = null
|
||||
private var passwordView: TextView? = null
|
||||
private var passwordRepeatTextInputLayout: TextInputLayout? = null
|
||||
private var passwordRepeatView: TextView? = null
|
||||
|
||||
private var keyFileTextInputLayout: TextInputLayout? = null
|
||||
private var keyFileCheckBox: CompoundButton? = null
|
||||
private var keyFileView: TextView? = null
|
||||
|
||||
private var mListener: AssignPasswordDialogListener? = null
|
||||
|
||||
private var mOpenFileHelper: OpenFileHelper? = null
|
||||
|
||||
private val passwordTextWatcher = object : TextWatcher {
|
||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun afterTextChanged(editable: Editable) {
|
||||
passwordCheckBox?.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
private val keyFileTextWatcher = object : TextWatcher {
|
||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun afterTextChanged(editable: Editable) {
|
||||
keyFileCheckBox?.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
interface AssignPasswordDialogListener {
|
||||
fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?,
|
||||
keyFileChecked: Boolean, keyFile: Uri?)
|
||||
fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean, masterPassword: String?,
|
||||
keyFileChecked: Boolean, keyFile: Uri?)
|
||||
}
|
||||
|
||||
override fun onAttach(activity: Context) {
|
||||
super.onAttach(activity)
|
||||
try {
|
||||
mListener = activity as AssignPasswordDialogListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(activity.toString()
|
||||
+ " must implement " + AssignPasswordDialogListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
val inflater = activity.layoutInflater
|
||||
|
||||
rootView = inflater.inflate(R.layout.fragment_set_password, null)
|
||||
builder.setView(rootView)
|
||||
.setTitle(R.string.assign_master_key)
|
||||
// Add action buttons
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
|
||||
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
||||
passwordView = rootView?.findViewById(R.id.pass_password)
|
||||
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
|
||||
passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password)
|
||||
|
||||
keyFileTextInputLayout = rootView?.findViewById(R.id.keyfile_input_layout)
|
||||
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
||||
keyFileView = rootView?.findViewById(R.id.pass_keyfile)
|
||||
|
||||
mOpenFileHelper = OpenFileHelper(this)
|
||||
rootView?.findViewById<View>(R.id.browse_button)?.setOnClickListener { view ->
|
||||
mOpenFileHelper?.openFileOnClickViewListener?.onClick(view) }
|
||||
|
||||
val dialog = builder.create()
|
||||
|
||||
if (passwordCheckBox != null && keyFileCheckBox!= null) {
|
||||
dialog.setOnShowListener { dialog1 ->
|
||||
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
positiveButton.setOnClickListener {
|
||||
|
||||
mMasterPassword = ""
|
||||
mKeyFile = null
|
||||
|
||||
var error = verifyPassword() || verifyFile()
|
||||
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
|
||||
error = true
|
||||
showNoKeyConfirmationDialog()
|
||||
}
|
||||
if (!error) {
|
||||
mListener?.onAssignKeyDialogPositiveClick(
|
||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
|
||||
negativeButton.setOnClickListener {
|
||||
mListener?.onAssignKeyDialogNegativeClick(
|
||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// To check checkboxes if a text is present
|
||||
passwordView?.addTextChangedListener(passwordTextWatcher)
|
||||
keyFileView?.addTextChangedListener(keyFileTextWatcher)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
passwordView?.removeTextChangedListener(passwordTextWatcher)
|
||||
keyFileView?.removeTextChangedListener(keyFileTextWatcher)
|
||||
}
|
||||
|
||||
private fun verifyPassword(): Boolean {
|
||||
var error = false
|
||||
if (passwordCheckBox != null
|
||||
&& passwordCheckBox!!.isChecked
|
||||
&& passwordView != null
|
||||
&& passwordRepeatView != null) {
|
||||
mMasterPassword = passwordView!!.text.toString()
|
||||
val confPassword = passwordRepeatView!!.text.toString()
|
||||
|
||||
// Verify that passwords match
|
||||
if (mMasterPassword != confPassword) {
|
||||
error = true
|
||||
// Passwords do not match
|
||||
passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match)
|
||||
}
|
||||
|
||||
if (mMasterPassword == null || mMasterPassword!!.isEmpty()) {
|
||||
error = true
|
||||
showEmptyPasswordConfirmationDialog()
|
||||
}
|
||||
}
|
||||
return error
|
||||
}
|
||||
|
||||
private fun verifyFile(): Boolean {
|
||||
var error = false
|
||||
if (keyFileCheckBox != null
|
||||
&& keyFileCheckBox!!.isChecked) {
|
||||
|
||||
UriUtil.parse(keyFileView?.text?.toString())?.let { uri ->
|
||||
mKeyFile = uri
|
||||
} ?: run {
|
||||
error = true
|
||||
keyFileTextInputLayout?.error = getString(R.string.error_nokeyfile)
|
||||
}
|
||||
}
|
||||
return error
|
||||
}
|
||||
|
||||
private fun showEmptyPasswordConfirmationDialog() {
|
||||
activity?.let {
|
||||
val builder = AlertDialog.Builder(it)
|
||||
builder.setMessage(R.string.warning_empty_password)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
if (!verifyFile()) {
|
||||
mListener?.onAssignKeyDialogPositiveClick(
|
||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
||||
this@AssignMasterKeyDialogFragment.dismiss()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
builder.create().show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showNoKeyConfirmationDialog() {
|
||||
activity?.let {
|
||||
val builder = AlertDialog.Builder(it)
|
||||
builder.setMessage(R.string.warning_no_encryption_key)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
mListener?.onAssignKeyDialogPositiveClick(
|
||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
||||
this@AssignMasterKeyDialogFragment.dismiss()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
builder.create().show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
||||
) { uri ->
|
||||
uri?.let { pathUri ->
|
||||
keyFileCheckBox?.isChecked = true
|
||||
keyFileView?.text = pathUri.toString()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
class BrowserDialogFragment : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
// Get the layout inflater
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_browser_install, null)
|
||||
builder.setView(root)
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
|
||||
val textDescription = root.findViewById<TextView>(R.id.file_manager_install_description)
|
||||
textDescription.text = getString(R.string.file_manager_install_description)
|
||||
|
||||
val market = root.findViewById<Button>(R.id.file_manager_install_play_store)
|
||||
market.setOnClickListener {
|
||||
UriUtil.gotoUrl(context!!, R.string.file_manager_play_store)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
val web = root.findViewById<Button>(R.id.file_manager_install_f_droid)
|
||||
web.setOnClickListener {
|
||||
UriUtil.gotoUrl(context!!, R.string.file_manager_f_droid)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.view.View
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.biometric.FingerPrintAnimatedVector
|
||||
import com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
class FingerPrintExplanationDialog : DialogFragment() {
|
||||
|
||||
private var fingerPrintAnimatedVector: FingerPrintAnimatedVector? = null
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
val inflater = activity.layoutInflater
|
||||
|
||||
val rootView = inflater.inflate(R.layout.fragment_fingerprint_explanation, null)
|
||||
|
||||
rootView.findViewById<View>(R.id.fingerprint_setting_link_text).setOnClickListener {
|
||||
startActivity(Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS))
|
||||
}
|
||||
|
||||
rootView.findViewById<View>(R.id.auto_open_biometric_prompt_button).setOnClickListener {
|
||||
startActivity(Intent(activity, SettingsAdvancedUnlockActivity::class.java))
|
||||
}
|
||||
|
||||
fingerPrintAnimatedVector = FingerPrintAnimatedVector(activity,
|
||||
rootView.findViewById(R.id.biometric_image))
|
||||
|
||||
builder.setView(rootView)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
fingerPrintAnimatedVector?.startScan()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
fingerPrintAnimatedVector?.stopScan()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.EditText
|
||||
import android.widget.SeekBar
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.password.PasswordGenerator
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||
|
||||
class GeneratePasswordDialogFragment : DialogFragment() {
|
||||
|
||||
private var mListener: GeneratePasswordListener? = null
|
||||
|
||||
private var root: View? = null
|
||||
private var lengthTextView: EditText? = null
|
||||
private var passwordInputLayoutView: TextInputLayout? = null
|
||||
private var passwordView: EditText? = null
|
||||
|
||||
private var uppercaseBox: CompoundButton? = null
|
||||
private var lowercaseBox: CompoundButton? = null
|
||||
private var digitsBox: CompoundButton? = null
|
||||
private var minusBox: CompoundButton? = null
|
||||
private var underlineBox: CompoundButton? = null
|
||||
private var spaceBox: CompoundButton? = null
|
||||
private var specialsBox: CompoundButton? = null
|
||||
private var bracketsBox: CompoundButton? = null
|
||||
private var extendedBox: CompoundButton? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
try {
|
||||
mListener = context as GeneratePasswordListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + GeneratePasswordListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
val inflater = activity.layoutInflater
|
||||
root = inflater.inflate(R.layout.fragment_generate_password, null)
|
||||
|
||||
passwordInputLayoutView = root?.findViewById(R.id.password_input_layout)
|
||||
passwordView = root?.findViewById(R.id.password)
|
||||
passwordView?.applyFontVisibility()
|
||||
|
||||
lengthTextView = root?.findViewById(R.id.length)
|
||||
|
||||
uppercaseBox = root?.findViewById(R.id.cb_uppercase)
|
||||
lowercaseBox = root?.findViewById(R.id.cb_lowercase)
|
||||
digitsBox = root?.findViewById(R.id.cb_digits)
|
||||
minusBox = root?.findViewById(R.id.cb_minus)
|
||||
underlineBox = root?.findViewById(R.id.cb_underline)
|
||||
spaceBox = root?.findViewById(R.id.cb_space)
|
||||
specialsBox = root?.findViewById(R.id.cb_specials)
|
||||
bracketsBox = root?.findViewById(R.id.cb_brackets)
|
||||
extendedBox = root?.findViewById(R.id.cb_extended)
|
||||
|
||||
assignDefaultCharacters()
|
||||
|
||||
val seekBar = root?.findViewById<SeekBar>(R.id.seekbar_length)
|
||||
seekBar?.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||
lengthTextView?.setText(progress.toString())
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||
})
|
||||
|
||||
context?.let { context ->
|
||||
seekBar?.progress = PreferencesUtil.getDefaultPasswordLength(context)
|
||||
}
|
||||
|
||||
root?.findViewById<Button>(R.id.generate_password_button)
|
||||
?.setOnClickListener { fillPassword() }
|
||||
|
||||
builder.setView(root)
|
||||
.setPositiveButton(R.string.accept) { _, _ ->
|
||||
val bundle = Bundle()
|
||||
bundle.putString(KEY_PASSWORD_ID, passwordView!!.text.toString())
|
||||
mListener?.acceptPassword(bundle)
|
||||
|
||||
dismiss()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
||||
val bundle = Bundle()
|
||||
mListener?.cancelPassword(bundle)
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
// Pre-populate a password to possibly save the user a few clicks
|
||||
fillPassword()
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun assignDefaultCharacters() {
|
||||
uppercaseBox?.isChecked = false
|
||||
lowercaseBox?.isChecked = false
|
||||
digitsBox?.isChecked = false
|
||||
minusBox?.isChecked = false
|
||||
underlineBox?.isChecked = false
|
||||
spaceBox?.isChecked = false
|
||||
specialsBox?.isChecked = false
|
||||
bracketsBox?.isChecked = false
|
||||
extendedBox?.isChecked = false
|
||||
|
||||
context?.let { context ->
|
||||
PreferencesUtil.getDefaultPasswordCharacters(context)?.let { charSet ->
|
||||
for (passwordChar in charSet) {
|
||||
when (passwordChar) {
|
||||
getString(R.string.value_password_uppercase) -> uppercaseBox?.isChecked = true
|
||||
getString(R.string.value_password_lowercase) -> lowercaseBox?.isChecked = true
|
||||
getString(R.string.value_password_digits) -> digitsBox?.isChecked = true
|
||||
getString(R.string.value_password_minus) -> minusBox?.isChecked = true
|
||||
getString(R.string.value_password_underline) -> underlineBox?.isChecked = true
|
||||
getString(R.string.value_password_space) -> spaceBox?.isChecked = true
|
||||
getString(R.string.value_password_special) -> specialsBox?.isChecked = true
|
||||
getString(R.string.value_password_brackets) -> bracketsBox?.isChecked = true
|
||||
getString(R.string.value_password_extended) -> extendedBox?.isChecked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fillPassword() {
|
||||
root?.findViewById<EditText>(R.id.password)?.setText(generatePassword())
|
||||
}
|
||||
|
||||
fun generatePassword(): String {
|
||||
var password = ""
|
||||
try {
|
||||
val length = Integer.valueOf(root?.findViewById<EditText>(R.id.length)?.text.toString())
|
||||
|
||||
password = PasswordGenerator(resources).generatePassword(length,
|
||||
uppercaseBox?.isChecked == true,
|
||||
lowercaseBox?.isChecked == true,
|
||||
digitsBox?.isChecked == true,
|
||||
minusBox?.isChecked == true,
|
||||
underlineBox?.isChecked == true,
|
||||
spaceBox?.isChecked == true,
|
||||
specialsBox?.isChecked == true,
|
||||
bracketsBox?.isChecked == true,
|
||||
extendedBox?.isChecked == true)
|
||||
} catch (e: NumberFormatException) {
|
||||
passwordInputLayoutView?.error = getString(R.string.error_wrong_length)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
passwordInputLayoutView?.error = e.message
|
||||
}
|
||||
|
||||
return password
|
||||
}
|
||||
|
||||
interface GeneratePasswordListener {
|
||||
fun acceptPassword(bundle: Bundle)
|
||||
fun cancelPassword(bundle: Bundle)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val KEY_PASSWORD_ID = "KEY_PASSWORD_ID"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION
|
||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.GroupVersioned
|
||||
import com.kunzisoft.keepass.database.element.PwIcon
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
|
||||
class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconPickerListener {
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
|
||||
private var editGroupListener: EditGroupListener? = null
|
||||
|
||||
private var editGroupDialogAction: EditGroupDialogAction? = null
|
||||
private var nameGroup: String? = null
|
||||
private var iconGroup: PwIcon? = null
|
||||
|
||||
private var nameTextLayoutView: TextInputLayout? = null
|
||||
private var nameTextView: TextView? = null
|
||||
private var iconButtonView: ImageView? = null
|
||||
private var iconColor: Int = 0
|
||||
|
||||
enum class EditGroupDialogAction {
|
||||
CREATION, UPDATE, NONE;
|
||||
|
||||
companion object {
|
||||
fun getActionFromOrdinal(ordinal: Int): EditGroupDialogAction {
|
||||
return values()[ordinal]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
// Verify that the host activity implements the callback interface
|
||||
try {
|
||||
// Instantiate the NoticeDialogListener so we can send events to the host
|
||||
editGroupListener = context as EditGroupListener
|
||||
} catch (e: ClassCastException) {
|
||||
// The activity doesn't implement the interface, throw exception
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + GroupEditDialogFragment::class.java.name)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_group_edit, null)
|
||||
nameTextLayoutView = root?.findViewById(R.id.group_edit_name_container)
|
||||
nameTextView = root?.findViewById(R.id.group_edit_name)
|
||||
iconButtonView = root?.findViewById(R.id.group_edit_icon_button)
|
||||
|
||||
// Retrieve the textColor to tint the icon
|
||||
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
iconColor = ta.getColor(0, Color.WHITE)
|
||||
ta.recycle()
|
||||
|
||||
// Init elements
|
||||
mDatabase = Database.getInstance()
|
||||
editGroupDialogAction = EditGroupDialogAction.NONE
|
||||
nameGroup = ""
|
||||
iconGroup = mDatabase?.iconFactory?.folderIcon
|
||||
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(KEY_ACTION_ID)
|
||||
&& savedInstanceState.containsKey(KEY_NAME)
|
||||
&& savedInstanceState.containsKey(KEY_ICON)) {
|
||||
editGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID))
|
||||
nameGroup = savedInstanceState.getString(KEY_NAME)
|
||||
iconGroup = savedInstanceState.getParcelable(KEY_ICON)
|
||||
|
||||
} else {
|
||||
arguments?.apply {
|
||||
if (containsKey(KEY_ACTION_ID))
|
||||
editGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID))
|
||||
|
||||
if (containsKey(KEY_NAME) && containsKey(KEY_ICON)) {
|
||||
nameGroup = getString(KEY_NAME)
|
||||
iconGroup = getParcelable(KEY_ICON)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// populate the name
|
||||
nameTextView?.text = nameGroup
|
||||
// populate the icon
|
||||
assignIconView()
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setView(root)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
||||
editGroupListener?.cancelEditGroup(
|
||||
editGroupDialogAction,
|
||||
nameTextView?.text?.toString(),
|
||||
iconGroup)
|
||||
}
|
||||
|
||||
iconButtonView?.setOnClickListener { _ ->
|
||||
fragmentManager?.let {
|
||||
IconPickerDialogFragment().show(it, "IconPickerDialogFragment")
|
||||
}
|
||||
}
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// To prevent auto dismiss
|
||||
val d = dialog as AlertDialog?
|
||||
if (d != null) {
|
||||
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
|
||||
positiveButton.setOnClickListener {
|
||||
if (isValid()) {
|
||||
editGroupListener?.approveEditGroup(
|
||||
editGroupDialogAction,
|
||||
nameTextView?.text?.toString(),
|
||||
iconGroup)
|
||||
d.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun assignIconView() {
|
||||
if (mDatabase?.drawFactory != null && iconGroup != null) {
|
||||
iconButtonView?.assignDatabaseIcon(mDatabase?.drawFactory!!, iconGroup!!, iconColor)
|
||||
}
|
||||
}
|
||||
|
||||
override fun iconPicked(bundle: Bundle) {
|
||||
iconGroup = IconPickerDialogFragment.getIconStandardFromBundle(bundle)
|
||||
assignIconView()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putInt(KEY_ACTION_ID, editGroupDialogAction!!.ordinal)
|
||||
outState.putString(KEY_NAME, nameGroup)
|
||||
outState.putParcelable(KEY_ICON, iconGroup)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
private fun isValid(): Boolean {
|
||||
if (nameTextView?.text?.toString()?.isNotEmpty() != true) {
|
||||
nameTextLayoutView?.error = getString(R.string.error_no_name)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
interface EditGroupListener {
|
||||
fun approveEditGroup(action: EditGroupDialogAction?, name: String?, icon: PwIcon?)
|
||||
fun cancelEditGroup(action: EditGroupDialogAction?, name: String?, icon: PwIcon?)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
|
||||
|
||||
const val KEY_NAME = "KEY_NAME"
|
||||
const val KEY_ICON = "KEY_ICON"
|
||||
const val KEY_ACTION_ID = "KEY_ACTION_ID"
|
||||
|
||||
fun build(): GroupEditDialogFragment {
|
||||
val bundle = Bundle()
|
||||
bundle.putInt(KEY_ACTION_ID, CREATION.ordinal)
|
||||
val fragment = GroupEditDialogFragment()
|
||||
fragment.arguments = bundle
|
||||
return fragment
|
||||
}
|
||||
|
||||
fun build(group: GroupVersioned): GroupEditDialogFragment {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(KEY_NAME, group.title)
|
||||
bundle.putParcelable(KEY_ICON, group.icon)
|
||||
bundle.putInt(KEY_ACTION_ID, UPDATE.ordinal)
|
||||
val fragment = GroupEditDialogFragment()
|
||||
fragment.arguments = bundle
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.core.widget.ImageViewCompat
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.GridView
|
||||
import android.widget.ImageView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.database.element.PwIconStandard
|
||||
import com.kunzisoft.keepass.icons.IconPack
|
||||
import com.kunzisoft.keepass.icons.IconPackChooser
|
||||
|
||||
|
||||
class IconPickerDialogFragment : DialogFragment() {
|
||||
|
||||
private var iconPickerListener: IconPickerListener? = null
|
||||
private var iconPack: IconPack? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
try {
|
||||
iconPickerListener = context as IconPickerListener
|
||||
} catch (e: ClassCastException) {
|
||||
// The activity doesn't implement the interface, throw exception
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + IconPickerListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
iconPack = IconPackChooser.getSelectedIconPack(context!!)
|
||||
|
||||
// Inflate and set the layout for the dialog
|
||||
// Pass null as the parent view because its going in the dialog layout
|
||||
val root = activity.layoutInflater.inflate(R.layout.fragment_icon_picker, null)
|
||||
builder.setView(root)
|
||||
|
||||
val currIconGridView = root.findViewById<GridView>(R.id.IconGridView)
|
||||
currIconGridView.adapter = ImageAdapter(activity)
|
||||
|
||||
currIconGridView.setOnItemClickListener { _, _, position, _ ->
|
||||
val bundle = Bundle()
|
||||
bundle.putParcelable(KEY_ICON_STANDARD, PwIconStandard(position))
|
||||
iconPickerListener?.iconPicked(bundle)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
builder.setNegativeButton(R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() }
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
inner class ImageAdapter internal constructor(private val context: Context) : BaseAdapter() {
|
||||
|
||||
override fun getCount(): Int {
|
||||
return iconPack?.numberOfIcons() ?: 0
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): Any? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val currentView: View = convertView
|
||||
?: (context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)
|
||||
.inflate(R.layout.item_icon, parent, false)
|
||||
|
||||
iconPack?.let { iconPack ->
|
||||
val iconImageView = currentView.findViewById<ImageView>(R.id.icon_image)
|
||||
iconImageView.setImageResource(iconPack.iconToResId(position))
|
||||
|
||||
// Assign color if icons are tintable
|
||||
if (iconPack.tintable()) {
|
||||
// Retrieve the textColor to tint the icon
|
||||
val ta = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
ImageViewCompat.setImageTintList(iconImageView, ColorStateList.valueOf(ta.getColor(0, Color.BLACK)))
|
||||
ta?.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
return currentView
|
||||
}
|
||||
}
|
||||
|
||||
interface IconPickerListener {
|
||||
fun iconPicked(bundle: Bundle)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_ICON_STANDARD = "KEY_ICON_STANDARD"
|
||||
|
||||
fun getIconStandardFromBundle(bundle: Bundle): PwIconStandard? {
|
||||
return bundle.getParcelable(KEY_ICON_STANDARD)
|
||||
}
|
||||
|
||||
fun launch(activity: StylishActivity) {
|
||||
// Create an instance of the dialog fragment and show it
|
||||
val dialog = IconPickerDialogFragment()
|
||||
dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.view.View
|
||||
import com.kunzisoft.keepass.BuildConfig
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
class KeyboardExplanationDialogFragment : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let {
|
||||
val builder = AlertDialog.Builder(activity!!)
|
||||
val inflater = activity!!.layoutInflater
|
||||
|
||||
val rootView = inflater.inflate(R.layout.fragment_keyboard_explanation, null)
|
||||
|
||||
rootView.findViewById<View>(R.id.keyboards_activate_device_setting_button)
|
||||
.setOnClickListener { launchActivateKeyboardSetting() }
|
||||
|
||||
val containerKeyboardSwitcher = rootView.findViewById<View>(R.id.container_keyboard_switcher)
|
||||
if (BuildConfig.CLOSED_STORE) {
|
||||
containerKeyboardSwitcher.setOnClickListener { UriUtil.gotoUrl(context!!, R.string.keyboard_switcher_play_store) }
|
||||
} else {
|
||||
containerKeyboardSwitcher.setOnClickListener { UriUtil.gotoUrl(context!!, R.string.keyboard_switcher_f_droid) }
|
||||
}
|
||||
|
||||
builder.setView(rootView)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun launchActivateKeyboardSetting() {
|
||||
val intent = Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)
|
||||
intent.addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
class PasswordEncodingDialogFragment : DialogFragment() {
|
||||
|
||||
var positiveButtonClickListener: DialogInterface.OnClickListener? = null
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning)
|
||||
builder.setPositiveButton(android.R.string.ok, positiveButtonClickListener)
|
||||
builder.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.cancel() }
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.BuildConfig
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
/**
|
||||
* Custom Dialog that asks the user to download the pro version or make a donation.
|
||||
*/
|
||||
class ProFeatureDialogFragment : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
// Use the Builder class for convenient dialog construction
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
val stringBuilder = SpannableStringBuilder()
|
||||
if (BuildConfig.CLOSED_STORE) {
|
||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_ad_free), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_buy_pro), FROM_HTML_MODE_LEGACY))
|
||||
builder.setPositiveButton(R.string.download) { _, _ ->
|
||||
UriUtil.gotoUrl(context!!, R.string.app_pro_url)
|
||||
}
|
||||
} else {
|
||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_feature_generosity), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_donation), FROM_HTML_MODE_LEGACY))
|
||||
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||
UriUtil.gotoUrl(context!!, R.string.contribution_url)
|
||||
}
|
||||
}
|
||||
builder.setMessage(stringBuilder)
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||
// Create the AlertDialog object and return it
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
class ReadOnlyDialog : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
// Use the Builder class for convenient dialog construction
|
||||
val builder = androidx.appcompat.app.AlertDialog.Builder(activity)
|
||||
|
||||
var warning = getString(R.string.read_only_warning)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
warning = warning + "\n\n" + getString(R.string.read_only_kitkat_warning)
|
||||
}
|
||||
builder.setMessage(warning)
|
||||
|
||||
builder.setPositiveButton(getString(android.R.string.ok)) { _, _ -> dismiss() }
|
||||
builder.setNegativeButton(getString(R.string.beta_dontask)) { _, _ ->
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val edit = prefs.edit()
|
||||
edit.putBoolean(getString(R.string.show_read_only_warning), false)
|
||||
edit.apply()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
// Create the AlertDialog object and return it
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.RadioGroup
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.SortNodeEnum
|
||||
|
||||
class SortDialogFragment : DialogFragment() {
|
||||
|
||||
private var mListener: SortSelectionListener? = null
|
||||
|
||||
private var mSortNodeEnum: SortNodeEnum = SortNodeEnum.DB
|
||||
@IdRes
|
||||
private var mCheckedId: Int = 0
|
||||
private var mGroupsBefore: Boolean = true
|
||||
private var mAscending: Boolean = true
|
||||
private var mRecycleBinBottom: Boolean = true
|
||||
|
||||
private var recycleBinBottomView: CompoundButton? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
try {
|
||||
mListener = context as SortSelectionListener
|
||||
} catch (e: ClassCastException) {
|
||||
throw ClassCastException(context.toString()
|
||||
+ " must implement " + SortSelectionListener::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
var recycleBinAllowed = false
|
||||
|
||||
arguments?.apply {
|
||||
if (containsKey(SORT_NODE_ENUM_BUNDLE_KEY))
|
||||
getString(SORT_NODE_ENUM_BUNDLE_KEY)?.let {
|
||||
mSortNodeEnum = SortNodeEnum.valueOf(it)
|
||||
}
|
||||
if (containsKey(SORT_ASCENDING_BUNDLE_KEY))
|
||||
mAscending = getBoolean(SORT_ASCENDING_BUNDLE_KEY)
|
||||
if (containsKey(SORT_GROUPS_BEFORE_BUNDLE_KEY))
|
||||
mGroupsBefore = getBoolean(SORT_GROUPS_BEFORE_BUNDLE_KEY)
|
||||
if (containsKey(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY)) {
|
||||
recycleBinAllowed = true
|
||||
mRecycleBinBottom = getBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY)
|
||||
}
|
||||
}
|
||||
|
||||
mCheckedId = retrieveViewFromEnum(mSortNodeEnum)
|
||||
|
||||
val rootView = activity.layoutInflater.inflate(R.layout.fragment_sort_selection, null)
|
||||
builder.setTitle(R.string.sort_menu)
|
||||
builder.setView(rootView)
|
||||
// Add action buttons
|
||||
.setPositiveButton(android.R.string.ok
|
||||
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum, mAscending, mGroupsBefore, mRecycleBinBottom) }
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||
|
||||
val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending)
|
||||
// Check if is ascending or descending
|
||||
ascendingView.isChecked = mAscending
|
||||
ascendingView.setOnCheckedChangeListener { _, isChecked -> mAscending = isChecked }
|
||||
|
||||
val groupsBeforeView = rootView.findViewById<CompoundButton>(R.id.sort_selection_groups_before)
|
||||
// Check if groups before
|
||||
groupsBeforeView.isChecked = mGroupsBefore
|
||||
groupsBeforeView.setOnCheckedChangeListener { _, isChecked -> mGroupsBefore = isChecked }
|
||||
|
||||
recycleBinBottomView = rootView.findViewById(R.id.sort_selection_recycle_bin_bottom)
|
||||
if (!recycleBinAllowed) {
|
||||
recycleBinBottomView?.visibility = View.GONE
|
||||
} else {
|
||||
// Check if recycle bin at the bottom
|
||||
recycleBinBottomView?.isChecked = mRecycleBinBottom
|
||||
recycleBinBottomView?.setOnCheckedChangeListener { _, isChecked -> mRecycleBinBottom = isChecked }
|
||||
|
||||
disableRecycleBinBottomOptionIfNaturalOrder()
|
||||
}
|
||||
|
||||
val sortSelectionRadioGroupView = rootView.findViewById<RadioGroup>(R.id.sort_selection_radio_group)
|
||||
// Check value by default
|
||||
sortSelectionRadioGroupView.check(mCheckedId)
|
||||
sortSelectionRadioGroupView.setOnCheckedChangeListener { _, checkedId ->
|
||||
mSortNodeEnum = retrieveSortEnumFromViewId(checkedId)
|
||||
disableRecycleBinBottomOptionIfNaturalOrder()
|
||||
}
|
||||
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun disableRecycleBinBottomOptionIfNaturalOrder() {
|
||||
// Disable recycle bin if natural order
|
||||
recycleBinBottomView?.isEnabled = mSortNodeEnum != SortNodeEnum.DB
|
||||
}
|
||||
|
||||
@IdRes
|
||||
private fun retrieveViewFromEnum(sortNodeEnum: SortNodeEnum): Int {
|
||||
return when (sortNodeEnum) {
|
||||
SortNodeEnum.DB -> R.id.sort_selection_db
|
||||
SortNodeEnum.TITLE -> R.id.sort_selection_title
|
||||
SortNodeEnum.USERNAME -> R.id.sort_selection_username
|
||||
SortNodeEnum.CREATION_TIME -> R.id.sort_selection_creation_time
|
||||
SortNodeEnum.LAST_MODIFY_TIME -> R.id.sort_selection_last_modify_time
|
||||
SortNodeEnum.LAST_ACCESS_TIME -> R.id.sort_selection_last_access_time
|
||||
}
|
||||
}
|
||||
|
||||
private fun retrieveSortEnumFromViewId(@IdRes checkedId: Int): SortNodeEnum {
|
||||
// Change enum
|
||||
return when (checkedId) {
|
||||
R.id.sort_selection_db -> SortNodeEnum.DB
|
||||
R.id.sort_selection_title -> SortNodeEnum.TITLE
|
||||
R.id.sort_selection_username -> SortNodeEnum.USERNAME
|
||||
R.id.sort_selection_creation_time -> SortNodeEnum.CREATION_TIME
|
||||
R.id.sort_selection_last_modify_time -> SortNodeEnum.LAST_MODIFY_TIME
|
||||
R.id.sort_selection_last_access_time -> SortNodeEnum.LAST_ACCESS_TIME
|
||||
else -> SortNodeEnum.TITLE
|
||||
}
|
||||
}
|
||||
|
||||
interface SortSelectionListener {
|
||||
fun onSortSelected(sortNodeEnum: SortNodeEnum,
|
||||
ascending: Boolean,
|
||||
groupsBefore: Boolean,
|
||||
recycleBinBottom: Boolean)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val SORT_NODE_ENUM_BUNDLE_KEY = "SORT_NODE_ENUM_BUNDLE_KEY"
|
||||
private const val SORT_ASCENDING_BUNDLE_KEY = "SORT_ASCENDING_BUNDLE_KEY"
|
||||
private const val SORT_GROUPS_BEFORE_BUNDLE_KEY = "SORT_GROUPS_BEFORE_BUNDLE_KEY"
|
||||
private const val SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY = "SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY"
|
||||
|
||||
private fun buildBundle(sortNodeEnum: SortNodeEnum,
|
||||
ascending: Boolean,
|
||||
groupsBefore: Boolean): Bundle {
|
||||
val bundle = Bundle()
|
||||
bundle.putString(SORT_NODE_ENUM_BUNDLE_KEY, sortNodeEnum.name)
|
||||
bundle.putBoolean(SORT_ASCENDING_BUNDLE_KEY, ascending)
|
||||
bundle.putBoolean(SORT_GROUPS_BEFORE_BUNDLE_KEY, groupsBefore)
|
||||
return bundle
|
||||
}
|
||||
|
||||
fun getInstance(sortNodeEnum: SortNodeEnum,
|
||||
ascending: Boolean,
|
||||
groupsBefore: Boolean): SortDialogFragment {
|
||||
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
|
||||
val fragment = SortDialogFragment()
|
||||
fragment.arguments = bundle
|
||||
return fragment
|
||||
}
|
||||
|
||||
fun getInstance(sortNodeEnum: SortNodeEnum,
|
||||
ascending: Boolean,
|
||||
groupsBefore: Boolean,
|
||||
recycleBinBottom: Boolean): SortDialogFragment {
|
||||
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
|
||||
bundle.putBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY, recycleBinBottom)
|
||||
val fragment = SortDialogFragment()
|
||||
fragment.arguments = bundle
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.text.Html
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.widget.TextView
|
||||
import androidx.core.text.HtmlCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
class UnavailableFeatureDialogFragment : DialogFragment() {
|
||||
private var minVersionRequired = Build.VERSION_CODES.M
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
arguments?.apply {
|
||||
if (containsKey(MIN_REQUIRED_VERSION_ARG))
|
||||
minVersionRequired = getInt(MIN_REQUIRED_VERSION_ARG)
|
||||
}
|
||||
|
||||
val rootView = activity.layoutInflater.inflate(R.layout.fragment_unavailable_feature, null)
|
||||
val messageView = rootView.findViewById<TextView>(R.id.unavailable_feature_message)
|
||||
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
val message = SpannableStringBuilder()
|
||||
message.append(getString(R.string.unavailable_feature_text))
|
||||
.append("\n\n")
|
||||
if (Build.VERSION.SDK_INT < minVersionRequired) {
|
||||
message.append(getString(R.string.unavailable_feature_version,
|
||||
androidNameFromApiNumber(Build.VERSION.SDK_INT, Build.VERSION.RELEASE),
|
||||
androidNameFromApiNumber(minVersionRequired)))
|
||||
message.append("\n\n")
|
||||
.append(HtmlCompat.fromHtml("<a href=\"https://source.android.com/setup/build-numbers\">CodeNames</a>", HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||
} else
|
||||
message.append(getString(R.string.unavailable_feature_hardware))
|
||||
|
||||
messageView.text = message
|
||||
messageView.movementMethod = LinkMovementMethod.getInstance()
|
||||
|
||||
builder.setView(rootView)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
|
||||
private fun androidNameFromApiNumber(apiNumber: Int, releaseVersion: String = ""): String {
|
||||
var version = releaseVersion
|
||||
val builder = StringBuilder()
|
||||
val fields = Build.VERSION_CODES::class.java.fields
|
||||
var apiName = ""
|
||||
for (field in fields) {
|
||||
val fieldName = field.name
|
||||
var fieldValue = -1
|
||||
try {
|
||||
fieldValue = field.getInt(Any())
|
||||
} catch (e: IllegalArgumentException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: IllegalAccessException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: NullPointerException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
if (fieldValue == apiNumber) {
|
||||
apiName = fieldName
|
||||
}
|
||||
}
|
||||
if (apiName.isEmpty()) {
|
||||
val mapper = arrayOf("ANDROID BASE", "ANDROID BASE 1.1", "CUPCAKE", "DONUT", "ECLAIR", "ECLAIR_0_1", "ECLAIR_MR1", "FROYO", "GINGERBREAD", "GINGERBREAD_MR1", "HONEYCOMB", "HONEYCOMB_MR1", "HONEYCOMB_MR2", "ICE_CREAM_SANDWICH", "ICE_CREAM_SANDWICH_MR1", "JELLY_BEAN", "JELLY_BEAN", "JELLY_BEAN", "KITKAT", "KITKAT", "LOLLIPOOP", "LOLLIPOOP_MR1", "MARSHMALLOW", "NOUGAT", "NOUGAT", "OREO", "OREO")
|
||||
val index = apiNumber - 1
|
||||
apiName = if (index < mapper.size) mapper[index] else "UNKNOWN_VERSION"
|
||||
}
|
||||
if (version.isEmpty()) {
|
||||
val versions = arrayOf("1.0", "1.1", "1.5", "1.6", "2.0", "2.0.1", "2.1", "2.2.X", "2.3", "2.3.3", "3.0", "3.1", "3.2.0", "4.0.1", "4.0.3", "4.1.0", "4.2.0", "4.3.0", "4.4", "4.4", "5.0", "5.1", "6.0", "7.0", "7.1", "8.0.0", "8.1.0")
|
||||
val index = apiNumber - 1
|
||||
version = if (index < versions.size) versions[index] else "UNKNOWN_VERSION"
|
||||
}
|
||||
|
||||
builder.append("\n\t")
|
||||
if (apiName.isNotEmpty())
|
||||
builder.append(apiName).append(" ")
|
||||
if (version.isNotEmpty())
|
||||
builder.append(version).append(" ")
|
||||
builder.append("(API ").append(apiNumber).append(")")
|
||||
builder.append("\n")
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val MIN_REQUIRED_VERSION_ARG = "MIN_REQUIRED_VERSION_ARG"
|
||||
|
||||
fun getInstance(minVersionRequired: Int): UnavailableFeatureDialogFragment {
|
||||
val fragment = UnavailableFeatureDialogFragment()
|
||||
val args = Bundle()
|
||||
args.putInt(MIN_REQUIRED_VERSION_ARG, minVersionRequired)
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.BuildConfig
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
/**
|
||||
* Custom Dialog that asks the user to download the pro version or make a donation.
|
||||
*/
|
||||
class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
// Use the Builder class for convenient dialog construction
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
val stringBuilder = SpannableStringBuilder()
|
||||
if (BuildConfig.CLOSED_STORE) {
|
||||
if (BuildConfig.FULL_VERSION) {
|
||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_thanks), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_rose), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_work_hard), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_upgrade), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() }
|
||||
} else {
|
||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_buy_pro), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||
builder.setPositiveButton(R.string.download) { _, _ ->
|
||||
UriUtil.gotoUrl(context!!, R.string.app_pro_url)
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||
}
|
||||
} else {
|
||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_contibute), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||
UriUtil.gotoUrl(context!!, R.string.contribution_url)
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||
}
|
||||
builder.setMessage(stringBuilder)
|
||||
// Create the AlertDialog object and return it
|
||||
return builder.create()
|
||||
}
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.kunzisoft.keepass.activities.helpers
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.assist.AssistStructure
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
|
||||
object EntrySelectionHelper {
|
||||
|
||||
private const val EXTRA_ENTRY_SELECTION_MODE = "com.kunzisoft.keepass.extra.ENTRY_SELECTION_MODE"
|
||||
private const val DEFAULT_ENTRY_SELECTION_MODE = false
|
||||
|
||||
fun startActivityForEntrySelection(activity: Activity, intent: Intent) {
|
||||
addEntrySelectionModeExtraInIntent(intent)
|
||||
// only to avoid visible flickering when redirecting
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
fun addEntrySelectionModeExtraInIntent(intent: Intent) {
|
||||
intent.putExtra(EXTRA_ENTRY_SELECTION_MODE, true)
|
||||
}
|
||||
|
||||
fun removeEntrySelectionModeFromIntent(intent: Intent) {
|
||||
intent.removeExtra(EXTRA_ENTRY_SELECTION_MODE)
|
||||
}
|
||||
|
||||
fun retrieveEntrySelectionModeFromIntent(intent: Intent): Boolean {
|
||||
return intent.getBooleanExtra(EXTRA_ENTRY_SELECTION_MODE, DEFAULT_ENTRY_SELECTION_MODE)
|
||||
}
|
||||
|
||||
fun doEntrySelectionAction(intent: Intent,
|
||||
standardAction: () -> Unit,
|
||||
keyboardAction: () -> Unit,
|
||||
autofillAction: (assistStructure: AssistStructure) -> Unit) {
|
||||
var assistStructureInit = false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AutofillHelper.retrieveAssistStructure(intent)?.let { assistStructure ->
|
||||
autofillAction.invoke(assistStructure)
|
||||
assistStructureInit = true
|
||||
}
|
||||
}
|
||||
if (!assistStructureInit) {
|
||||
if (intent.getBooleanExtra(EXTRA_ENTRY_SELECTION_MODE, DEFAULT_ENTRY_SELECTION_MODE)) {
|
||||
intent.removeExtra(EXTRA_ENTRY_SELECTION_MODE)
|
||||
keyboardAction.invoke()
|
||||
} else {
|
||||
standardAction.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.helpers
|
||||
|
||||
import android.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 androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
class OpenFileHelper {
|
||||
|
||||
private var activity: Activity? = null
|
||||
private var fragment: Fragment? = null
|
||||
|
||||
val openFileOnClickViewListener: OpenFileOnClickViewListener
|
||||
get() = OpenFileOnClickViewListener(null)
|
||||
|
||||
constructor(context: Activity) {
|
||||
this.activity = context
|
||||
this.fragment = null
|
||||
}
|
||||
|
||||
constructor(context: Fragment) {
|
||||
this.activity = context.activity
|
||||
this.fragment = context
|
||||
}
|
||||
|
||||
inner class OpenFileOnClickViewListener(private val dataUri: (() -> Uri?)?) : View.OnClickListener {
|
||||
|
||||
override fun onClick(v: View) {
|
||||
try {
|
||||
try {
|
||||
openActivityWithActionOpenDocument()
|
||||
} catch(e: Exception) {
|
||||
openActivityWithActionGetContent()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Enable to start the file picker activity", e)
|
||||
|
||||
// Open File picker if can't open activity
|
||||
if (lookForOpenIntentsFilePicker(dataUri?.invoke()))
|
||||
showBrowserDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openActivityWithActionOpenDocument() {
|
||||
val i = Intent(ACTION_OPEN_DOCUMENT)
|
||||
i.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
i.type = "*/*"
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
i.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||
} else {
|
||||
i.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(i, OPEN_DOC)
|
||||
else
|
||||
activity?.startActivityForResult(i, OPEN_DOC)
|
||||
}
|
||||
|
||||
private fun openActivityWithActionGetContent() {
|
||||
val i = Intent(Intent.ACTION_GET_CONTENT)
|
||||
i.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
i.type = "*/*"
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(i, GET_CONTENT)
|
||||
else
|
||||
activity?.startActivityForResult(i, GET_CONTENT)
|
||||
}
|
||||
|
||||
fun getOpenFileOnClickViewListener(dataUri: () -> Uri?): OpenFileOnClickViewListener {
|
||||
return OpenFileOnClickViewListener(dataUri)
|
||||
}
|
||||
|
||||
private fun lookForOpenIntentsFilePicker(dataUri: Uri?): Boolean {
|
||||
var showBrowser = false
|
||||
try {
|
||||
if (isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) {
|
||||
val intent = Intent(OPEN_INTENTS_FILE_BROWSE)
|
||||
// Get file path parent if possible
|
||||
if (dataUri != null
|
||||
&& dataUri.toString().isNotEmpty()
|
||||
&& dataUri.scheme == "file") {
|
||||
intent.data = dataUri
|
||||
} else {
|
||||
Log.w(javaClass.name, "Unable to read the URI")
|
||||
}
|
||||
if (fragment != null)
|
||||
fragment?.startActivityForResult(intent, FILE_BROWSE)
|
||||
else
|
||||
activity?.startActivityForResult(intent, FILE_BROWSE)
|
||||
} else {
|
||||
showBrowser = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Enable to start OPEN_INTENTS_FILE_BROWSE", e)
|
||||
showBrowser = true
|
||||
}
|
||||
|
||||
return showBrowser
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the specified action can be used as an intent. This
|
||||
* method queries the package manager for installed packages that can
|
||||
* respond to an intent with the specified action. If no suitable package is
|
||||
* found, this method returns false.
|
||||
*
|
||||
* @param context The application's environment.
|
||||
* @param action The Intent action to check for availability.
|
||||
*
|
||||
* @return True if an Intent with the specified action can be sent and
|
||||
* responded to, false otherwise.
|
||||
*/
|
||||
private fun isIntentAvailable(context: Context, action: String): Boolean {
|
||||
val packageManager = context.packageManager
|
||||
val intent = Intent(action)
|
||||
val list = packageManager.queryIntentActivities(intent,
|
||||
PackageManager.MATCH_DEFAULT_ONLY)
|
||||
return list.size > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Show Browser dialog to select file picker app
|
||||
*/
|
||||
private fun showBrowserDialog() {
|
||||
try {
|
||||
val browserDialogFragment = BrowserDialogFragment()
|
||||
if (fragment != null && fragment!!.fragmentManager != null)
|
||||
browserDialogFragment.show(fragment!!.fragmentManager!!, "browserDialog")
|
||||
else
|
||||
browserDialogFragment.show((activity as FragmentActivity).supportFragmentManager, "browserDialog")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Can't open BrowserDialog", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* To use in onActivityResultCallback in Fragment or Activity
|
||||
* @param keyFileCallback Callback retrieve from data
|
||||
* @return true if requestCode was captured, false elsechere
|
||||
*/
|
||||
fun onActivityResultCallback(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
data: Intent?,
|
||||
keyFileCallback: ((uri: Uri?) -> Unit)?): Boolean {
|
||||
|
||||
when (requestCode) {
|
||||
FILE_BROWSE -> {
|
||||
if (resultCode == RESULT_OK) {
|
||||
val filename = data?.dataString
|
||||
var keyUri: Uri? = null
|
||||
if (filename != null) {
|
||||
keyUri = UriUtil.parse(filename)
|
||||
}
|
||||
keyFileCallback?.invoke(keyUri)
|
||||
}
|
||||
return true
|
||||
}
|
||||
GET_CONTENT, OPEN_DOC -> {
|
||||
if (resultCode == RESULT_OK) {
|
||||
if (data != null) {
|
||||
var 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
|
||||
}
|
||||
if (requestCode == GET_CONTENT) {
|
||||
uri = UriUtil.translateUri(activity!!, uri)
|
||||
}
|
||||
keyFileCallback?.invoke(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "OpenFileHelper"
|
||||
|
||||
private var ACTION_OPEN_DOCUMENT: String
|
||||
|
||||
init {
|
||||
ACTION_OPEN_DOCUMENT = try {
|
||||
val openDocument = Intent::class.java.getField("ACTION_OPEN_DOCUMENT")
|
||||
openDocument.get(null) as String
|
||||
} catch (e: Exception) {
|
||||
"android.intent.action.OPEN_DOCUMENT"
|
||||
}
|
||||
}
|
||||
|
||||
const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE"
|
||||
|
||||
private const val GET_CONTENT = 25745
|
||||
private const val OPEN_DOC = 25845
|
||||
private const val FILE_BROWSE = 25645
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.kunzisoft.keepass.activities.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
|
||||
object ReadOnlyHelper {
|
||||
|
||||
private const val READ_ONLY_KEY = "READ_ONLY_KEY"
|
||||
|
||||
const val READ_ONLY_DEFAULT = false
|
||||
|
||||
fun retrieveReadOnlyFromIntent(intent: Intent): Boolean {
|
||||
return intent.getBooleanExtra(READ_ONLY_KEY, READ_ONLY_DEFAULT)
|
||||
}
|
||||
|
||||
fun retrieveReadOnlyFromInstanceStateOrPreference(context: Context, savedInstanceState: Bundle?): Boolean {
|
||||
return if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) {
|
||||
savedInstanceState.getBoolean(READ_ONLY_KEY)
|
||||
} else {
|
||||
PreferencesUtil.enableReadOnlyDatabase(context)
|
||||
}
|
||||
}
|
||||
|
||||
fun retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState: Bundle?, arguments: Bundle?): Boolean {
|
||||
var readOnly = READ_ONLY_DEFAULT
|
||||
if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) {
|
||||
readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY)
|
||||
} else if (arguments != null && arguments.containsKey(READ_ONLY_KEY)) {
|
||||
readOnly = arguments.getBoolean(READ_ONLY_KEY)
|
||||
}
|
||||
return readOnly
|
||||
}
|
||||
|
||||
fun retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState: Bundle?, intent: Intent?): Boolean {
|
||||
var readOnly = READ_ONLY_DEFAULT
|
||||
if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) {
|
||||
readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY)
|
||||
} else {
|
||||
if (intent != null)
|
||||
readOnly = intent.getBooleanExtra(READ_ONLY_KEY, READ_ONLY_DEFAULT)
|
||||
}
|
||||
return readOnly
|
||||
}
|
||||
|
||||
fun putReadOnlyInIntent(intent: Intent, readOnly: Boolean) {
|
||||
intent.putExtra(READ_ONLY_KEY, readOnly)
|
||||
}
|
||||
|
||||
fun putReadOnlyInBundle(bundle: Bundle, readOnly: Boolean) {
|
||||
bundle.putBoolean(READ_ONLY_KEY, readOnly)
|
||||
}
|
||||
|
||||
fun onSaveInstanceState(outState: Bundle, readOnly: Boolean) {
|
||||
outState.putBoolean(READ_ONLY_KEY, readOnly)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.lock
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
|
||||
abstract class LockingActivity : StylishActivity() {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "LockingActivity"
|
||||
|
||||
const val RESULT_EXIT_LOCK = 1450
|
||||
|
||||
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
||||
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
||||
}
|
||||
|
||||
protected var mTimeoutEnable: Boolean = true
|
||||
|
||||
private var mLockReceiver: LockReceiver? = null
|
||||
private var mExitLock: Boolean = false
|
||||
|
||||
// Force readOnly if Entry Selection mode
|
||||
protected var mReadOnly: Boolean = false
|
||||
get() {
|
||||
return field || mSelectionMode
|
||||
}
|
||||
protected var mSelectionMode: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (savedInstanceState != null
|
||||
&& savedInstanceState.containsKey(TIMEOUT_ENABLE_KEY)) {
|
||||
mTimeoutEnable = savedInstanceState.getBoolean(TIMEOUT_ENABLE_KEY)
|
||||
} else {
|
||||
if (intent != null)
|
||||
mTimeoutEnable = intent.getBooleanExtra(TIMEOUT_ENABLE_KEY, TIMEOUT_ENABLE_KEY_DEFAULT)
|
||||
}
|
||||
|
||||
if (mTimeoutEnable) {
|
||||
mLockReceiver = LockReceiver()
|
||||
val intentFilter = IntentFilter().apply {
|
||||
addAction(Intent.ACTION_SCREEN_OFF)
|
||||
addAction(LOCK_ACTION)
|
||||
}
|
||||
registerReceiver(mLockReceiver, intentFilter)
|
||||
}
|
||||
|
||||
mExitLock = false
|
||||
mReadOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState, intent)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (resultCode == RESULT_EXIT_LOCK) {
|
||||
mExitLock = true
|
||||
if (Database.getInstance().loaded) {
|
||||
lockAndExit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// To refresh when back to normal workflow from selection workflow
|
||||
mSelectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(intent)
|
||||
|
||||
if (mTimeoutEnable) {
|
||||
// End activity if database not loaded
|
||||
if (!Database.getInstance().loaded) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
// After the first creation
|
||||
// or If simply swipe with another application
|
||||
// If the time is out -> close the Activity
|
||||
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
||||
// If onCreate already record time
|
||||
if (!mExitLock)
|
||||
TimeoutHelper.recordTime(this)
|
||||
}
|
||||
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
ReadOnlyHelper.onSaveInstanceState(outState, mReadOnly)
|
||||
outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
if (mTimeoutEnable) {
|
||||
// If the time is out during our navigation in activity -> close the Activity
|
||||
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (mLockReceiver != null)
|
||||
unregisterReceiver(mLockReceiver)
|
||||
}
|
||||
|
||||
inner class LockReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
// If allowed, lock and exit
|
||||
if (!TimeoutHelper.temporarilyDisableTimeout) {
|
||||
intent.action?.let {
|
||||
when (it) {
|
||||
Intent.ACTION_SCREEN_OFF -> if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(this@LockingActivity)) {
|
||||
lockAndExit()
|
||||
}
|
||||
LOCK_ACTION -> lockAndExit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun lockAndExit() {
|
||||
lock()
|
||||
}
|
||||
|
||||
/**
|
||||
* To reset the app timeout when a view is focused or changed
|
||||
*/
|
||||
protected fun resetAppTimeoutWhenViewFocusedOrChanged(vararg views: View?) {
|
||||
views.forEach {
|
||||
it?.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (hasFocus) {
|
||||
Log.d(TAG, "View focused, reset app timeout")
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
|
||||
}
|
||||
}
|
||||
if (it is ViewGroup) {
|
||||
for (i in 0..it.childCount) {
|
||||
resetAppTimeoutWhenViewFocusedOrChanged(it.getChildAt(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (mTimeoutEnable) {
|
||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) {
|
||||
super.onBackPressed()
|
||||
}
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Activity.lock() {
|
||||
// Stop the Magikeyboard service
|
||||
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
||||
MagikIME.removeEntry(this)
|
||||
|
||||
Log.i(Activity::class.java.name, "Shutdown " + localClassName +
|
||||
" after inactivity or manual lock")
|
||||
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).apply {
|
||||
cancelAll()
|
||||
}
|
||||
// Clear data
|
||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
||||
// Add onActivityForResult response
|
||||
setResult(LockingActivity.RESULT_EXIT_LOCK)
|
||||
finish()
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.lock
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
|
||||
/**
|
||||
* Locking Hide Activity that sets FLAG_SECURE to prevent screenshots, and from
|
||||
* appearing in the recent app preview
|
||||
*/
|
||||
abstract class LockingHideActivity : LockingActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Several gingerbread devices have problems with FLAG_SECURE
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
|
||||
/* (non-Javadoc) Workaround for HTC Linkify issues
|
||||
* @see android.app.Activity#startActivity(android.content.Intent)
|
||||
*/
|
||||
override fun startActivity(intent: Intent) {
|
||||
try {
|
||||
if (intent.component != null && intent.component!!.shortClassName == ".HtcLinkifyDispatcherActivity") {
|
||||
intent.component = null
|
||||
}
|
||||
super.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
/* Catch the bad HTC implementation case */
|
||||
super.startActivity(Intent.createChooser(intent, null))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.stylish
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
|
||||
/**
|
||||
* Class that provides functions to retrieve and assign a theme to a module
|
||||
*/
|
||||
object Stylish {
|
||||
|
||||
private var themeString: String? = null
|
||||
|
||||
/**
|
||||
* Initialize the class with a theme preference
|
||||
* @param context Context to retrieve the theme preference
|
||||
*/
|
||||
fun init(context: Context) {
|
||||
val stylishPrefKey = context.getString(R.string.setting_style_key)
|
||||
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
|
||||
themeString = PreferenceManager.getDefaultSharedPreferences(context).getString(stylishPrefKey, context.getString(R.string.list_style_name_light))
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign the style to the class attribute
|
||||
* @param styleString Style id String
|
||||
*/
|
||||
fun assignStyle(styleString: String) {
|
||||
themeString = styleString
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that returns the current id of the style selected in the preference
|
||||
* @param context Context to retrieve the id
|
||||
* @return Id of the style
|
||||
*/
|
||||
@StyleRes
|
||||
fun getThemeId(context: Context): Int {
|
||||
|
||||
return when (themeString) {
|
||||
context.getString(R.string.list_style_name_night) -> R.style.KeepassDXStyle_Night
|
||||
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_red) -> R.style.KeepassDXStyle_Red
|
||||
context.getString(R.string.list_style_name_purple) -> R.style.KeepassDXStyle_Purple
|
||||
else -> R.style.KeepassDXStyle_Light
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.stylish
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.util.Log
|
||||
|
||||
abstract class StylishActivity : AppCompatActivity() {
|
||||
|
||||
@StyleRes
|
||||
private var themeId: Int = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
this.themeId = Stylish.getThemeId(this)
|
||||
setTheme(themeId)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (Stylish.getThemeId(this) != this.themeId) {
|
||||
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
|
||||
this.recreate()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.activities.stylish
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
||||
abstract class StylishFragment : Fragment() {
|
||||
|
||||
@StyleRes
|
||||
protected var themeId: Int = 0
|
||||
protected var contextThemed: Context? = null
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
this.themeId = Stylish.getThemeId(context)
|
||||
contextThemed = ContextThemeWrapper(context, themeId)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
// To fix status bar color
|
||||
if (activity != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val window = activity!!.window
|
||||
|
||||
val attrColorPrimaryDark = intArrayOf(android.R.attr.colorPrimaryDark)
|
||||
val taColorPrimaryDark = contextThemed?.theme?.obtainStyledAttributes(attrColorPrimaryDark)
|
||||
val defaultColor = Color.BLACK
|
||||
window.statusBarColor = taColorPrimaryDark?.getColor(0, defaultColor) ?: defaultColor
|
||||
taColorPrimaryDark?.recycle()
|
||||
}
|
||||
|
||||
return super.onCreateView(inflater, container, savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
contextThemed = null
|
||||
super.onDetach()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.model.Field
|
||||
|
||||
import java.util.ArrayList
|
||||
|
||||
class FieldsAdapter(context: Context) : RecyclerView.Adapter<FieldsAdapter.FieldViewHolder>() {
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
var fields: MutableList<Field> = ArrayList()
|
||||
var onItemClickListener: OnItemClickListener? = null
|
||||
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FieldViewHolder {
|
||||
val view = inflater.inflate(R.layout.keyboard_popup_fields_item, parent, false)
|
||||
return FieldViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: FieldViewHolder, position: Int) {
|
||||
val field = fields[position]
|
||||
holder.name.text = field.name
|
||||
holder.bind(field, onItemClickListener)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return fields.size
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
fields.clear()
|
||||
}
|
||||
|
||||
interface OnItemClickListener {
|
||||
fun onItemClick(item: Field)
|
||||
}
|
||||
|
||||
inner class FieldViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
var name: TextView = itemView.findViewById(R.id.keyboard_popup_field_item_name)
|
||||
|
||||
fun bind(item: Field, listener: OnItemClickListener?) {
|
||||
itemView.setOnClickListener { listener?.onItemClick(item) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.util.TypedValue
|
||||
import android.view.*
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.ViewSwitcher
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryEntity
|
||||
import com.kunzisoft.keepass.utils.FileDatabaseInfo
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
class FileDatabaseHistoryAdapter(private val context: Context)
|
||||
: RecyclerView.Adapter<FileDatabaseHistoryAdapter.FileDatabaseHistoryViewHolder>() {
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
private var fileItemOpenListener: ((FileDatabaseHistoryEntity)->Unit)? = null
|
||||
private var fileSelectClearListener: ((FileDatabaseHistoryEntity)->Boolean)? = null
|
||||
private var saveAliasListener: ((FileDatabaseHistoryEntity)->Unit)? = null
|
||||
|
||||
private val listDatabaseFiles = ArrayList<FileDatabaseHistoryEntity>()
|
||||
|
||||
private var mExpandedPosition = -1
|
||||
private var mPreviousExpandedPosition = -1
|
||||
|
||||
@ColorInt
|
||||
private val defaultColor: Int
|
||||
@ColorInt
|
||||
private val warningColor: Int
|
||||
|
||||
init {
|
||||
val typedValue = TypedValue()
|
||||
val theme = context.theme
|
||||
theme.resolveAttribute(R.attr.colorAccent, typedValue, true)
|
||||
warningColor = typedValue.data
|
||||
theme.resolveAttribute(android.R.attr.textColorHintInverse, typedValue, true)
|
||||
defaultColor = typedValue.data
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileDatabaseHistoryViewHolder {
|
||||
val view = inflater.inflate(R.layout.item_file_row, parent, false)
|
||||
return FileDatabaseHistoryViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) {
|
||||
// Get info from position
|
||||
val fileHistoryEntity = listDatabaseFiles[position]
|
||||
val fileDatabaseInfo = FileDatabaseInfo(context, fileHistoryEntity.databaseUri)
|
||||
|
||||
// Click item to open file
|
||||
if (fileItemOpenListener != null)
|
||||
holder.fileContainer.setOnClickListener {
|
||||
fileItemOpenListener?.invoke(fileHistoryEntity)
|
||||
}
|
||||
|
||||
// File alias
|
||||
holder.fileAlias.text = fileDatabaseInfo.retrieveDatabaseAlias(fileHistoryEntity.databaseAlias)
|
||||
|
||||
// File path
|
||||
holder.filePath.text = UriUtil.decode(fileDatabaseInfo.fileUri?.toString())
|
||||
|
||||
holder.filePreciseInfoContainer.visibility = if (fileDatabaseInfo.found()) {
|
||||
// Modification
|
||||
holder.fileModification.text = fileDatabaseInfo.getModificationString()
|
||||
// Size
|
||||
holder.fileSize.text = fileDatabaseInfo.getSizeString()
|
||||
|
||||
View.VISIBLE
|
||||
} else
|
||||
View.GONE
|
||||
|
||||
// Click on information
|
||||
val isExpanded = position == mExpandedPosition
|
||||
//This line hides or shows the layout in question
|
||||
holder.fileExpandContainer.visibility = if (isExpanded) View.VISIBLE else View.GONE
|
||||
|
||||
// Save alias modification
|
||||
holder.fileAliasCloseButton.setOnClickListener {
|
||||
// Change the alias
|
||||
fileHistoryEntity.databaseAlias = holder.fileAliasEdit.text.toString()
|
||||
saveAliasListener?.invoke(fileHistoryEntity)
|
||||
|
||||
// Finish save mode
|
||||
holder.fileMainSwitcher.showPrevious()
|
||||
// Refresh current position to show alias
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
|
||||
// Open alias modification
|
||||
holder.fileModifyButton.setOnClickListener {
|
||||
holder.fileAliasEdit.setText(holder.fileAlias.text)
|
||||
holder.fileMainSwitcher.showNext()
|
||||
}
|
||||
|
||||
holder.fileDeleteButton.setOnClickListener {
|
||||
fileSelectClearListener?.invoke(fileHistoryEntity)
|
||||
}
|
||||
|
||||
if (isExpanded) {
|
||||
mPreviousExpandedPosition = position
|
||||
}
|
||||
|
||||
holder.fileInformation.setOnClickListener {
|
||||
mExpandedPosition = if (isExpanded) -1 else position
|
||||
|
||||
// Notify change
|
||||
if (mPreviousExpandedPosition < itemCount)
|
||||
notifyItemChanged(mPreviousExpandedPosition)
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
|
||||
// Refresh View / Close alias modification if not contains fileAlias
|
||||
if (holder.fileMainSwitcher.currentView.findViewById<View>(R.id.file_alias)
|
||||
!= holder.fileAlias)
|
||||
holder.fileMainSwitcher.showPrevious()
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return listDatabaseFiles.size
|
||||
}
|
||||
|
||||
fun addDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<FileDatabaseHistoryEntity>) {
|
||||
listDatabaseFiles.clear()
|
||||
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
|
||||
}
|
||||
|
||||
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: FileDatabaseHistoryEntity) {
|
||||
listDatabaseFiles.remove(fileDatabaseHistoryToDelete)
|
||||
}
|
||||
|
||||
fun setOnFileDatabaseHistoryOpenListener(listener : ((FileDatabaseHistoryEntity)->Unit)?) {
|
||||
this.fileItemOpenListener = listener
|
||||
}
|
||||
|
||||
fun setOnFileDatabaseHistoryDeleteListener(listener : ((FileDatabaseHistoryEntity)->Boolean)?) {
|
||||
this.fileSelectClearListener = listener
|
||||
}
|
||||
|
||||
fun setOnSaveAliasListener(listener : ((FileDatabaseHistoryEntity)->Unit)?) {
|
||||
this.saveAliasListener = listener
|
||||
}
|
||||
|
||||
inner class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
var fileContainer: ViewGroup = itemView.findViewById(R.id.file_container_basic_info)
|
||||
|
||||
var fileAlias: TextView = itemView.findViewById(R.id.file_alias)
|
||||
var fileInformation: ImageView = itemView.findViewById(R.id.file_information)
|
||||
|
||||
var fileMainSwitcher: ViewSwitcher = itemView.findViewById(R.id.file_main_switcher)
|
||||
var fileAliasEdit: EditText = itemView.findViewById(R.id.file_alias_edit)
|
||||
var fileAliasCloseButton: ImageView = itemView.findViewById(R.id.file_alias_save)
|
||||
|
||||
var fileExpandContainer: ViewGroup = itemView.findViewById(R.id.file_expand_container)
|
||||
var fileModifyButton: ImageView = itemView.findViewById(R.id.file_modify_button)
|
||||
var fileDeleteButton: ImageView = itemView.findViewById(R.id.file_delete_button)
|
||||
var filePath: TextView = itemView.findViewById(R.id.file_path)
|
||||
var filePreciseInfoContainer: ViewGroup = itemView.findViewById(R.id.file_precise_info_container)
|
||||
var fileModification: TextView = itemView.findViewById(R.id.file_modification)
|
||||
var fileSize: TextView = itemView.findViewById(R.id.file_size)
|
||||
}
|
||||
}
|
||||
@@ -1,411 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Color;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.util.SortedList;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.util.SortedListAdapterCallback;
|
||||
import android.util.Log;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.kunzisoft.keepass.R;
|
||||
import com.kunzisoft.keepass.app.App;
|
||||
import com.kunzisoft.keepass.database.Database;
|
||||
import com.kunzisoft.keepass.database.PwEntry;
|
||||
import com.kunzisoft.keepass.database.PwGroup;
|
||||
import com.kunzisoft.keepass.database.PwNode;
|
||||
import com.kunzisoft.keepass.database.SortNodeEnum;
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil;
|
||||
import com.kunzisoft.keepass.utils.Util;
|
||||
|
||||
public class NodeAdapter extends RecyclerView.Adapter<BasicViewHolder> {
|
||||
private static final String TAG = NodeAdapter.class.getName();
|
||||
|
||||
private SortedList<PwNode> nodeSortedList;
|
||||
|
||||
private Context context;
|
||||
private LayoutInflater inflater;
|
||||
private MenuInflater menuInflater;
|
||||
private float textSize;
|
||||
private float subtextSize;
|
||||
private float iconSize;
|
||||
private SortNodeEnum listSort;
|
||||
private boolean groupsBeforeSort;
|
||||
private boolean ascendingSort;
|
||||
private boolean showUsernames;
|
||||
|
||||
private NodeClickCallback nodeClickCallback;
|
||||
private NodeMenuListener nodeMenuListener;
|
||||
private boolean activateContextMenu;
|
||||
private boolean readOnly;
|
||||
private boolean isASearchResult;
|
||||
|
||||
private Database database;
|
||||
|
||||
private int iconGroupColor;
|
||||
private int iconEntryColor;
|
||||
|
||||
/**
|
||||
* Create node list adapter with contextMenu or not
|
||||
* @param context Context to use
|
||||
*/
|
||||
public NodeAdapter(final Context context, MenuInflater menuInflater) {
|
||||
this.inflater = LayoutInflater.from(context);
|
||||
this.menuInflater = menuInflater;
|
||||
this.context = context;
|
||||
assignPreferences();
|
||||
this.activateContextMenu = false;
|
||||
this.readOnly = false;
|
||||
this.isASearchResult = false;
|
||||
|
||||
this.nodeSortedList = new SortedList<>(PwNode.class, new SortedListAdapterCallback<PwNode>(this) {
|
||||
@Override public int compare(PwNode item1, PwNode item2) {
|
||||
return listSort.getNodeComparator(ascendingSort, groupsBeforeSort).compare(item1, item2);
|
||||
}
|
||||
|
||||
@Override public boolean areContentsTheSame(PwNode oldItem, PwNode newItem) {
|
||||
return oldItem.isContentVisuallyTheSame(newItem);
|
||||
}
|
||||
|
||||
@Override public boolean areItemsTheSame(PwNode item1, PwNode item2) {
|
||||
return item1.equals(item2);
|
||||
}
|
||||
});
|
||||
|
||||
// Database
|
||||
this.database = App.getDB();
|
||||
|
||||
// Retrieve the color to tint the icon
|
||||
int[] attrTextColorPrimary = {android.R.attr.textColorPrimary};
|
||||
TypedArray taTextColorPrimary = context.getTheme().obtainStyledAttributes(attrTextColorPrimary);
|
||||
this.iconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK);
|
||||
taTextColorPrimary.recycle();
|
||||
int[] attrTextColor = {android.R.attr.textColor}; // In two times to fix bug compilation
|
||||
TypedArray taTextColor = context.getTheme().obtainStyledAttributes(attrTextColor);
|
||||
this.iconEntryColor = taTextColor.getColor(0, Color.BLACK);
|
||||
taTextColor.recycle();
|
||||
}
|
||||
|
||||
public void setReadOnly(boolean readOnly) {
|
||||
this.readOnly = readOnly;
|
||||
}
|
||||
|
||||
public void setIsASearchResult(boolean isASearchResult) {
|
||||
this.isASearchResult = isASearchResult;
|
||||
}
|
||||
|
||||
public void setActivateContextMenu(boolean activate) {
|
||||
this.activateContextMenu = activate;
|
||||
}
|
||||
|
||||
private void assignPreferences() {
|
||||
float textSizeDefault = Util.getListTextDefaultSize(context);
|
||||
this.textSize = PreferencesUtil.getListTextSize(context);
|
||||
this.subtextSize = context.getResources().getDimension(R.dimen.list_small_size_default)
|
||||
* textSize / textSizeDefault;
|
||||
// Retrieve the icon size
|
||||
float iconDefaultSize = context.getResources().getDimension(R.dimen.list_icon_size_default);
|
||||
this.iconSize = iconDefaultSize * textSize / textSizeDefault;
|
||||
this.listSort = PreferencesUtil.getListSort(context);
|
||||
this.groupsBeforeSort = PreferencesUtil.getGroupsBeforeSort(context);
|
||||
this.ascendingSort = PreferencesUtil.getAscendingSort(context);
|
||||
this.showUsernames = PreferencesUtil.showUsernamesListEntries(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the list by clear and build children from the group
|
||||
*/
|
||||
public void rebuildList(PwGroup group) {
|
||||
this.nodeSortedList.clear();
|
||||
assignPreferences();
|
||||
// TODO verify sort
|
||||
try {
|
||||
this.nodeSortedList.addAll(group.getDirectChildren());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Can't add node elements to the list", e);
|
||||
Toast.makeText(context, "Can't add node elements to the list : " + e.getMessage(), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the adapter contains or not any element
|
||||
* @return true if the list is empty
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return nodeSortedList.size() <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a node to the list
|
||||
* @param node Node to add
|
||||
*/
|
||||
public void addNode(PwNode node) {
|
||||
nodeSortedList.add(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a node in the list
|
||||
* @param node Node to delete
|
||||
*/
|
||||
public void removeNode(PwNode node) {
|
||||
nodeSortedList.remove(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a node in the list
|
||||
* @param oldNode Node before the update
|
||||
* @param newNode Node after the update
|
||||
*/
|
||||
public void updateNode(PwNode oldNode, PwNode newNode) {
|
||||
nodeSortedList.beginBatchedUpdates();
|
||||
nodeSortedList.remove(oldNode);
|
||||
nodeSortedList.add(newNode);
|
||||
nodeSortedList.endBatchedUpdates();
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify a change sort of the list
|
||||
*/
|
||||
public void notifyChangeSort(SortNodeEnum sortNodeEnum, boolean ascending, boolean groupsBefore) {
|
||||
this.listSort = sortNodeEnum;
|
||||
this.ascendingSort = ascending;
|
||||
this.groupsBeforeSort = groupsBefore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return nodeSortedList.get(position).getType().ordinal();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public BasicViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
BasicViewHolder basicViewHolder;
|
||||
View view;
|
||||
if (viewType == PwNode.Type.GROUP.ordinal()) {
|
||||
view = inflater.inflate(R.layout.list_nodes_group, parent, false);
|
||||
basicViewHolder = new GroupViewHolder(view);
|
||||
} else {
|
||||
view = inflater.inflate(R.layout.list_nodes_entry, parent, false);
|
||||
basicViewHolder = new EntryViewHolder(view);
|
||||
}
|
||||
return basicViewHolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull BasicViewHolder holder, int position) {
|
||||
PwNode subNode = nodeSortedList.get(position);
|
||||
// Assign image
|
||||
int iconColor = Color.BLACK;
|
||||
switch (subNode.getType()) {
|
||||
case GROUP:
|
||||
iconColor = iconGroupColor;
|
||||
break;
|
||||
case ENTRY:
|
||||
iconColor = iconEntryColor;
|
||||
break;
|
||||
}
|
||||
database.getDrawFactory().assignDatabaseIconTo(context, holder.icon, subNode.getIcon(), iconColor);
|
||||
// Assign text
|
||||
holder.text.setText(subNode.getTitle());
|
||||
// Assign click
|
||||
holder.container.setOnClickListener(
|
||||
new OnNodeClickListener(subNode));
|
||||
// Context menu
|
||||
if (activateContextMenu) {
|
||||
holder.container.setOnCreateContextMenuListener(
|
||||
new ContextMenuBuilder(subNode, nodeMenuListener, readOnly));
|
||||
}
|
||||
|
||||
// Add username
|
||||
holder.subText.setText("");
|
||||
holder.subText.setVisibility(View.GONE);
|
||||
if (subNode.getType().equals(PwNode.Type.ENTRY)) {
|
||||
PwEntry entry = (PwEntry) subNode;
|
||||
entry.startToManageFieldReferences(database.getPwDatabase());
|
||||
|
||||
holder.text.setText(entry.getVisualTitle());
|
||||
|
||||
String username = entry.getUsername();
|
||||
if (showUsernames && !username.isEmpty()) {
|
||||
holder.subText.setVisibility(View.VISIBLE);
|
||||
holder.subText.setText(username);
|
||||
}
|
||||
|
||||
entry.stopToManageFieldReferences();
|
||||
}
|
||||
|
||||
// Assign image and text size
|
||||
// Relative size of the icon
|
||||
holder.icon.getLayoutParams().height = ((int) iconSize);
|
||||
holder.icon.getLayoutParams().width = ((int) iconSize);
|
||||
holder.text.setTextSize(textSize);
|
||||
holder.subText.setTextSize(subtextSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return nodeSortedList.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a listener when a node is clicked
|
||||
*/
|
||||
public void setOnNodeClickListener(NodeClickCallback nodeClickCallback) {
|
||||
this.nodeClickCallback = nodeClickCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a listener when an element of menu is clicked
|
||||
*/
|
||||
public void setNodeMenuListener(NodeMenuListener nodeMenuListener) {
|
||||
this.nodeMenuListener = nodeMenuListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback listener to redefine to do an action when a node is click
|
||||
*/
|
||||
public interface NodeClickCallback {
|
||||
void onNodeClick(PwNode node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu listener to redefine to do an action in menu
|
||||
*/
|
||||
public interface NodeMenuListener {
|
||||
boolean onOpenMenuClick(PwNode node);
|
||||
boolean onEditMenuClick(PwNode node);
|
||||
boolean onCopyMenuClick(PwNode node);
|
||||
boolean onMoveMenuClick(PwNode node);
|
||||
boolean onDeleteMenuClick(PwNode node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility class for node listener
|
||||
*/
|
||||
private class OnNodeClickListener implements View.OnClickListener {
|
||||
private PwNode node;
|
||||
|
||||
OnNodeClickListener(PwNode node) {
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (nodeClickCallback != null)
|
||||
nodeClickCallback.onNodeClick(node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility class for menu listener
|
||||
*/
|
||||
private class ContextMenuBuilder implements View.OnCreateContextMenuListener {
|
||||
|
||||
private PwNode node;
|
||||
private NodeMenuListener menuListener;
|
||||
private boolean readOnly;
|
||||
|
||||
ContextMenuBuilder(PwNode node, NodeMenuListener menuListener, boolean readOnly) {
|
||||
this.menuListener = menuListener;
|
||||
this.node = node;
|
||||
this.readOnly = readOnly;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu contextMenu, View view, ContextMenu.ContextMenuInfo contextMenuInfo) {
|
||||
menuInflater.inflate(R.menu.node_menu, contextMenu);
|
||||
|
||||
// Opening
|
||||
MenuItem menuItem = contextMenu.findItem(R.id.menu_open);
|
||||
menuItem.setOnMenuItemClickListener(mOnMyActionClickListener);
|
||||
|
||||
// Edition
|
||||
if (readOnly || node.equals(App.getDB().getPwDatabase().getRecycleBin())) {
|
||||
contextMenu.removeItem(R.id.menu_edit);
|
||||
} else {
|
||||
menuItem = contextMenu.findItem(R.id.menu_edit);
|
||||
menuItem.setOnMenuItemClickListener(mOnMyActionClickListener);
|
||||
}
|
||||
|
||||
// Copy (not for group)
|
||||
if (readOnly
|
||||
|| isASearchResult
|
||||
|| node.equals(App.getDB().getPwDatabase().getRecycleBin())
|
||||
|| node.getType().equals(PwNode.Type.GROUP)) {
|
||||
// TODO COPY For Group
|
||||
contextMenu.removeItem(R.id.menu_copy);
|
||||
} else {
|
||||
menuItem = contextMenu.findItem(R.id.menu_copy);
|
||||
menuItem.setOnMenuItemClickListener(mOnMyActionClickListener);
|
||||
}
|
||||
|
||||
// Move
|
||||
if (readOnly
|
||||
|| isASearchResult
|
||||
|| node.equals(App.getDB().getPwDatabase().getRecycleBin())) {
|
||||
contextMenu.removeItem(R.id.menu_move);
|
||||
} else {
|
||||
menuItem = contextMenu.findItem(R.id.menu_move);
|
||||
menuItem.setOnMenuItemClickListener(mOnMyActionClickListener);
|
||||
}
|
||||
|
||||
// Deletion
|
||||
if (readOnly || node.equals(App.getDB().getPwDatabase().getRecycleBin())) {
|
||||
contextMenu.removeItem(R.id.menu_delete);
|
||||
} else {
|
||||
menuItem = contextMenu.findItem(R.id.menu_delete);
|
||||
menuItem.setOnMenuItemClickListener(mOnMyActionClickListener);
|
||||
}
|
||||
}
|
||||
|
||||
private MenuItem.OnMenuItemClickListener mOnMyActionClickListener = new MenuItem.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
if (menuListener == null)
|
||||
return false;
|
||||
switch ( item.getItemId() ) {
|
||||
case R.id.menu_open:
|
||||
return menuListener.onOpenMenuClick(node);
|
||||
case R.id.menu_edit:
|
||||
return menuListener.onEditMenuClick(node);
|
||||
case R.id.menu_copy:
|
||||
return menuListener.onCopyMenuClick(node);
|
||||
case R.id.menu_move:
|
||||
return menuListener.onMoveMenuClick(node);
|
||||
case R.id.menu_delete:
|
||||
return menuListener.onDeleteMenuClick(node);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
407
app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt
Normal file
407
app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt
Normal file
@@ -0,0 +1,407 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.*
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.SortNodeEnum
|
||||
import com.kunzisoft.keepass.database.element.*
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
|
||||
class NodeAdapter
|
||||
/**
|
||||
* Create node list adapter with contextMenu or not
|
||||
* @param context Context to use
|
||||
*/
|
||||
(private val context: Context, private val menuInflater: MenuInflater)
|
||||
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
|
||||
|
||||
private val nodeSortedList: SortedList<NodeVersioned>
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
|
||||
private var calculateViewTypeTextSize = Array(2) { true} // number of view type
|
||||
private var textSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
||||
private var prefTextSize: Float = 0F
|
||||
private var subtextSize: Float = 0F
|
||||
private var infoTextSize: Float = 0F
|
||||
private var numberChildrenTextSize: Float = 0F
|
||||
private var iconSize: Float = 0F
|
||||
private var listSort: SortNodeEnum = SortNodeEnum.DB
|
||||
private var ascendingSort: Boolean = true
|
||||
private var groupsBeforeSort: Boolean = true
|
||||
private var recycleBinBottomSort: Boolean = true
|
||||
private var showUserNames: Boolean = true
|
||||
private var showNumberEntries: Boolean = true
|
||||
|
||||
private var nodeClickCallback: NodeClickCallback? = null
|
||||
private var nodeMenuListener: NodeMenuListener? = null
|
||||
private var activateContextMenu: Boolean = false
|
||||
private var readOnly: Boolean = false
|
||||
private var isASearchResult: Boolean = false
|
||||
|
||||
private val mDatabase: Database
|
||||
|
||||
private val iconGroupColor: Int
|
||||
private val iconEntryColor: Int
|
||||
|
||||
/**
|
||||
* Determine if the adapter contains or not any element
|
||||
* @return true if the list is empty
|
||||
*/
|
||||
val isEmpty: Boolean
|
||||
get() = nodeSortedList.size() <= 0
|
||||
|
||||
init {
|
||||
assignPreferences()
|
||||
this.activateContextMenu = false
|
||||
this.readOnly = false
|
||||
this.isASearchResult = false
|
||||
|
||||
this.nodeSortedList = SortedList(NodeVersioned::class.java, object : SortedListAdapterCallback<NodeVersioned>(this) {
|
||||
override fun compare(item1: NodeVersioned, item2: NodeVersioned): Int {
|
||||
return listSort.getNodeComparator(ascendingSort, groupsBeforeSort, recycleBinBottomSort).compare(item1, item2)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: NodeVersioned, newItem: NodeVersioned): Boolean {
|
||||
return oldItem.type == newItem.type
|
||||
&& oldItem.title == newItem.title
|
||||
&& oldItem.icon == newItem.icon
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(item1: NodeVersioned, item2: NodeVersioned): Boolean {
|
||||
return item1 == item2
|
||||
}
|
||||
})
|
||||
|
||||
// Database
|
||||
this.mDatabase = Database.getInstance()
|
||||
|
||||
// Retrieve the color to tint the icon
|
||||
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
||||
this.iconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
|
||||
taTextColorPrimary.recycle()
|
||||
// In two times to fix bug compilation
|
||||
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||
this.iconEntryColor = taTextColor.getColor(0, Color.BLACK)
|
||||
taTextColor.recycle()
|
||||
}
|
||||
|
||||
fun setReadOnly(readOnly: Boolean) {
|
||||
this.readOnly = readOnly
|
||||
}
|
||||
|
||||
fun setIsASearchResult(isASearchResult: Boolean) {
|
||||
this.isASearchResult = isASearchResult
|
||||
}
|
||||
|
||||
fun setActivateContextMenu(activate: Boolean) {
|
||||
this.activateContextMenu = activate
|
||||
}
|
||||
|
||||
private fun assignPreferences() {
|
||||
this.prefTextSize = PreferencesUtil.getListTextSize(context)
|
||||
this.infoTextSize = context.resources.getDimension(R.dimen.list_medium_size_default) * prefTextSize
|
||||
this.subtextSize = context.resources.getDimension(R.dimen.list_small_size_default) * prefTextSize
|
||||
this.numberChildrenTextSize = context.resources.getDimension(R.dimen.list_tiny_size_default) * prefTextSize
|
||||
this.iconSize = context.resources.getDimension(R.dimen.list_icon_size_default) * prefTextSize
|
||||
|
||||
this.listSort = PreferencesUtil.getListSort(context)
|
||||
this.ascendingSort = PreferencesUtil.getAscendingSort(context)
|
||||
this.groupsBeforeSort = PreferencesUtil.getGroupsBeforeSort(context)
|
||||
this.recycleBinBottomSort = PreferencesUtil.getRecycleBinBottomSort(context)
|
||||
this.showUserNames = PreferencesUtil.showUsernamesListEntries(context)
|
||||
this.showNumberEntries = PreferencesUtil.showNumberEntries(context)
|
||||
|
||||
// Reinit textSize for all view type
|
||||
calculateViewTypeTextSize.forEachIndexed { index, _ -> calculateViewTypeTextSize[index] = true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the list by clear and build children from the group
|
||||
*/
|
||||
fun rebuildList(group: GroupVersioned) {
|
||||
this.nodeSortedList.clear()
|
||||
assignPreferences()
|
||||
try {
|
||||
this.nodeSortedList.addAll(group.getChildren())
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Can't add node elements to the list", e)
|
||||
Toast.makeText(context, "Can't add node elements to the list : " + e.message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun contains(node: NodeVersioned): Boolean {
|
||||
return nodeSortedList.indexOf(node) != SortedList.INVALID_POSITION
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a node to the list
|
||||
* @param node Node to add
|
||||
*/
|
||||
fun addNode(node: NodeVersioned) {
|
||||
nodeSortedList.add(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a node in the list
|
||||
* @param node Node to delete
|
||||
*/
|
||||
fun removeNode(node: NodeVersioned) {
|
||||
nodeSortedList.remove(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a node at [position] in the list
|
||||
*/
|
||||
fun removeNodeAt(position: Int) {
|
||||
nodeSortedList.removeItemAt(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a node in the list
|
||||
* @param oldNode Node before the update
|
||||
* @param newNode Node after the update
|
||||
*/
|
||||
fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned) {
|
||||
nodeSortedList.beginBatchedUpdates()
|
||||
nodeSortedList.remove(oldNode)
|
||||
nodeSortedList.add(newNode)
|
||||
nodeSortedList.endBatchedUpdates()
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify a change sort of the list
|
||||
*/
|
||||
fun notifyChangeSort(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean) {
|
||||
this.listSort = sortNodeEnum
|
||||
this.ascendingSort = ascending
|
||||
this.groupsBeforeSort = groupsBefore
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return nodeSortedList.get(position).type.ordinal
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NodeViewHolder {
|
||||
val view: View = if (viewType == Type.GROUP.ordinal) {
|
||||
inflater.inflate(R.layout.item_list_nodes_group, parent, false)
|
||||
} else {
|
||||
inflater.inflate(R.layout.item_list_nodes_entry, parent, false)
|
||||
}
|
||||
return NodeViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: NodeViewHolder, position: Int) {
|
||||
val subNode = nodeSortedList.get(position)
|
||||
// Assign image
|
||||
val iconColor = when (subNode.type) {
|
||||
Type.GROUP -> iconGroupColor
|
||||
Type.ENTRY -> iconEntryColor
|
||||
}
|
||||
holder.icon.apply {
|
||||
assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
|
||||
// Relative size of the icon
|
||||
layoutParams?.apply {
|
||||
height = iconSize.toInt()
|
||||
width = iconSize.toInt()
|
||||
}
|
||||
}
|
||||
// Assign text
|
||||
holder.text.apply {
|
||||
text = subNode.title
|
||||
setTextSize(textSizeUnit, infoTextSize)
|
||||
}
|
||||
// Assign click
|
||||
holder.container.setOnClickListener { nodeClickCallback?.onNodeClick(subNode) }
|
||||
// Context menu
|
||||
if (activateContextMenu) {
|
||||
holder.container.setOnCreateContextMenuListener(
|
||||
ContextMenuBuilder(menuInflater, subNode, readOnly, isASearchResult, nodeMenuListener))
|
||||
}
|
||||
|
||||
// Add subText with username
|
||||
holder.subText.apply {
|
||||
text = ""
|
||||
visibility = View.GONE
|
||||
if (subNode.type == Type.ENTRY) {
|
||||
val entry = subNode as EntryVersioned
|
||||
|
||||
mDatabase.startManageEntry(entry)
|
||||
|
||||
holder.text.text = entry.getVisualTitle()
|
||||
|
||||
val username = entry.username
|
||||
if (showUserNames && username.isNotEmpty()) {
|
||||
visibility = View.VISIBLE
|
||||
text = username
|
||||
setTextSize(textSizeUnit, subtextSize)
|
||||
}
|
||||
|
||||
mDatabase.stopManageEntry(entry)
|
||||
}
|
||||
}
|
||||
|
||||
// Add number of entries in groups
|
||||
if (subNode.type == Type.GROUP) {
|
||||
if (showNumberEntries) {
|
||||
holder.numberChildren?.apply {
|
||||
text = (subNode as GroupVersioned).getChildEntries(true).size.toString()
|
||||
setTextSize(textSizeUnit, numberChildrenTextSize)
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
holder.numberChildren?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return nodeSortedList.size()
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a listener when a node is clicked
|
||||
*/
|
||||
fun setOnNodeClickListener(nodeClickCallback: NodeClickCallback?) {
|
||||
this.nodeClickCallback = nodeClickCallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a listener when an element of menu is clicked
|
||||
*/
|
||||
fun setNodeMenuListener(nodeMenuListener: NodeMenuListener?) {
|
||||
this.nodeMenuListener = nodeMenuListener
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback listener to redefine to do an action when a node is click
|
||||
*/
|
||||
interface NodeClickCallback {
|
||||
fun onNodeClick(node: NodeVersioned)
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu listener to redefine to do an action in menu
|
||||
*/
|
||||
interface NodeMenuListener {
|
||||
fun onOpenMenuClick(node: NodeVersioned): Boolean
|
||||
fun onEditMenuClick(node: NodeVersioned): Boolean
|
||||
fun onCopyMenuClick(node: NodeVersioned): Boolean
|
||||
fun onMoveMenuClick(node: NodeVersioned): Boolean
|
||||
fun onDeleteMenuClick(node: NodeVersioned): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility class for menu listener
|
||||
*/
|
||||
private class ContextMenuBuilder(val menuInflater: MenuInflater,
|
||||
val node: NodeVersioned,
|
||||
val readOnly: Boolean,
|
||||
val isASearchResult: Boolean,
|
||||
val menuListener: NodeMenuListener?)
|
||||
: View.OnCreateContextMenuListener {
|
||||
|
||||
private val mOnMyActionClickListener = MenuItem.OnMenuItemClickListener { item ->
|
||||
if (menuListener == null)
|
||||
return@OnMenuItemClickListener false
|
||||
when (item.itemId) {
|
||||
R.id.menu_open -> menuListener.onOpenMenuClick(node)
|
||||
R.id.menu_edit -> menuListener.onEditMenuClick(node)
|
||||
R.id.menu_copy -> menuListener.onCopyMenuClick(node)
|
||||
R.id.menu_move -> menuListener.onMoveMenuClick(node)
|
||||
R.id.menu_delete -> menuListener.onDeleteMenuClick(node)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateContextMenu(contextMenu: ContextMenu?,
|
||||
view: View?,
|
||||
contextMenuInfo: ContextMenu.ContextMenuInfo?) {
|
||||
menuInflater.inflate(R.menu.node_menu, contextMenu)
|
||||
|
||||
// Opening
|
||||
var menuItem = contextMenu?.findItem(R.id.menu_open)
|
||||
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
|
||||
|
||||
val database = Database.getInstance()
|
||||
|
||||
// Edition
|
||||
if (readOnly || node == database.recycleBin) {
|
||||
contextMenu?.removeItem(R.id.menu_edit)
|
||||
} else {
|
||||
menuItem = contextMenu?.findItem(R.id.menu_edit)
|
||||
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
|
||||
}
|
||||
|
||||
// Copy (not for group)
|
||||
if (readOnly
|
||||
|| isASearchResult
|
||||
|| node == database.recycleBin
|
||||
|| node.type == Type.GROUP) {
|
||||
// TODO COPY For Group
|
||||
contextMenu?.removeItem(R.id.menu_copy)
|
||||
} else {
|
||||
menuItem = contextMenu?.findItem(R.id.menu_copy)
|
||||
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
|
||||
}
|
||||
|
||||
// Move
|
||||
if (readOnly
|
||||
|| isASearchResult
|
||||
|| node == database.recycleBin) {
|
||||
contextMenu?.removeItem(R.id.menu_move)
|
||||
} else {
|
||||
menuItem = contextMenu?.findItem(R.id.menu_move)
|
||||
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
|
||||
}
|
||||
|
||||
// Deletion
|
||||
if (readOnly || node == database.recycleBin) {
|
||||
contextMenu?.removeItem(R.id.menu_delete)
|
||||
} else {
|
||||
menuItem = contextMenu?.findItem(R.id.menu_delete)
|
||||
menuItem?.setOnMenuItemClickListener(mOnMyActionClickListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
var container: View = itemView.findViewById(R.id.node_container)
|
||||
var icon: ImageView = itemView.findViewById(R.id.node_icon)
|
||||
var text: TextView = itemView.findViewById(R.id.node_text)
|
||||
var subText: TextView = itemView.findViewById(R.id.node_subtext)
|
||||
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = NodeAdapter::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Color;
|
||||
import android.support.v4.widget.CursorAdapter;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.kunzisoft.keepass.R;
|
||||
import com.kunzisoft.keepass.database.Database;
|
||||
import com.kunzisoft.keepass.database.PwEntry;
|
||||
import com.kunzisoft.keepass.database.PwIcon;
|
||||
import com.kunzisoft.keepass.database.PwIconFactory;
|
||||
import com.kunzisoft.keepass.database.cursor.EntryCursor;
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class SearchEntryCursorAdapter extends CursorAdapter {
|
||||
|
||||
private LayoutInflater cursorInflater;
|
||||
private Database database;
|
||||
private boolean displayUsername;
|
||||
private int iconColor;
|
||||
|
||||
public SearchEntryCursorAdapter(Context context, Database database) {
|
||||
super(context, null, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
|
||||
cursorInflater = (LayoutInflater) context.getSystemService(
|
||||
Context.LAYOUT_INFLATER_SERVICE);
|
||||
this.database = database;
|
||||
|
||||
// Get the icon color
|
||||
int[] attrTextColor = {R.attr.textColorInverse};
|
||||
TypedArray taTextColor = context.getTheme().obtainStyledAttributes(attrTextColor);
|
||||
this.iconColor = taTextColor.getColor(0, Color.WHITE);
|
||||
taTextColor.recycle();
|
||||
|
||||
reInit(context);
|
||||
}
|
||||
|
||||
public void reInit(Context context) {
|
||||
this.displayUsername = PreferencesUtil.showUsernamesListEntries(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View newView(Context context, Cursor cursor, ViewGroup parent) {
|
||||
|
||||
View view = cursorInflater.inflate(R.layout.search_entry, parent ,false);
|
||||
ViewHolder viewHolder = new ViewHolder();
|
||||
viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon);
|
||||
viewHolder.textViewTitle = view.findViewById(R.id.entry_text);
|
||||
viewHolder.textViewSubTitle = view.findViewById(R.id.entry_subtext);
|
||||
view.setTag(viewHolder);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor) {
|
||||
|
||||
// Retrieve elements from cursor
|
||||
UUID uuid = new UUID(cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS)),
|
||||
cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS)));
|
||||
PwIconFactory iconFactory = database.getPwDatabase().getIconFactory();
|
||||
PwIcon icon = iconFactory.getIcon(
|
||||
new UUID(cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS)),
|
||||
cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS))));
|
||||
if (icon.isUnknown()) {
|
||||
icon = iconFactory.getIcon(cursor.getInt(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_STANDARD)));
|
||||
if (icon.isUnknown())
|
||||
icon = iconFactory.getKeyIcon();
|
||||
}
|
||||
String title = cursor.getString( cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_TITLE) );
|
||||
String username = cursor.getString( cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_USERNAME) );
|
||||
String url = cursor.getString( cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_URL) );
|
||||
|
||||
ViewHolder viewHolder = (ViewHolder) view.getTag();
|
||||
|
||||
// Assign image
|
||||
database.getDrawFactory().assignDatabaseIconTo(context, viewHolder.imageViewIcon, icon, iconColor);
|
||||
|
||||
// Assign title
|
||||
String showTitle = PwEntry.getVisualTitle(false, title, username, url, uuid);
|
||||
viewHolder.textViewTitle.setText(showTitle);
|
||||
if (displayUsername && !username.isEmpty()) {
|
||||
viewHolder.textViewSubTitle.setText(String.format("(%s)", username));
|
||||
} else {
|
||||
viewHolder.textViewSubTitle.setText("");
|
||||
}
|
||||
}
|
||||
|
||||
private static class ViewHolder {
|
||||
ImageView imageViewIcon;
|
||||
TextView textViewTitle;
|
||||
TextView textViewSubTitle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
|
||||
return database.searchEntry(constraint.toString());
|
||||
}
|
||||
|
||||
public PwEntry getEntryFromPosition(int position) {
|
||||
PwEntry pwEntry = null;
|
||||
|
||||
Cursor cursor = this.getCursor();
|
||||
if (cursor.moveToFirst()
|
||||
&&
|
||||
cursor.move(position)) {
|
||||
|
||||
pwEntry = database.createEntry();
|
||||
database.populateEntry(pwEntry, (EntryCursor) cursor);
|
||||
}
|
||||
return pwEntry;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.graphics.Color
|
||||
import androidx.cursoradapter.widget.CursorAdapter
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.cursor.EntryCursor
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.EntryVersioned
|
||||
import com.kunzisoft.keepass.database.element.PwIcon
|
||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import java.util.*
|
||||
|
||||
class SearchEntryCursorAdapter(context: Context, private val database: Database)
|
||||
: androidx.cursoradapter.widget.CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
|
||||
|
||||
private val cursorInflater: LayoutInflater = context.getSystemService(
|
||||
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
private var displayUsername: Boolean = false
|
||||
private val iconColor: Int
|
||||
|
||||
init {
|
||||
// Get the icon color
|
||||
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
||||
this.iconColor = taTextColor.getColor(0, Color.WHITE)
|
||||
taTextColor.recycle()
|
||||
|
||||
reInit(context)
|
||||
}
|
||||
|
||||
fun reInit(context: Context) {
|
||||
this.displayUsername = PreferencesUtil.showUsernamesListEntries(context)
|
||||
}
|
||||
|
||||
override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
|
||||
|
||||
val view = cursorInflater.inflate(R.layout.item_search_entry, parent, false)
|
||||
val viewHolder = ViewHolder()
|
||||
viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon)
|
||||
viewHolder.textViewTitle = view.findViewById(R.id.entry_text)
|
||||
viewHolder.textViewSubTitle = view.findViewById(R.id.entry_subtext)
|
||||
view.tag = viewHolder
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun bindView(view: View, context: Context, cursor: Cursor) {
|
||||
|
||||
// Retrieve elements from cursor
|
||||
val uuid = UUID(cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS)),
|
||||
cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS)))
|
||||
val iconFactory = database.iconFactory
|
||||
var icon: PwIcon = iconFactory.getIcon(
|
||||
UUID(cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_CUSTOM_UUID_MOST_SIGNIFICANT_BITS)),
|
||||
cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_CUSTOM_UUID_LEAST_SIGNIFICANT_BITS))))
|
||||
if (icon.isUnknown) {
|
||||
icon = iconFactory.getIcon(cursor.getInt(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_STANDARD)))
|
||||
if (icon.isUnknown)
|
||||
icon = iconFactory.keyIcon
|
||||
}
|
||||
val title = cursor.getString(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_TITLE))
|
||||
val username = cursor.getString(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_USERNAME))
|
||||
val url = cursor.getString(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_URL))
|
||||
|
||||
val viewHolder = view.tag as ViewHolder
|
||||
|
||||
// Assign image
|
||||
viewHolder.imageViewIcon?.assignDatabaseIcon(database.drawFactory, icon, iconColor)
|
||||
|
||||
// Assign title
|
||||
val showTitle = EntryVersioned.getVisualTitle(false, title, username, url, uuid.toString())
|
||||
viewHolder.textViewTitle?.text = showTitle
|
||||
if (displayUsername && username.isNotEmpty()) {
|
||||
viewHolder.textViewSubTitle?.text = String.format("(%s)", username)
|
||||
} else {
|
||||
viewHolder.textViewSubTitle?.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
private class ViewHolder {
|
||||
internal var imageViewIcon: ImageView? = null
|
||||
internal var textViewTitle: TextView? = null
|
||||
internal var textViewSubTitle: TextView? = null
|
||||
}
|
||||
|
||||
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
|
||||
return database.searchEntries(constraint.toString())
|
||||
}
|
||||
|
||||
fun getEntryFromPosition(position: Int): EntryVersioned? {
|
||||
var pwEntry: EntryVersioned? = null
|
||||
|
||||
val cursor = this.cursor
|
||||
if (cursor.moveToFirst() && cursor.move(position)) {
|
||||
pwEntry = database.getEntryFrom(cursor)
|
||||
}
|
||||
return pwEntry
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.app;
|
||||
|
||||
import android.support.multidex.MultiDexApplication;
|
||||
|
||||
import com.kunzisoft.keepass.compat.PRNGFixes;
|
||||
import com.kunzisoft.keepass.database.Database;
|
||||
import com.kunzisoft.keepass.fileselect.RecentFileHistory;
|
||||
import com.kunzisoft.keepass.stylish.Stylish;
|
||||
|
||||
import java.util.Calendar;
|
||||
|
||||
public class App extends MultiDexApplication {
|
||||
private static Database db = null;
|
||||
private static boolean shutdown = false;
|
||||
private static CharSequence mMessage = "";
|
||||
private static Calendar calendar = null;
|
||||
private static RecentFileHistory fileHistory = null;
|
||||
|
||||
public static Database getDB() {
|
||||
if ( db == null ) {
|
||||
db = new Database();
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
public static RecentFileHistory getFileHistory() {
|
||||
return fileHistory;
|
||||
}
|
||||
|
||||
public static void setDB(Database d) {
|
||||
db = d;
|
||||
}
|
||||
|
||||
public static boolean isShutdown() {
|
||||
return shutdown;
|
||||
}
|
||||
|
||||
public static void setShutdown() {
|
||||
shutdown = true;
|
||||
mMessage = "";
|
||||
}
|
||||
|
||||
public static void setShutdown(CharSequence message) {
|
||||
shutdown = true;
|
||||
mMessage = message;
|
||||
}
|
||||
|
||||
public static CharSequence getMessage() {
|
||||
return mMessage;
|
||||
}
|
||||
|
||||
public static void clearShutdown() {
|
||||
shutdown = false;
|
||||
mMessage = "";
|
||||
}
|
||||
|
||||
public static Calendar getCalendar() {
|
||||
if ( calendar == null ) {
|
||||
calendar = Calendar.getInstance();
|
||||
}
|
||||
return calendar;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
Stylish.init(this);
|
||||
fileHistory = new RecentFileHistory(this);
|
||||
PRNGFixes.apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
if ( db != null ) {
|
||||
db.clear();
|
||||
}
|
||||
super.onTerminate();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2018 Jeremy Jamet / Kunzisoft.
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
@@ -17,19 +17,23 @@
|
||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.adapters;
|
||||
package com.kunzisoft.keepass.app
|
||||
|
||||
import android.view.View;
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
|
||||
import com.kunzisoft.keepass.R;
|
||||
class App : MultiDexApplication() {
|
||||
|
||||
class GroupViewHolder extends BasicViewHolder {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
GroupViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
container = itemView.findViewById(R.id.group_container);
|
||||
icon = itemView.findViewById(R.id.group_icon);
|
||||
text = itemView.findViewById(R.id.group_text);
|
||||
subText = itemView.findViewById(R.id.group_subtext);
|
||||
Stylish.init(this)
|
||||
PRNGFixes.apply()
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
||||
super.onTerminate()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.kunzisoft.keepass.compat;
|
||||
package com.kunzisoft.keepass.app;
|
||||
|
||||
/*
|
||||
* This software is provided 'as-is', without any express or implied
|
||||
@@ -13,7 +13,7 @@ package com.kunzisoft.keepass.compat;
|
||||
import android.os.Build;
|
||||
import android.os.Process;
|
||||
|
||||
import com.kunzisoft.keepass.utils.StrUtil;
|
||||
import com.kunzisoft.keepass.utils.StringUtil;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@@ -66,7 +66,7 @@ public final class PRNGFixes {
|
||||
|
||||
private static boolean supportedOnThisDevice() {
|
||||
// Blacklist on samsung devices
|
||||
if (StrUtil.indexOfIgnoreCase(Build.MANUFACTURER, "samsung", Locale.ENGLISH) >= 0) {
|
||||
if (StringUtil.INSTANCE.indexOfIgnoreCase(Build.MANUFACTURER, "samsung", Locale.ENGLISH) >= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.kunzisoft.keepass.app.database
|
||||
|
||||
import android.os.AsyncTask
|
||||
|
||||
/**
|
||||
* Private class to invoke each method in a separate thread
|
||||
*/
|
||||
class ActionDatabaseAsyncTask<T>(
|
||||
private val action: () -> T ,
|
||||
private val afterActionDatabaseListener: ((T?) -> Unit)? = null
|
||||
) : AsyncTask<Void, Void, T>() {
|
||||
|
||||
override fun doInBackground(vararg args: Void?): T? {
|
||||
return action.invoke()
|
||||
}
|
||||
|
||||
override fun onPostExecute(result: T?) {
|
||||
afterActionDatabaseListener?.invoke(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.kunzisoft.keepass.app.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import android.content.Context
|
||||
|
||||
@Database(version = 1, entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class])
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun fileDatabaseHistoryDao(): FileDatabaseHistoryDao
|
||||
abstract fun cipherDatabaseDao(): CipherDatabaseDao
|
||||
|
||||
companion object {
|
||||
fun getDatabase(applicationContext: Context): AppDatabase {
|
||||
return Room.databaseBuilder(
|
||||
applicationContext,
|
||||
AppDatabase::class.java, "com.kunzisoft.keepass.database"
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user