mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
619 Commits
2.5.0.0bet
...
2.5.0.0bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
c303ffafb5 | ||
|
|
6f513b4920 | ||
|
|
a83c60583f | ||
|
|
cde8950257 | ||
|
|
0b4dd1e909 | ||
|
|
28e2600271 | ||
|
|
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 | ||
|
|
edc7324c1d | ||
|
|
b9190e8254 | ||
|
|
534878ae99 | ||
|
|
99bfd21ecc | ||
|
|
cb5c324c9d | ||
|
|
55dc504f26 | ||
|
|
ecb0138c90 | ||
|
|
0860eeb87f | ||
|
|
f08bff61cf | ||
|
|
432aca6465 | ||
|
|
7cdb8db146 | ||
|
|
6ba9dedcb8 | ||
|
|
6d603608f4 | ||
|
|
64f4d9fb84 | ||
|
|
60ccc450ae | ||
|
|
ddb5f327a3 | ||
|
|
df90ea42eb | ||
|
|
a062d648b3 | ||
|
|
a59ae820b5 | ||
|
|
228831acdd | ||
|
|
bf15ee43da | ||
|
|
608f45677c | ||
|
|
1cb15f214b | ||
|
|
cd4a9e9b03 | ||
|
|
d6eae56d4f | ||
|
|
b5a87a63dc | ||
|
|
1eb17c4f34 | ||
|
|
8a6ce1f711 | ||
|
|
0f22f8af45 | ||
|
|
b2e81e6fd9 | ||
|
|
f82eab942d | ||
|
|
aa29aec40f | ||
|
|
181def52ab | ||
|
|
96d2bd63cc | ||
|
|
194021a957 | ||
|
|
9e307f94ea | ||
|
|
155b2de138 | ||
|
|
c76c3fd2be | ||
|
|
01c5554944 | ||
|
|
542cf65b41 | ||
|
|
f037561a67 | ||
|
|
379263a6d3 | ||
|
|
c420ca01f6 | ||
|
|
a1f9db6eee | ||
|
|
c9212174c4 | ||
|
|
0065336377 | ||
|
|
4f9625a3e1 | ||
|
|
7688ebd29b | ||
|
|
3f2a7f1eb3 | ||
|
|
b3d067d0c8 | ||
|
|
a3b4ad5ac1 | ||
|
|
e3b329d27f | ||
|
|
7d10c43822 | ||
|
|
fcb0d45d39 | ||
|
|
5492db0223 | ||
|
|
2207b05f5f | ||
|
|
f15a0c2591 | ||
|
|
92fb22129c | ||
|
|
ccca9c4400 | ||
|
|
0597cb4416 | ||
|
|
0602174e50 | ||
|
|
6fddc92ce7 | ||
|
|
aa30df6454 | ||
|
|
f6c61ab407 | ||
|
|
2ab81ed77c | ||
|
|
0ade035f43 | ||
|
|
73b62035d8 | ||
|
|
28837db308 | ||
|
|
85befef260 | ||
|
|
e7bbb47422 | ||
|
|
846bc7edb1 | ||
|
|
99a9842a1f | ||
|
|
33009138c3 | ||
|
|
c74b82ebd8 | ||
|
|
49ea92a4a5 | ||
|
|
6cc4eeaa30 | ||
|
|
1b89f79888 | ||
|
|
07d99de3ea | ||
|
|
ae757affa1 | ||
|
|
482b6296fc | ||
|
|
d37ce729f0 | ||
|
|
e92ce232bc | ||
|
|
78ae44b160 | ||
|
|
49ce87a8e0 | ||
|
|
d2fe5ce884 | ||
|
|
5e695756de | ||
|
|
0506a75417 | ||
|
|
34e1316144 | ||
|
|
18dffa6c75 | ||
|
|
03621c378e | ||
|
|
aff9312419 | ||
|
|
4651b1be96 | ||
|
|
32497a22d2 | ||
|
|
3b5ef56c16 | ||
|
|
d4cc3a58d8 | ||
|
|
afedbb38b2 | ||
|
|
fe70bf3877 | ||
|
|
f14ee6cf1c | ||
|
|
1791b15f85 | ||
|
|
6c5b3b3a0d | ||
|
|
6ea2287679 | ||
|
|
cad4ec22b4 | ||
|
|
f277983caf | ||
|
|
5e561e5321 | ||
|
|
ef83dbcae2 | ||
|
|
ab6c69adcb | ||
|
|
1c90747476 | ||
|
|
c3b3d8482c | ||
|
|
86b4d92599 | ||
|
|
1f6786b1f8 | ||
|
|
4cd2b153f4 | ||
|
|
27b0810688 | ||
|
|
94c72a4cf6 | ||
|
|
e634116e71 | ||
|
|
9021cb5bc8 | ||
|
|
3674900a54 | ||
|
|
9855ae79c3 | ||
|
|
fe1a4985f1 | ||
|
|
015a368a1d | ||
|
|
07f59e071d | ||
|
|
670e2bfe33 | ||
|
|
992b6382a3 | ||
|
|
c3ac550c93 | ||
|
|
acba7fc5de | ||
|
|
ccb500fdf4 | ||
|
|
44111507e7 | ||
|
|
5f14596ed2 | ||
|
|
4ad65c8f4a | ||
|
|
b4b215fe30 | ||
|
|
d8a8005d70 | ||
|
|
37ca15b77c | ||
|
|
6c18fe5591 | ||
|
|
fdcb05467c | ||
|
|
ef8db46ae7 | ||
|
|
14f56c77e8 | ||
|
|
44e21084e4 | ||
|
|
b7f1275789 | ||
|
|
b1be05db4d | ||
|
|
09c776fd7e | ||
|
|
e8a24790a5 | ||
|
|
8b3be79266 | ||
|
|
e5f0572c1c | ||
|
|
68df3bc6c3 | ||
|
|
a3da960c26 | ||
|
|
d9490f9840 | ||
|
|
3b24f9d821 | ||
|
|
bf35897e92 | ||
|
|
977705b42d | ||
|
|
459bc40515 | ||
|
|
eb8ca9355c | ||
|
|
31f7b0d5be | ||
|
|
7c54946c4b | ||
|
|
5aaf2c222a | ||
|
|
9424feefce | ||
|
|
a3917ccab6 | ||
|
|
5286a60142 | ||
|
|
d4459de49b | ||
|
|
05dba6668c | ||
|
|
c15a1c6eaa | ||
|
|
fe8c962f73 | ||
|
|
4192cf2403 | ||
|
|
1817f1aa9e | ||
|
|
afacf352ed | ||
|
|
d3ebbba2a1 | ||
|
|
460edf1745 | ||
|
|
dacb19d412 | ||
|
|
fe8158db85 | ||
|
|
a36e4fbcd0 | ||
|
|
9442c7ef07 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -71,6 +71,7 @@ app/app.iml
|
|||||||
# Art
|
# Art
|
||||||
art/screen*.png
|
art/screen*.png
|
||||||
art/logo_512.png
|
art/logo_512.png
|
||||||
|
art/store_screens/
|
||||||
|
|
||||||
# Dir linux
|
# Dir linux
|
||||||
.directory
|
.directory
|
||||||
|
|||||||
75
CHANGELOG
75
CHANGELOG
@@ -1,3 +1,78 @@
|
|||||||
|
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
|
||||||
|
* Added the display of usernames
|
||||||
|
* Added translations
|
||||||
|
* Fix read-only mode
|
||||||
|
* Fix parcelable / toolbar / back
|
||||||
|
|
||||||
|
KeepassDX (2.5.0.0beta15)
|
||||||
|
* Read only mode
|
||||||
|
* Best group recovery for the navigation fragment
|
||||||
|
* Fix copies in notifications
|
||||||
|
* Fix orientation
|
||||||
|
* Added translations
|
||||||
|
|
||||||
|
KeepassDX (2.5.0.0beta14)
|
||||||
|
* Optimize all the memory with parcelables / fix search
|
||||||
|
|
||||||
|
KeepassDX (2.5.0.0beta13)
|
||||||
|
* Fix memory issue with parcelable (crash in beta12 version)
|
||||||
|
|
||||||
|
KeepassDX (2.5.0.0beta12)
|
||||||
|
* Added the Magikeyboard to fill the forms (settings still in development)
|
||||||
|
* Added move and copy for groups and entries
|
||||||
|
* New navigation in a single screen / new animations between activities
|
||||||
|
* New icons for the material pack / vectorization
|
||||||
|
* New adaptive launcher icon
|
||||||
|
* Icons depends now of setting of list items
|
||||||
|
* Added a setting to disable the open button when no password is identified
|
||||||
|
* Added a setting to disable the education screens
|
||||||
|
* Added a setting to disable the copy of protected custom fields
|
||||||
|
* Fix the fingerprint recognition (WARNING : The keystore is reinit, you must delete the old keys)
|
||||||
|
* Fix small bugs
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta11)
|
KeepassDX (2.5.0.0beta11)
|
||||||
* Fix crash in beta10 version
|
* Fix crash in beta10 version
|
||||||
|
|
||||||
|
|||||||
50
FAQ.md
50
FAQ.md
@@ -1,50 +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 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.
|
||||||
14
ReadMe.md
14
ReadMe.md
@@ -13,7 +13,9 @@
|
|||||||
* **Fingerprint** for fast unlocking
|
* **Fingerprint** for fast unlocking
|
||||||
* Material design with **themes**
|
* Material design with **themes**
|
||||||
* **AutoFill** and Integration
|
* **AutoFill** and Integration
|
||||||
|
* Field filling **keyboard**
|
||||||
* Precise management of **settings**
|
* Precise management of **settings**
|
||||||
|
* Code written in **native language** *(Kotlin / Java / JNI / C)*
|
||||||
|
|
||||||
Keepass DX is **open source** and **ad-free**.
|
Keepass DX is **open source** and **ad-free**.
|
||||||
|
|
||||||
@@ -27,14 +29,14 @@ KeePass DX is a **free open source password manager for Android**, which helps y
|
|||||||
|
|
||||||
Yes, KeePass DX is under **free license (OSI certified)** and **without advertising**. You can have a look at its full source and check whether the encryption algorithms are implemented correctly.
|
Yes, KeePass DX is under **free license (OSI certified)** and **without advertising**. You can have a look at its full source and check whether the encryption algorithms are implemented correctly.
|
||||||
|
|
||||||
*Note : If you access the application from a store, visual features may not be available to incentivize the contribution to the work of open source projects. These optional visuals are accessible after a donation (and a small congratulation message :) or the purchase of an extended version, but do not worry, the main features remain completely free.*
|
*Note : If you access the application from a store, visual features may not be available to incentivize the contribution to the work of open source projects. These optional visuals are accessible after a donation (and a small congratulation message :) or the purchase of an extended version, but do not worry, the main features remain completely free. If you contribute to the project and you do not have access to the themes, do not hesitate to contact me at [contact@kunzisoft.com](contact@kunzisoft.com), I will give you the procedure.*
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
You can contribute in different ways to help us on our work.
|
You can contribute in different ways to help us on our work.
|
||||||
|
|
||||||
* Add features by a **pull request**.
|
* Add features by a **[pull request](https://help.github.com/articles/about-pull-requests/)**.
|
||||||
* Help to **translate** into your language
|
* Help to **[translate](https://hosted.weblate.org/projects/keepass-dx/strings/)** into your language (By using [Weblate](https://hosted.weblate.org/projects/keepass-dx/) or with a manual [pull request](https://help.github.com/articles/about-pull-requests/))
|
||||||
* **[Donate](https://www.kunzisoft.com/donation)** 人◕ ‿‿ ◕人Y for a better service and a quick development of your features.
|
* **[Donate](https://www.kunzisoft.com/donation)** 人◕ ‿‿ ◕人Y for a better service and a quick development of your features.
|
||||||
* Buy the **[Pro version](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro)** of KeePass DX
|
* Buy the **[Pro version](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro)** of KeePass DX
|
||||||
|
|
||||||
@@ -52,7 +54,7 @@ You can contribute in different ways to help us on our work.
|
|||||||
|
|
||||||
## F.A.Q.
|
## F.A.Q.
|
||||||
|
|
||||||
Other questions? You can read the [F.A.Q.](https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/FAQ.md)
|
Other questions? You can read the [F.A.Q.](https://www.keepassdx.com/FAQ)
|
||||||
|
|
||||||
## Other devices
|
## Other devices
|
||||||
|
|
||||||
@@ -62,11 +64,11 @@ Other questions? You can read the [F.A.Q.](https://raw.githubusercontent.com/Kun
|
|||||||
|
|
||||||
## License
|
## 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.
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlin-android-extensions'
|
||||||
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 27
|
compileSdkVersion 27
|
||||||
buildToolsVersion '27.0.3'
|
buildToolsVersion '28.0.3'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 14
|
minSdkVersion 14
|
||||||
targetSdkVersion 27
|
targetSdkVersion 27
|
||||||
versionCode = 11
|
versionCode = 22
|
||||||
versionName = "2.5.0.0beta11"
|
versionName = "2.5.0.0beta22"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
testInstrumentationRunner = "android.test.InstrumentationTestRunner"
|
testInstrumentationRunner = "android.test.InstrumentationTestRunner"
|
||||||
|
|
||||||
ndk {
|
|
||||||
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
|
||||||
}
|
|
||||||
|
|
||||||
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
|
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +42,7 @@ android {
|
|||||||
productFlavors {
|
productFlavors {
|
||||||
libre {
|
libre {
|
||||||
applicationIdSuffix = ".libre"
|
applicationIdSuffix = ".libre"
|
||||||
versionNameSuffix "-libre"
|
buildConfigField "String", "BUILD_VERSION", "\"libre\""
|
||||||
buildConfigField "boolean", "FULL_VERSION", "true"
|
buildConfigField "boolean", "FULL_VERSION", "true"
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "false"
|
buildConfigField "boolean", "CLOSED_STORE", "false"
|
||||||
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Dark\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Dark\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
||||||
@@ -52,7 +51,7 @@ android {
|
|||||||
}
|
}
|
||||||
pro {
|
pro {
|
||||||
applicationIdSuffix = ".pro"
|
applicationIdSuffix = ".pro"
|
||||||
versionNameSuffix "-pro"
|
buildConfigField "String", "BUILD_VERSION", "\"pro\""
|
||||||
buildConfigField "boolean", "FULL_VERSION", "true"
|
buildConfigField "boolean", "FULL_VERSION", "true"
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "true"
|
buildConfigField "boolean", "CLOSED_STORE", "true"
|
||||||
buildConfigField "String[]", "STYLES_DISABLED", "{}"
|
buildConfigField "String[]", "STYLES_DISABLED", "{}"
|
||||||
@@ -60,7 +59,7 @@ android {
|
|||||||
}
|
}
|
||||||
free {
|
free {
|
||||||
applicationIdSuffix = ".free"
|
applicationIdSuffix = ".free"
|
||||||
versionNameSuffix "-free"
|
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
||||||
buildConfigField "boolean", "FULL_VERSION", "false"
|
buildConfigField "boolean", "FULL_VERSION", "false"
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "true"
|
buildConfigField "boolean", "CLOSED_STORE", "true"
|
||||||
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Dark\",\"KeepassDXStyle_Blue\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Dark\",\"KeepassDXStyle_Blue\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
||||||
@@ -82,14 +81,16 @@ android {
|
|||||||
|
|
||||||
def supportVersion = "27.1.1"
|
def supportVersion = "27.1.1"
|
||||||
def spongycastleVersion = "1.58.0.0"
|
def spongycastleVersion = "1.58.0.0"
|
||||||
def permissionDispatcherVersion = "3.1.0"
|
def permissionDispatcherVersion = "3.3.1"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
implementation "com.android.support:appcompat-v7:$supportVersion"
|
implementation "com.android.support:appcompat-v7:$supportVersion"
|
||||||
implementation "com.android.support:design:$supportVersion"
|
implementation "com.android.support:design:$supportVersion"
|
||||||
implementation "com.android.support:preference-v7:$supportVersion"
|
implementation "com.android.support:preference-v7:$supportVersion"
|
||||||
implementation "com.android.support:preference-v14:$supportVersion"
|
implementation "com.android.support:preference-v14:$supportVersion"
|
||||||
implementation "com.android.support:cardview-v7:$supportVersion"
|
implementation "com.android.support:cardview-v7:$supportVersion"
|
||||||
|
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
|
||||||
implementation "com.madgag.spongycastle:core:$spongycastleVersion"
|
implementation "com.madgag.spongycastle:core:$spongycastleVersion"
|
||||||
implementation "com.madgag.spongycastle:prov:$spongycastleVersion"
|
implementation "com.madgag.spongycastle:prov:$spongycastleVersion"
|
||||||
// Expandable view
|
// Expandable view
|
||||||
@@ -98,20 +99,23 @@ dependencies {
|
|||||||
implementation 'joda-time:joda-time:2.9.9'
|
implementation 'joda-time:joda-time:2.9.9'
|
||||||
implementation 'org.sufficientlysecure:html-textview:3.5'
|
implementation 'org.sufficientlysecure:html-textview:3.5'
|
||||||
implementation 'com.nononsenseapps:filepicker:4.1.0'
|
implementation 'com.nononsenseapps:filepicker:4.1.0'
|
||||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.11.0'
|
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.12.0'
|
||||||
// Permissions
|
// Permissions
|
||||||
implementation("com.github.hotchemi:permissionsdispatcher:$permissionDispatcherVersion") {
|
implementation("com.github.hotchemi:permissionsdispatcher:$permissionDispatcherVersion") {
|
||||||
// if you don't use android.app.Fragment you can exclude support for them
|
// if you don't use android.app.Fragment you can exclude support for them
|
||||||
exclude module: "support-v13"
|
exclude module: "support-v13"
|
||||||
}
|
}
|
||||||
annotationProcessor "com.github.hotchemi:permissionsdispatcher-processor:$permissionDispatcherVersion"
|
kapt "com.github.hotchemi:permissionsdispatcher-processor:$permissionDispatcherVersion"
|
||||||
// Apache Commons Collections
|
// Apache Commons Collections
|
||||||
implementation 'commons-collections:commons-collections:3.2.1'
|
implementation 'commons-collections:commons-collections:3.2.1'
|
||||||
|
implementation 'org.apache.commons:commons-io:1.3.2'
|
||||||
// Base64
|
// Base64
|
||||||
implementation 'biz.source_code:base64coder:2010-12-19'
|
implementation 'biz.source_code:base64coder:2010-12-19'
|
||||||
implementation 'com.google.code.gson:gson:2.8.1'
|
// IO-Extras
|
||||||
|
implementation 'com.github.davidmoten:io-extras:0.1'
|
||||||
|
implementation 'com.google.code.gson:gson:2.8.4'
|
||||||
implementation 'com.google.guava:guava:23.0-android'
|
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-classic')
|
||||||
implementation project(path: ':icon-pack-material')
|
implementation project(path: ':icon-pack-material')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* KeePass DX is distributed in the hope that it will be useful,
|
* KeePass DX is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* GNU General Public License for more details.
|
* GNU General Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests;
|
package com.kunzisoft.keepass.tests;
|
||||||
|
|
||||||
import android.test.AndroidTestCase;
|
import android.test.AndroidTestCase;
|
||||||
@@ -24,19 +24,21 @@ import android.test.AndroidTestCase;
|
|||||||
import com.kunzisoft.keepass.tests.database.TestData;
|
import com.kunzisoft.keepass.tests.database.TestData;
|
||||||
|
|
||||||
public class AccentTest extends AndroidTestCase {
|
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 {
|
private static final String KEYFILE = "";
|
||||||
TestData.GetDb(getContext(), ASSET, PASSWORD, KEYFILE, FILENAME);
|
private static final String PASSWORD = "é";
|
||||||
} catch (Exception e) {
|
private static final String ASSET = "accent.kdb";
|
||||||
assertTrue("Failed to open database", false);
|
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,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePass DX is free software: you can redistribute it and/or modify
|
||||||
@@ -26,10 +26,10 @@ import android.test.suitebuilder.TestSuiteBuilder;
|
|||||||
|
|
||||||
public class OutputTests extends TestSuite {
|
public class OutputTests extends TestSuite {
|
||||||
|
|
||||||
public static Test suite() {
|
public static Test suite() {
|
||||||
|
|
||||||
return new TestSuiteBuilder(AllTests.class)
|
return new TestSuiteBuilder(AllTests.class)
|
||||||
.includePackages("com.kunzisoft.keepass.tests.output")
|
.includePackages("com.kunzisoft.keepass.tests.output")
|
||||||
.build();
|
.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.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
@@ -17,21 +17,21 @@
|
|||||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.settings.preferenceDialogFragment.adapter;
|
package com.kunzisoft.keepass.tests
|
||||||
|
|
||||||
import android.support.v7.widget.RecyclerView;
|
import junit.framework.TestCase
|
||||||
import android.view.View;
|
|
||||||
import android.widget.RadioButton;
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R;
|
import com.kunzisoft.keepass.database.element.PwDate
|
||||||
|
import org.junit.Assert
|
||||||
|
|
||||||
public class ListRadioViewHolder extends RecyclerView.ViewHolder {
|
class PwDateTest : TestCase() {
|
||||||
|
|
||||||
public RadioButton radioButton;
|
fun testDate() {
|
||||||
|
val jDate = PwDate(System.currentTimeMillis())
|
||||||
|
val intermediate = PwDate(jDate)
|
||||||
|
val cDate = PwDate(intermediate.byteArrayDate!!, 0)
|
||||||
|
|
||||||
public ListRadioViewHolder(View itemView) {
|
Assert.assertTrue("jDate and intermediate not equal", jDate == intermediate)
|
||||||
super(itemView);
|
Assert.assertTrue("jDate and cDate not equal", cDate == jDate)
|
||||||
|
|
||||||
radioButton = itemView.findViewById(R.id.pref_dialog_list_radio);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePass DX is free software: you can redistribute it and/or modify
|
||||||
@@ -27,37 +27,37 @@ import java.util.Calendar;
|
|||||||
|
|
||||||
import android.test.AndroidTestCase;
|
import android.test.AndroidTestCase;
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.PwEntryV3;
|
import com.kunzisoft.keepass.database.element.PwEntryV3;
|
||||||
import com.kunzisoft.keepass.tests.database.TestData;
|
import com.kunzisoft.keepass.tests.database.TestData;
|
||||||
|
|
||||||
public class PwEntryTestV3 extends AndroidTestCase {
|
public class PwEntryTestV3 extends AndroidTestCase {
|
||||||
PwEntryV3 mPE;
|
PwEntryV3 mPE;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setUp() throws Exception {
|
protected void setUp() throws Exception {
|
||||||
super.setUp();
|
super.setUp();
|
||||||
|
|
||||||
mPE = (PwEntryV3) TestData.GetTest1(getContext()).getEntryAt(0);
|
// mPE = (PwEntryV3) TestData.GetTest1(getContext()).getEntryAt(0);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testName() {
|
public void testName() {
|
||||||
assertTrue("Name was " + mPE.getTitle(), mPE.getTitle().equals("Amazon"));
|
assertTrue("Name was " + mPE.getTitle(), mPE.getTitle().equals("Amazon"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testPassword() throws UnsupportedEncodingException {
|
public void testPassword() throws UnsupportedEncodingException {
|
||||||
String sPass = "12345";
|
String sPass = "12345";
|
||||||
byte[] password = sPass.getBytes("UTF-8");
|
byte[] password = sPass.getBytes("UTF-8");
|
||||||
|
|
||||||
assertArrayEquals(password, mPE.getPasswordBytes());
|
assertArrayEquals(password, mPE.getPasswordBytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testCreation() {
|
public void testCreation() {
|
||||||
Calendar cal = Calendar.getInstance();
|
Calendar cal = Calendar.getInstance();
|
||||||
cal.setTime(mPE.getCreationTime().getDate());
|
cal.setTime(mPE.getCreationTime().getDate());
|
||||||
|
|
||||||
assertEquals("Incorrect year.", cal.get(Calendar.YEAR), 2009);
|
assertEquals("Incorrect year.", cal.get(Calendar.YEAR), 2009);
|
||||||
assertEquals("Incorrect month.", cal.get(Calendar.MONTH), 3);
|
assertEquals("Incorrect month.", cal.get(Calendar.MONTH), 3);
|
||||||
assertEquals("Incorrect day.", cal.get(Calendar.DAY_OF_MONTH), 23);
|
assertEquals("Incorrect day.", cal.get(Calendar.DAY_OF_MONTH), 23);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePass DX is free software: you can redistribute it and/or modify
|
||||||
@@ -19,20 +19,12 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests;
|
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 junit.framework.TestCase;
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public class PwEntryTestV4 extends TestCase {
|
public class PwEntryTestV4 extends TestCase {
|
||||||
public void testAssign() {
|
public void testAssign() {
|
||||||
|
/*
|
||||||
|
TODO Test
|
||||||
PwEntryV4 entry = new PwEntryV4();
|
PwEntryV4 entry = new PwEntryV4();
|
||||||
|
|
||||||
entry.setAdditional("test223");
|
entry.setAdditional("test223");
|
||||||
@@ -43,17 +35,17 @@ public class PwEntryTestV4 extends TestCase {
|
|||||||
entry.getAutoType().obfuscationOptions = 123412432109L;
|
entry.getAutoType().obfuscationOptions = 123412432109L;
|
||||||
entry.getAutoType().put("key", "value");
|
entry.getAutoType().put("key", "value");
|
||||||
|
|
||||||
entry.setBackgroupColor("blue");
|
entry.setBackgroundColor("blue");
|
||||||
entry.putProtectedBinary("key1", new ProtectedBinary(false, new byte[] {0,1}));
|
entry.putProtectedBinary("key1", new ProtectedBinary(false, new byte[] {0,1}));
|
||||||
entry.setCustomIcon(new PwIconCustom(UUID.randomUUID(), new byte[0]));
|
entry.setIconCustom(new PwIconCustom(UUID.randomUUID(), new byte[0]));
|
||||||
entry.setForegroundColor("red");
|
entry.setForegroundColor("red");
|
||||||
entry.addToHistory(new PwEntryV4());
|
entry.addToHistory(new PwEntryV4());
|
||||||
entry.setIcon(new PwIconStandard(5));
|
entry.setIconStandard(new PwIconStandard(5));
|
||||||
entry.setOverrideURL("override");
|
entry.setOverrideURL("override");
|
||||||
entry.setParent(new PwGroupV4());
|
entry.setParent(new PwGroupV4());
|
||||||
entry.addExtraField("key2", new ProtectedString(false, "value2"));
|
entry.addExtraField("key2", new ProtectedString(false, "value2"));
|
||||||
entry.setUrl("http://localhost");
|
entry.setUrl("http://localhost");
|
||||||
entry.setUUID(UUID.randomUUID());
|
entry.setNodeId(UUID.randomUUID());
|
||||||
|
|
||||||
PwEntryV4 target = new PwEntryV4();
|
PwEntryV4 target = new PwEntryV4();
|
||||||
target.updateWith(entry);
|
target.updateWith(entry);
|
||||||
@@ -61,7 +53,7 @@ public class PwEntryTestV4 extends TestCase {
|
|||||||
/* This test is not so useful now that I am not implementing value equality for Entries
|
/* This test is not so useful now that I am not implementing value equality for Entries
|
||||||
assertTrue("Entries do not match.", entry.equals(target));
|
assertTrue("Entries do not match.", entry.equals(target));
|
||||||
*/
|
*/
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePass DX is free software: you can redistribute it and/or modify
|
||||||
@@ -22,23 +22,23 @@ package com.kunzisoft.keepass.tests;
|
|||||||
|
|
||||||
import android.test.AndroidTestCase;
|
import android.test.AndroidTestCase;
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.PwGroupV3;
|
import com.kunzisoft.keepass.database.element.PwGroupV3;
|
||||||
import com.kunzisoft.keepass.tests.database.TestData;
|
import com.kunzisoft.keepass.tests.database.TestData;
|
||||||
|
|
||||||
public class PwGroupTest extends AndroidTestCase {
|
public class PwGroupTest extends AndroidTestCase {
|
||||||
|
|
||||||
PwGroupV3 mPG;
|
PwGroupV3 mPG;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setUp() throws Exception {
|
protected void setUp() throws Exception {
|
||||||
super.setUp();
|
super.setUp();
|
||||||
|
|
||||||
mPG = (PwGroupV3) TestData.GetTest1(getContext()).getGroups().get(0);
|
//mPG = (PwGroupV3) TestData.GetTest1(getContext()).getGroups().get(0);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testGroupName() {
|
public void testGroupName() {
|
||||||
assertTrue("Name was " + mPG.getName(), mPG.getName().equals("Internet"));
|
//assertTrue("Name was " + mPG.getTitle(), mPG.getTitle().equals("Internet"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePass DX is free software: you can redistribute it and/or modify
|
||||||
@@ -19,53 +19,38 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests;
|
package com.kunzisoft.keepass.tests;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.AssetManager;
|
||||||
|
import android.os.Environment;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.InputStream;
|
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 {
|
public class TestUtil {
|
||||||
private static final File sdcard = Environment.getExternalStorageDirectory();
|
private static final File sdcard = Environment.getExternalStorageDirectory();
|
||||||
|
|
||||||
public static void extractKey(Context ctx, String asset, String target) throws Exception {
|
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();
|
|
||||||
|
|
||||||
}
|
InputStream key = ctx.getAssets().open(asset, AssetManager.ACCESS_STREAMING);
|
||||||
|
|
||||||
public static InputStream getKeyFileInputStream(Context ctx, String keyfile) throws FileNotFoundException {
|
FileOutputStream keyFile = new FileOutputStream(target);
|
||||||
InputStream keyIs = null;
|
while (true) {
|
||||||
if (!EmptyUtils.isNullOrEmpty(keyfile)) {
|
byte[] buf = new byte[1024];
|
||||||
Uri uri = UriUtil.parseDefaultFile(keyfile);
|
int read = key.read(buf);
|
||||||
keyIs = UriUtil.getUriInputStream(ctx, uri);
|
if ( read == -1 ) {
|
||||||
}
|
break;
|
||||||
|
} else {
|
||||||
|
keyFile.write(buf, 0, read);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return keyIs;
|
keyFile.close();
|
||||||
}
|
|
||||||
|
|
||||||
public static String getSdPath(String filename) {
|
}
|
||||||
File file = new File(sdcard, filename);
|
|
||||||
return file.getAbsolutePath();
|
public static String getSdPath(String filename) {
|
||||||
}
|
File file = new File(sdcard, filename);
|
||||||
|
return file.getAbsolutePath();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePass DX is free software: you can redistribute it and/or modify
|
||||||
@@ -28,184 +28,184 @@ import java.util.UUID;
|
|||||||
|
|
||||||
import junit.framework.TestCase;
|
import junit.framework.TestCase;
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.PwDate;
|
import com.kunzisoft.keepass.database.element.PwDate;
|
||||||
import com.kunzisoft.keepass.stream.LEDataInputStream;
|
import com.kunzisoft.keepass.stream.LEDataInputStream;
|
||||||
import com.kunzisoft.keepass.stream.LEDataOutputStream;
|
import com.kunzisoft.keepass.stream.LEDataOutputStream;
|
||||||
import com.kunzisoft.keepass.utils.Types;
|
import com.kunzisoft.keepass.utils.Types;
|
||||||
|
|
||||||
public class TypesTest extends TestCase {
|
public class TypesTest extends TestCase {
|
||||||
|
|
||||||
public void testReadWriteLongZero() {
|
public void testReadWriteLongZero() {
|
||||||
testReadWriteLong((byte) 0);
|
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 testReadWriteLongMax() {
|
||||||
|
testReadWriteLong(Byte.MAX_VALUE);
|
||||||
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);
|
public void testReadWriteLongMin() {
|
||||||
|
testReadWriteLong(Byte.MIN_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 testReadWriteLongRnd() {
|
||||||
|
Random rnd = new Random();
|
||||||
|
byte[] buf = new byte[1];
|
||||||
|
rnd.nextBytes(buf);
|
||||||
|
|
||||||
public void testReadWriteByteZero() {
|
testReadWriteLong(buf[0]);
|
||||||
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 {
|
private void testReadWriteLong(byte value) {
|
||||||
byte[] ulongBytes = new byte[8];
|
byte[] orig = new byte[8];
|
||||||
for (int i = 0; i < ulongBytes.length; i++) {
|
byte[] dest = new byte[8];
|
||||||
ulongBytes[i] = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
setArray(orig, value, 0, 8);
|
||||||
LEDataOutputStream leos = new LEDataOutputStream(bos);
|
|
||||||
leos.writeLong(Types.ULONG_MAX_VALUE);
|
|
||||||
leos.close();
|
|
||||||
|
|
||||||
byte[] uLongMax = bos.toByteArray();
|
long one = LEDataInputStream.readLong(orig, 0);
|
||||||
|
LEDataOutputStream.writeLong(one, dest, 0);
|
||||||
|
|
||||||
assertArrayEquals(ulongBytes, uLongMax);
|
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.Companion.writeTime(expected.getTime(), cal);
|
||||||
|
Calendar actual = Calendar.getInstance();
|
||||||
|
actual.setTime(PwDate.Companion.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* KeePass DX is distributed in the hope that it will be useful,
|
* KeePass DX is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* GNU General Public License for more details.
|
* GNU General Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests.crypto;
|
package com.kunzisoft.keepass.tests.crypto;
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory;
|
import com.kunzisoft.keepass.crypto.CipherFactory;
|
||||||
@@ -38,46 +38,46 @@ import javax.crypto.spec.SecretKeySpec;
|
|||||||
import static org.junit.Assert.assertArrayEquals;
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
|
|
||||||
public class AESTest extends TestCase {
|
public class AESTest extends TestCase {
|
||||||
|
|
||||||
private Random mRand = new Random();
|
private Random mRand = new Random();
|
||||||
|
|
||||||
public void testEncrypt() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
|
public void testEncrypt() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
|
||||||
// Test above below and at the blocksize
|
// Test above below and at the blocksize
|
||||||
testFinal(15);
|
testFinal(15);
|
||||||
testFinal(16);
|
testFinal(16);
|
||||||
testFinal(17);
|
testFinal(17);
|
||||||
|
|
||||||
// Test random larger sizes
|
// Test random larger sizes
|
||||||
int size = mRand.nextInt(494) + 18;
|
int size = mRand.nextInt(494) + 18;
|
||||||
testFinal(size);
|
testFinal(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void testFinal(int dataSize) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
|
private void testFinal(int dataSize) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
|
||||||
|
|
||||||
// Generate some input
|
// Generate some input
|
||||||
byte[] input = new byte[dataSize];
|
byte[] input = new byte[dataSize];
|
||||||
mRand.nextBytes(input);
|
mRand.nextBytes(input);
|
||||||
|
|
||||||
// Generate key
|
// Generate key
|
||||||
byte[] keyArray = new byte[32];
|
byte[] keyArray = new byte[32];
|
||||||
mRand.nextBytes(keyArray);
|
mRand.nextBytes(keyArray);
|
||||||
SecretKeySpec key = new SecretKeySpec(keyArray, "AES");
|
SecretKeySpec key = new SecretKeySpec(keyArray, "AES");
|
||||||
|
|
||||||
// Generate IV
|
// Generate IV
|
||||||
byte[] ivArray = new byte[16];
|
byte[] ivArray = new byte[16];
|
||||||
mRand.nextBytes(ivArray);
|
mRand.nextBytes(ivArray);
|
||||||
IvParameterSpec iv = new IvParameterSpec(ivArray);
|
IvParameterSpec iv = new IvParameterSpec(ivArray);
|
||||||
|
|
||||||
Cipher android = CipherFactory.getInstance("AES/CBC/PKCS5Padding", true);
|
Cipher android = CipherFactory.INSTANCE.getInstance("AES/CBC/PKCS5Padding", true);
|
||||||
android.init(Cipher.ENCRYPT_MODE, key, iv);
|
android.init(Cipher.ENCRYPT_MODE, key, iv);
|
||||||
byte[] outAndroid = android.doFinal(input, 0, dataSize);
|
byte[] outAndroid = android.doFinal(input, 0, dataSize);
|
||||||
|
|
||||||
Cipher nat = CipherFactory.getInstance("AES/CBC/PKCS5Padding");
|
Cipher nat = CipherFactory.INSTANCE.getInstance("AES/CBC/PKCS5Padding");
|
||||||
nat.init(Cipher.ENCRYPT_MODE, key, iv);
|
nat.init(Cipher.ENCRYPT_MODE, key, iv);
|
||||||
byte[] outNative = nat.doFinal(input, 0, dataSize);
|
byte[] outNative = nat.doFinal(input, 0, dataSize);
|
||||||
|
|
||||||
assertArrayEquals("Arrays differ on size: " + dataSize, outAndroid, outNative);
|
assertArrayEquals("Arrays differ on size: " + dataSize, outAndroid, outNative);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* KeePass DX is distributed in the hope that it will be useful,
|
* KeePass DX is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* GNU General Public License for more details.
|
* GNU General Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests.crypto;
|
package com.kunzisoft.keepass.tests.crypto;
|
||||||
|
|
||||||
import static org.junit.Assert.assertArrayEquals;
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
@@ -44,57 +44,57 @@ import com.kunzisoft.keepass.stream.BetterCipherInputStream;
|
|||||||
import com.kunzisoft.keepass.stream.LEDataInputStream;
|
import com.kunzisoft.keepass.stream.LEDataInputStream;
|
||||||
|
|
||||||
public class CipherTest extends TestCase {
|
public class CipherTest extends TestCase {
|
||||||
private Random rand = new Random();
|
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);
|
public void testCipherFactory() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
|
||||||
byte[] decrypttext = decrypt.doFinal(secrettext);
|
byte[] key = new byte[32];
|
||||||
|
byte[] iv = new byte[16];
|
||||||
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void testCipherStreams() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, IOException {
|
byte[] plaintext = new byte[1024];
|
||||||
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);
|
rand.nextBytes(key);
|
||||||
Cipher encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv);
|
rand.nextBytes(iv);
|
||||||
Cipher decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv);
|
rand.nextBytes(plaintext);
|
||||||
|
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
CipherEngine aes = CipherFactory.INSTANCE.getInstance(AesEngine.CIPHER_UUID);
|
||||||
CipherOutputStream cos = new CipherOutputStream(bos, encrypt);
|
Cipher encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv);
|
||||||
cos.write(plaintext);
|
Cipher decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv);
|
||||||
cos.close();
|
|
||||||
|
byte[] secrettext = encrypt.doFinal(plaintext);
|
||||||
byte[] secrettext = bos.toByteArray();
|
byte[] decrypttext = decrypt.doFinal(secrettext);
|
||||||
|
|
||||||
ByteArrayInputStream bis = new ByteArrayInputStream(secrettext);
|
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext);
|
||||||
BetterCipherInputStream cis = new BetterCipherInputStream(bis, decrypt);
|
}
|
||||||
LEDataInputStream lis = new LEDataInputStream(cis);
|
|
||||||
|
public void testCipherStreams() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, IOException {
|
||||||
byte[] decrypttext = lis.readBytes(MESSAGE_LENGTH);
|
final int MESSAGE_LENGTH = 1024;
|
||||||
|
|
||||||
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext);
|
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.INSTANCE.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* KeePass DX is distributed in the hope that it will be useful,
|
* KeePass DX is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* GNU General Public License for more details.
|
* GNU General Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests.crypto;
|
package com.kunzisoft.keepass.tests.crypto;
|
||||||
|
|
||||||
import static org.junit.Assert.assertArrayEquals;
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
@@ -30,37 +30,37 @@ import com.kunzisoft.keepass.crypto.finalkey.AndroidFinalKey;
|
|||||||
import com.kunzisoft.keepass.crypto.finalkey.NativeFinalKey;
|
import com.kunzisoft.keepass.crypto.finalkey.NativeFinalKey;
|
||||||
|
|
||||||
public class FinalKeyTest extends TestCase {
|
public class FinalKeyTest extends TestCase {
|
||||||
private Random mRand;
|
private Random mRand;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setUp() throws Exception {
|
protected void setUp() throws Exception {
|
||||||
super.setUp();
|
super.setUp();
|
||||||
|
|
||||||
mRand = new Random();
|
mRand = new Random();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testNativeAndroid() throws IOException {
|
public void testNativeAndroid() throws IOException {
|
||||||
// Test both an old and an even number to test my flip variable
|
// Test both an old and an even number to test my flip variable
|
||||||
testNativeFinalKey(5);
|
testNativeFinalKey(5);
|
||||||
testNativeFinalKey(6);
|
testNativeFinalKey(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void testNativeFinalKey(int rounds) throws IOException {
|
private void testNativeFinalKey(int rounds) throws IOException {
|
||||||
byte[] seed = new byte[32];
|
byte[] seed = new byte[32];
|
||||||
byte[] key = new byte[32];
|
byte[] key = new byte[32];
|
||||||
byte[] nativeKey;
|
byte[] nativeKey;
|
||||||
byte[] androidKey;
|
byte[] androidKey;
|
||||||
|
|
||||||
mRand.nextBytes(seed);
|
mRand.nextBytes(seed);
|
||||||
mRand.nextBytes(key);
|
mRand.nextBytes(key);
|
||||||
|
|
||||||
AndroidFinalKey aKey = new AndroidFinalKey();
|
AndroidFinalKey aKey = new AndroidFinalKey();
|
||||||
androidKey = aKey.transformMasterKey(seed, key, rounds);
|
androidKey = aKey.transformMasterKey(seed, key, rounds);
|
||||||
|
|
||||||
NativeFinalKey nKey = new NativeFinalKey();
|
NativeFinalKey nKey = new NativeFinalKey();
|
||||||
nativeKey = nKey.transformMasterKey(seed, key, rounds);
|
nativeKey = nKey.transformMasterKey(seed, key, rounds);
|
||||||
|
|
||||||
assertArrayEquals("Does not match", androidKey, nativeKey);
|
assertArrayEquals("Does not match", androidKey, nativeKey);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePass DX is free software: you can redistribute it and/or modify
|
||||||
@@ -19,31 +19,24 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests.database;
|
package com.kunzisoft.keepass.tests.database;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.test.AndroidTestCase;
|
import android.test.AndroidTestCase;
|
||||||
|
import com.kunzisoft.keepass.database.element.GroupVersioned;
|
||||||
import com.kunzisoft.keepass.database.Database;
|
import com.kunzisoft.keepass.database.element.PwDatabase;
|
||||||
import com.kunzisoft.keepass.database.PwDatabase;
|
import com.kunzisoft.keepass.database.element.PwDatabaseV3;
|
||||||
import com.kunzisoft.keepass.database.PwDatabaseV3;
|
import com.kunzisoft.keepass.database.element.PwEntryV3;
|
||||||
import com.kunzisoft.keepass.database.PwEntry;
|
|
||||||
import com.kunzisoft.keepass.database.PwEntryV3;
|
|
||||||
import com.kunzisoft.keepass.database.PwGroup;
|
|
||||||
import com.kunzisoft.keepass.database.action.DeleteGroupRunnable;
|
|
||||||
import com.kunzisoft.keepass.search.SearchDbHelper;
|
|
||||||
|
|
||||||
public class DeleteEntry extends AndroidTestCase {
|
public class DeleteEntry extends AndroidTestCase {
|
||||||
private static final String GROUP1_NAME = "Group1";
|
private static final String GROUP1_NAME = "Group1";
|
||||||
private static final String ENTRY1_NAME = "Test1";
|
private static final String ENTRY1_NAME = "Test1";
|
||||||
private static final String ENTRY2_NAME = "Test2";
|
private static final String ENTRY2_NAME = "Test2";
|
||||||
private static final String KEYFILE = "";
|
private static final String KEYFILE = "";
|
||||||
private static final String PASSWORD = "12345";
|
private static final String PASSWORD = "12345";
|
||||||
private static final String ASSET = "delete.kdb";
|
private static final String ASSET = "delete.kdb";
|
||||||
private static final String FILENAME = "/sdcard/delete.kdb";
|
private static final String FILENAME = "/sdcard/delete.kdb";
|
||||||
|
|
||||||
public void testDelete() {
|
public void testDelete() {
|
||||||
|
|
||||||
|
/*
|
||||||
Database db;
|
Database db;
|
||||||
|
|
||||||
Context ctx = getContext();
|
Context ctx = getContext();
|
||||||
@@ -56,7 +49,7 @@ public class DeleteEntry extends AndroidTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PwDatabaseV3 pm = (PwDatabaseV3) db.getPwDatabase();
|
PwDatabaseV3 pm = (PwDatabaseV3) db.getPwDatabase();
|
||||||
PwGroup group1 = getGroup(pm, GROUP1_NAME);
|
GroupVersioned group1 = getGroup(pm, GROUP1_NAME);
|
||||||
assertNotNull("Could not find group1", group1);
|
assertNotNull("Could not find group1", group1);
|
||||||
|
|
||||||
// Delete the group
|
// Delete the group
|
||||||
@@ -64,16 +57,16 @@ public class DeleteEntry extends AndroidTestCase {
|
|||||||
task.run();
|
task.run();
|
||||||
|
|
||||||
// Verify the entries were deleted
|
// Verify the entries were deleted
|
||||||
PwEntry entry1 = getEntry(pm, ENTRY1_NAME);
|
PwEntryInterface entry1 = getEntry(pm, ENTRY1_NAME);
|
||||||
assertNull("Entry 1 was not removed", entry1);
|
assertNull("Entry 1 was not removed", entry1);
|
||||||
|
|
||||||
PwEntry entry2 = getEntry(pm, ENTRY2_NAME);
|
PwEntryInterface entry2 = getEntry(pm, ENTRY2_NAME);
|
||||||
assertNull("Entry 2 was not removed", entry2);
|
assertNull("Entry 2 was not removed", entry2);
|
||||||
|
|
||||||
// Verify the entries were removed from the search index
|
// Verify the entries were removed from the search index
|
||||||
SearchDbHelper dbHelp = new SearchDbHelper(ctx);
|
SearchDbHelper dbHelp = new SearchDbHelper(ctx);
|
||||||
PwGroup results1 = dbHelp.search(db.getPwDatabase(), ENTRY1_NAME);
|
GroupVersioned results1 = dbHelp.search(db.getPwDatabase(), ENTRY1_NAME, 100);
|
||||||
PwGroup results2 = dbHelp.search(db.getPwDatabase(), ENTRY2_NAME);
|
GroupVersioned results2 = dbHelp.search(db.getPwDatabase(), ENTRY2_NAME, 100);
|
||||||
|
|
||||||
assertEquals("Entry1 was not removed from the search results", 0, results1.numbersOfChildEntries());
|
assertEquals("Entry1 was not removed from the search results", 0, results1.numbersOfChildEntries());
|
||||||
assertEquals("Entry2 was not removed from the search results", 0, results2.numbersOfChildEntries());
|
assertEquals("Entry2 was not removed from the search results", 0, results2.numbersOfChildEntries());
|
||||||
@@ -81,10 +74,13 @@ public class DeleteEntry extends AndroidTestCase {
|
|||||||
// Verify the group was deleted
|
// Verify the group was deleted
|
||||||
group1 = getGroup(pm, GROUP1_NAME);
|
group1 = getGroup(pm, GROUP1_NAME);
|
||||||
assertNull("Group 1 was not removed.", group1);
|
assertNull("Group 1 was not removed.", group1);
|
||||||
|
*/
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private PwEntryV3 getEntry(PwDatabaseV3 pm, String name) {
|
private PwEntryV3 getEntry(PwDatabaseV3 pm, String name) {
|
||||||
|
/*
|
||||||
|
TODO test
|
||||||
List<PwEntryV3> entries = pm.getEntries();
|
List<PwEntryV3> entries = pm.getEntries();
|
||||||
for ( int i = 0; i < entries.size(); i++ ) {
|
for ( int i = 0; i < entries.size(); i++ ) {
|
||||||
PwEntryV3 entry = entries.get(i);
|
PwEntryV3 entry = entries.get(i);
|
||||||
@@ -92,22 +88,24 @@ public class DeleteEntry extends AndroidTestCase {
|
|||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private PwGroup getGroup(PwDatabase pm, String name) {
|
private GroupVersioned getGroup(PwDatabase pm, String name) {
|
||||||
List<PwGroup> groups = pm.getGroups();
|
/*
|
||||||
|
List<GroupVersioned> groups = pm.getGroups();
|
||||||
for ( int i = 0; i < groups.size(); i++ ) {
|
for ( int i = 0; i < groups.size(); i++ ) {
|
||||||
PwGroup group = groups.get(i);
|
GroupVersioned group = groups.get(i);
|
||||||
if ( group.getName().equals(name) ) {
|
if ( group.getTitle().equals(name) ) {
|
||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
return null;
|
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePass DX is free software: you can redistribute it and/or modify
|
||||||
@@ -19,14 +19,15 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests.database;
|
package com.kunzisoft.keepass.tests.database;
|
||||||
|
|
||||||
import junit.framework.TestCase;
|
import com.kunzisoft.keepass.database.element.PwDatabaseV4;
|
||||||
|
import com.kunzisoft.keepass.database.element.PwEntryV4;
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.PwDatabaseV4;
|
import junit.framework.TestCase;
|
||||||
import com.kunzisoft.keepass.database.PwEntryV4;
|
|
||||||
|
|
||||||
public class EntryV4 extends TestCase {
|
public class EntryV4 extends TestCase {
|
||||||
|
|
||||||
public void testBackup() {
|
public void testBackup() {
|
||||||
|
/*
|
||||||
PwDatabaseV4 db = new PwDatabaseV4();
|
PwDatabaseV4 db = new PwDatabaseV4();
|
||||||
|
|
||||||
db.setHistoryMaxItems(2);
|
db.setHistoryMaxItems(2);
|
||||||
@@ -46,9 +47,10 @@ public class EntryV4 extends TestCase {
|
|||||||
entry.createBackup(db);
|
entry.createBackup(db);
|
||||||
|
|
||||||
PwEntryV4 backup = entry.getHistory().get(0);
|
PwEntryV4 backup = entry.getHistory().get(0);
|
||||||
entry.endToManageFieldReferences();
|
entry.stopToManageFieldReferences();
|
||||||
assertEquals("Title2", backup.getTitle());
|
assertEquals("Title2", backup.getTitle());
|
||||||
assertEquals("User2", backup.getUsername());
|
assertEquals("User2", backup.getUsername());
|
||||||
}
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePass DX is free software: you can redistribute it and/or modify
|
||||||
@@ -19,22 +19,12 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests.database;
|
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 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 {
|
public class Kdb3 extends AndroidTestCase {
|
||||||
|
|
||||||
private void testKeyfile(String dbAsset, String keyAsset, String password) throws Exception {
|
private void testKeyfile(String dbAsset, String keyAsset, String password) throws Exception {
|
||||||
|
/*
|
||||||
Context ctx = getContext();
|
Context ctx = getContext();
|
||||||
|
|
||||||
File sdcard = Environment.getExternalStorageDirectory();
|
File sdcard = Environment.getExternalStorageDirectory();
|
||||||
@@ -49,14 +39,15 @@ public class Kdb3 extends AndroidTestCase {
|
|||||||
importer.openDatabase(is, password, TestUtil.getKeyFileInputStream(ctx, keyPath));
|
importer.openDatabase(is, password, TestUtil.getKeyFileInputStream(ctx, keyPath));
|
||||||
|
|
||||||
is.close();
|
is.close();
|
||||||
}
|
*/
|
||||||
|
}
|
||||||
public void testXMLKeyFile() throws Exception {
|
|
||||||
testKeyfile("kdb_with_xml_keyfile.kdb", "keyfile.key", "12345");
|
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");
|
public void testBinary64KeyFile() throws Exception {
|
||||||
}
|
testKeyfile("binary-key.kdb", "binary.key", "12345");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePass DX is free software: you can redistribute it and/or modify
|
||||||
@@ -19,18 +19,11 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests.database;
|
package com.kunzisoft.keepass.tests.database;
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.AssetManager;
|
|
||||||
import android.test.AndroidTestCase;
|
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 class Kdb3Twofish extends AndroidTestCase {
|
||||||
public void testReadTwofish() throws Exception {
|
public void testReadTwofish() throws Exception {
|
||||||
|
/*
|
||||||
Context ctx = getContext();
|
Context ctx = getContext();
|
||||||
|
|
||||||
AssetManager am = ctx.getAssets();
|
AssetManager am = ctx.getAssets();
|
||||||
@@ -43,6 +36,6 @@ public class Kdb3Twofish extends AndroidTestCase {
|
|||||||
assertTrue(db.getEncryptionAlgorithm() == PwEncryptionAlgorithm.Twofish);
|
assertTrue(db.getEncryptionAlgorithm() == PwEncryptionAlgorithm.Twofish);
|
||||||
|
|
||||||
is.close();
|
is.close();
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,27 +19,17 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests.database;
|
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.Context;
|
||||||
import android.content.res.AssetManager;
|
import android.content.res.AssetManager;
|
||||||
import android.test.AndroidTestCase;
|
import android.test.AndroidTestCase;
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.PwDatabaseV4;
|
|
||||||
import com.kunzisoft.keepass.database.exception.InvalidDBException;
|
import com.kunzisoft.keepass.database.exception.InvalidDBException;
|
||||||
import com.kunzisoft.keepass.database.exception.PwDbOutputException;
|
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;
|
import com.kunzisoft.keepass.tests.TestUtil;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
public class Kdb4 extends AndroidTestCase {
|
public class Kdb4 extends AndroidTestCase {
|
||||||
|
|
||||||
public void testDetection() throws IOException, InvalidDBException {
|
public void testDetection() throws IOException, InvalidDBException {
|
||||||
@@ -48,11 +38,13 @@ public class Kdb4 extends AndroidTestCase {
|
|||||||
AssetManager am = ctx.getAssets();
|
AssetManager am = ctx.getAssets();
|
||||||
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
|
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO Test
|
||||||
Importer importer = ImporterFactory.createImporter(is);
|
Importer importer = ImporterFactory.createImporter(is);
|
||||||
|
|
||||||
assertTrue(importer instanceof ImporterV4);
|
assertTrue(importer instanceof ImporterV4);
|
||||||
is.close();
|
is.close();
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testParsing() throws IOException, InvalidDBException {
|
public void testParsing() throws IOException, InvalidDBException {
|
||||||
@@ -61,12 +53,13 @@ public class Kdb4 extends AndroidTestCase {
|
|||||||
AssetManager am = ctx.getAssets();
|
AssetManager am = ctx.getAssets();
|
||||||
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
|
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO Test
|
||||||
ImporterV4 importer = new ImporterV4();
|
ImporterV4 importer = new ImporterV4();
|
||||||
importer.openDatabase(is, "12345", null);
|
importer.openDatabase(is, "12345", null);
|
||||||
|
|
||||||
is.close();
|
is.close();
|
||||||
|
*/
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testSavingKDBXV3() throws IOException, InvalidDBException, PwDbOutputException {
|
public void testSavingKDBXV3() throws IOException, InvalidDBException, PwDbOutputException {
|
||||||
@@ -83,6 +76,8 @@ public class Kdb4 extends AndroidTestCase {
|
|||||||
AssetManager am = ctx.getAssets();
|
AssetManager am = ctx.getAssets();
|
||||||
InputStream is = am.open(inputFile, AssetManager.ACCESS_STREAMING);
|
InputStream is = am.open(inputFile, AssetManager.ACCESS_STREAMING);
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO Test
|
||||||
ImporterV4 importer = new ImporterV4();
|
ImporterV4 importer = new ImporterV4();
|
||||||
PwDatabaseV4 db = importer.openDatabase(is, password, null);
|
PwDatabaseV4 db = importer.openDatabase(is, password, null);
|
||||||
is.close();
|
is.close();
|
||||||
@@ -103,7 +98,7 @@ public class Kdb4 extends AndroidTestCase {
|
|||||||
bis.close();
|
bis.close();
|
||||||
|
|
||||||
fos.close();
|
fos.close();
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -120,10 +115,13 @@ public class Kdb4 extends AndroidTestCase {
|
|||||||
AssetManager am = ctx.getAssets();
|
AssetManager am = ctx.getAssets();
|
||||||
InputStream is = am.open("keyfile.kdbx", AssetManager.ACCESS_STREAMING);
|
InputStream is = am.open("keyfile.kdbx", AssetManager.ACCESS_STREAMING);
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO Test
|
||||||
ImporterV4 importer = new ImporterV4();
|
ImporterV4 importer = new ImporterV4();
|
||||||
importer.openDatabase(is, "12345", TestUtil.getKeyFileInputStream(ctx, TestUtil.getSdPath("key")));
|
importer.openDatabase(is, "12345", TestUtil.getKeyFileInputStream(ctx, TestUtil.getSdPath("key")));
|
||||||
|
|
||||||
is.close();
|
is.close();
|
||||||
|
*/
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,11 +131,13 @@ public class Kdb4 extends AndroidTestCase {
|
|||||||
AssetManager am = ctx.getAssets();
|
AssetManager am = ctx.getAssets();
|
||||||
InputStream is = am.open("keyfile-binary.kdbx", AssetManager.ACCESS_STREAMING);
|
InputStream is = am.open("keyfile-binary.kdbx", AssetManager.ACCESS_STREAMING);
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO Test
|
||||||
ImporterV4 importer = new ImporterV4();
|
ImporterV4 importer = new ImporterV4();
|
||||||
importer.openDatabase(is, "12345", TestUtil.getKeyFileInputStream(ctx,TestUtil.getSdPath("key-binary")));
|
importer.openDatabase(is, "12345", TestUtil.getKeyFileInputStream(ctx,TestUtil.getSdPath("key-binary")));
|
||||||
|
|
||||||
is.close();
|
is.close();
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testKeyfile() throws IOException, InvalidDBException {
|
public void testKeyfile() throws IOException, InvalidDBException {
|
||||||
@@ -145,13 +145,13 @@ public class Kdb4 extends AndroidTestCase {
|
|||||||
|
|
||||||
AssetManager am = ctx.getAssets();
|
AssetManager am = ctx.getAssets();
|
||||||
InputStream is = am.open("key-only.kdbx", AssetManager.ACCESS_STREAMING);
|
InputStream is = am.open("key-only.kdbx", AssetManager.ACCESS_STREAMING);
|
||||||
|
/*
|
||||||
|
TODO Test
|
||||||
ImporterV4 importer = new ImporterV4();
|
ImporterV4 importer = new ImporterV4();
|
||||||
importer.openDatabase(is, "", TestUtil.getKeyFileInputStream(ctx, TestUtil.getSdPath("key")));
|
importer.openDatabase(is, "", TestUtil.getKeyFileInputStream(ctx, TestUtil.getSdPath("key")));
|
||||||
|
|
||||||
is.close();
|
is.close();
|
||||||
|
*/
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testNoGzip() throws IOException, InvalidDBException {
|
public void testNoGzip() throws IOException, InvalidDBException {
|
||||||
@@ -159,13 +159,13 @@ public class Kdb4 extends AndroidTestCase {
|
|||||||
|
|
||||||
AssetManager am = ctx.getAssets();
|
AssetManager am = ctx.getAssets();
|
||||||
InputStream is = am.open("no-encrypt.kdbx", AssetManager.ACCESS_STREAMING);
|
InputStream is = am.open("no-encrypt.kdbx", AssetManager.ACCESS_STREAMING);
|
||||||
|
/*
|
||||||
|
TODO Test
|
||||||
ImporterV4 importer = new ImporterV4();
|
ImporterV4 importer = new ImporterV4();
|
||||||
importer.openDatabase(is, "12345", null);
|
importer.openDatabase(is, "12345", null);
|
||||||
|
|
||||||
is.close();
|
is.close();
|
||||||
|
*/
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePass DX is free software: you can redistribute it and/or modify
|
||||||
@@ -23,19 +23,17 @@ import android.content.Context;
|
|||||||
import android.content.res.AssetManager;
|
import android.content.res.AssetManager;
|
||||||
import android.test.AndroidTestCase;
|
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;
|
import java.io.InputStream;
|
||||||
|
|
||||||
public class Kdb4Header extends AndroidTestCase {
|
public class Kdb4Header extends AndroidTestCase {
|
||||||
public void testReadHeader() throws Exception {
|
public void testReadHeader() throws Exception {
|
||||||
Context ctx = getContext();
|
Context ctx = getContext();
|
||||||
|
|
||||||
AssetManager am = ctx.getAssets();
|
AssetManager am = ctx.getAssets();
|
||||||
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
|
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO Test
|
||||||
ImporterV4 importer = new ImporterV4();
|
ImporterV4 importer = new ImporterV4();
|
||||||
|
|
||||||
PwDatabaseV4 db = importer.openDatabase(is, "12345", null);
|
PwDatabaseV4 db = importer.openDatabase(is, "12345", null);
|
||||||
@@ -45,6 +43,7 @@ public class Kdb4Header extends AndroidTestCase {
|
|||||||
assertTrue(db.getDataCipher().equals(AesEngine.CIPHER_UUID));
|
assertTrue(db.getDataCipher().equals(AesEngine.CIPHER_UUID));
|
||||||
|
|
||||||
is.close();
|
is.close();
|
||||||
|
*/
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePass DX is free software: you can redistribute it and/or modify
|
||||||
@@ -19,62 +19,52 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests.database;
|
package com.kunzisoft.keepass.tests.database;
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.AssetManager;
|
import android.content.res.AssetManager;
|
||||||
import android.test.AndroidTestCase;
|
import android.test.AndroidTestCase;
|
||||||
import biz.source_code.base64Coder.Base64Coder;
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.PwDatabase;
|
import com.kunzisoft.keepass.database.element.PwDatabaseV4;
|
||||||
import com.kunzisoft.keepass.database.PwDatabaseV4;
|
import com.kunzisoft.keepass.database.element.SprEngineV4;
|
||||||
import com.kunzisoft.keepass.database.PwEntryV4;
|
|
||||||
import com.kunzisoft.keepass.database.load.ImporterV4;
|
import java.io.InputStream;
|
||||||
import com.kunzisoft.keepass.utils.SprEngineV4;
|
|
||||||
import com.kunzisoft.keepass.utils.Types;
|
|
||||||
|
|
||||||
public class SprEngineTest extends AndroidTestCase {
|
public class SprEngineTest extends AndroidTestCase {
|
||||||
private PwDatabaseV4 db;
|
private PwDatabaseV4 db;
|
||||||
private SprEngineV4 spr;
|
private SprEngineV4 spr;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setUp() throws Exception {
|
protected void setUp() throws Exception {
|
||||||
super.setUp();
|
super.setUp();
|
||||||
|
|
||||||
Context ctx = getContext();
|
Context ctx = getContext();
|
||||||
|
|
||||||
AssetManager am = ctx.getAssets();
|
AssetManager am = ctx.getAssets();
|
||||||
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
|
InputStream is = am.open("test.kdbx", AssetManager.ACCESS_STREAMING);
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO Test
|
||||||
ImporterV4 importer = new ImporterV4();
|
ImporterV4 importer = new ImporterV4();
|
||||||
db = importer.openDatabase(is, "12345", null);
|
db = importer.openDatabase(is, "12345", null);
|
||||||
|
|
||||||
is.close();
|
is.close();
|
||||||
|
|
||||||
spr = new SprEngineV4();
|
spr = new SprEngineV4();
|
||||||
}
|
*/
|
||||||
|
}
|
||||||
private final String REF = "{REF:P@I:2B1D56590D961F48A8CE8C392CE6CD35}";
|
|
||||||
private final String ENCODE_UUID = "IN7RkON49Ui1UZ2ddqmLcw==";
|
private final String REF = "{REF:P@I:2B1D56590D961F48A8CE8C392CE6CD35}";
|
||||||
private final String RESULT = "Password";
|
private final String ENCODE_UUID = "IN7RkON49Ui1UZ2ddqmLcw==";
|
||||||
public void testRefReplace() {
|
private final String RESULT = "Password";
|
||||||
|
public void testRefReplace() {
|
||||||
|
/*
|
||||||
|
TODO TEST
|
||||||
UUID entryUUID = decodeUUID(ENCODE_UUID);
|
UUID entryUUID = decodeUUID(ENCODE_UUID);
|
||||||
|
|
||||||
PwEntryV4 entry = (PwEntryV4) db.getEntryByUUIDId(entryUUID);
|
PwEntryV4 entry = (PwEntryV4) db.getEntryById(entryUUID);
|
||||||
|
|
||||||
|
|
||||||
assertEquals(RESULT, spr.compile(REF, entry, db));
|
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,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePass DX is free software: you can redistribute it and/or modify
|
||||||
@@ -19,24 +19,16 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests.database;
|
package com.kunzisoft.keepass.tests.database;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import com.kunzisoft.keepass.database.element.Database;
|
||||||
|
|
||||||
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 {
|
public class TestData {
|
||||||
private static final String TEST1_KEYFILE = "";
|
private static final String TEST1_KEYFILE = "";
|
||||||
private static final String TEST1_KDB = "test1.kdb";
|
private static final String TEST1_KDB = "test1.kdb";
|
||||||
private static final String TEST1_PASSWORD = "12345";
|
private static final String TEST1_PASSWORD = "12345";
|
||||||
|
|
||||||
private static Database mDb1;
|
private static Database mDb1;
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
public static Database GetDb1(Context ctx) throws Exception {
|
public static Database GetDb1(Context ctx) throws Exception {
|
||||||
return GetDb1(ctx, false);
|
return GetDb1(ctx, false);
|
||||||
@@ -72,6 +64,8 @@ public class TestData {
|
|||||||
GetDb1(ctx);
|
GetDb1(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (PwDatabaseV3Debug) mDb1.getPwDatabase();
|
//return (PwDatabaseV3Debug) mDb1.getPwDatabase();
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,33 +19,12 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests.output;
|
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 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 {
|
public class PwManagerOutputTest extends AndroidTestCase {
|
||||||
PwDatabaseV3Debug mPM;
|
// PwDatabaseV3Debug mPM;
|
||||||
|
|
||||||
|
/*
|
||||||
@Override
|
@Override
|
||||||
protected void setUp() throws Exception {
|
protected void setUp() throws Exception {
|
||||||
super.setUp();
|
super.setUp();
|
||||||
@@ -143,4 +122,5 @@ public class PwManagerOutputTest extends AndroidTestCase {
|
|||||||
assertArrayEquals("Databases do not match.", bExpected.toByteArray(), bActual.toByteArray());
|
assertArrayEquals("Databases do not match.", bExpected.toByteArray(), bActual.toByteArray());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* KeePass DX is distributed in the hope that it will be useful,
|
* KeePass DX is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* GNU General Public License for more details.
|
* GNU General Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests.search;
|
package com.kunzisoft.keepass.tests.search;
|
||||||
|
|
||||||
|
|
||||||
@@ -24,49 +24,47 @@ import android.content.Context;
|
|||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.test.AndroidTestCase;
|
import android.test.AndroidTestCase;
|
||||||
|
import com.kunzisoft.keepass.database.element.Database;
|
||||||
import com.kunzisoft.keepass.database.Database;
|
import com.kunzisoft.keepass.database.element.GroupVersioned;
|
||||||
import com.kunzisoft.keepass.database.PwGroup;
|
|
||||||
import com.kunzisoft.keepass.tests.database.TestData;
|
|
||||||
|
|
||||||
public class SearchTest extends AndroidTestCase {
|
public class SearchTest extends AndroidTestCase {
|
||||||
|
|
||||||
private Database mDb;
|
private Database mDb;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setUp() throws Exception {
|
protected void setUp() throws Exception {
|
||||||
super.setUp();
|
super.setUp();
|
||||||
|
|
||||||
mDb = TestData.GetDb1(getContext(), true);
|
//mDb = TestData.GetDb1(getContext(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testSearch() {
|
public void testSearch() {
|
||||||
PwGroup results = mDb.search("Amazon");
|
GroupVersioned results = mDb.search("Amazon");
|
||||||
assertTrue("Search result not found.", results.numbersOfChildEntries() > 0);
|
//assertTrue("Search result not found.", results.numbersOfChildEntries() > 0);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testBackupIncluded() {
|
public void testBackupIncluded() {
|
||||||
updateOmitSetting(false);
|
updateOmitSetting(false);
|
||||||
PwGroup results = mDb.search("BackupOnly");
|
GroupVersioned results = mDb.search("BackupOnly");
|
||||||
|
|
||||||
assertTrue("Search result not found.", results.numbersOfChildEntries() > 0);
|
//assertTrue("Search result not found.", results.numbersOfChildEntries() > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testBackupExcluded() {
|
public void testBackupExcluded() {
|
||||||
updateOmitSetting(true);
|
updateOmitSetting(true);
|
||||||
PwGroup results = mDb.search("BackupOnly");
|
GroupVersioned results = mDb.search("BackupOnly");
|
||||||
|
|
||||||
assertFalse("Search result found, but should not have been.", results.numbersOfChildEntries() > 0);
|
//assertFalse("Search result found, but should not have been.", results.numbersOfChildEntries() > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateOmitSetting(boolean setting) {
|
private void updateOmitSetting(boolean setting) {
|
||||||
Context ctx = getContext();
|
Context ctx = getContext();
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
|
||||||
SharedPreferences.Editor editor = prefs.edit();
|
SharedPreferences.Editor editor = prefs.edit();
|
||||||
|
|
||||||
editor.putBoolean("settings_omitbackup_key", setting);
|
editor.putBoolean("settings_omitbackup_key", setting);
|
||||||
editor.commit();
|
editor.commit();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* (at your option) any later version.
|
* (at your option) any later version.
|
||||||
*
|
*
|
||||||
* KeePass DX is distributed in the hope that it will be useful,
|
* KeePass DX is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* GNU General Public License for more details.
|
* GNU General Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests.stream;
|
package com.kunzisoft.keepass.tests.stream;
|
||||||
|
|
||||||
import static org.junit.Assert.assertArrayEquals;
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
@@ -34,77 +34,77 @@ import com.kunzisoft.keepass.stream.HashedBlockInputStream;
|
|||||||
import com.kunzisoft.keepass.stream.HashedBlockOutputStream;
|
import com.kunzisoft.keepass.stream.HashedBlockOutputStream;
|
||||||
|
|
||||||
public class HashedBlock extends TestCase {
|
public class HashedBlock extends TestCase {
|
||||||
|
|
||||||
private static Random rand = new Random();
|
|
||||||
|
|
||||||
public void testBlockAligned() throws IOException {
|
private static Random rand = new Random();
|
||||||
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();
|
public void testBlockAligned() throws IOException {
|
||||||
while ( true ) {
|
testSize(1024, 1024);
|
||||||
byte[] buf = new byte[1024];
|
}
|
||||||
int read = input.read(buf);
|
|
||||||
if ( read == -1 ) {
|
public void testOffset() throws IOException {
|
||||||
break;
|
testSize(1500, 1024);
|
||||||
}
|
}
|
||||||
|
|
||||||
decoded.write(buf, 0, read);
|
private void testSize(int blockSize, int bufferSize) throws IOException {
|
||||||
}
|
byte[] orig = new byte[blockSize];
|
||||||
|
|
||||||
byte[] out = decoded.toByteArray();
|
rand.nextBytes(orig);
|
||||||
|
|
||||||
assertArrayEquals(orig, out);
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
HashedBlockOutputStream output = new HashedBlockOutputStream(bos, bufferSize);
|
||||||
}
|
output.write(orig);
|
||||||
|
output.close();
|
||||||
public void testGZIPStream() throws IOException {
|
|
||||||
final int testLength = 32000;
|
byte[] encoded = bos.toByteArray();
|
||||||
|
|
||||||
byte[] orig = new byte[testLength];
|
ByteArrayInputStream bis = new ByteArrayInputStream(encoded);
|
||||||
rand.nextBytes(orig);
|
HashedBlockInputStream input = new HashedBlockInputStream(bis);
|
||||||
|
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
ByteArrayOutputStream decoded = new ByteArrayOutputStream();
|
||||||
HashedBlockOutputStream hos = new HashedBlockOutputStream(bos);
|
while ( true ) {
|
||||||
GZIPOutputStream zos = new GZIPOutputStream(hos);
|
byte[] buf = new byte[1024];
|
||||||
|
int read = input.read(buf);
|
||||||
zos.write(orig);
|
if ( read == -1 ) {
|
||||||
zos.close();
|
break;
|
||||||
|
}
|
||||||
byte[] compressed = bos.toByteArray();
|
|
||||||
ByteArrayInputStream bis = new ByteArrayInputStream(compressed);
|
decoded.write(buf, 0, read);
|
||||||
HashedBlockInputStream his = new HashedBlockInputStream(bis);
|
}
|
||||||
GZIPInputStream zis = new GZIPInputStream(his);
|
|
||||||
|
byte[] out = decoded.toByteArray();
|
||||||
byte[] uncompressed = new byte[testLength];
|
|
||||||
|
assertArrayEquals(orig, out);
|
||||||
int read = 0;
|
|
||||||
while (read != -1 && testLength - read > 0) {
|
}
|
||||||
read += zis.read(uncompressed, read, testLength - read);
|
|
||||||
|
public void testGZIPStream() throws IOException {
|
||||||
}
|
final int testLength = 32000;
|
||||||
|
|
||||||
assertArrayEquals("Output not equal to input", orig, uncompressed);
|
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);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,57 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
public class StringUtilTest extends TestCase {
|
||||||
|
private final String text = "AbCdEfGhIj";
|
||||||
|
private final String search = "BcDe";
|
||||||
|
private final String badSearch = "Ed";
|
||||||
|
|
||||||
|
public void testIndexOfIgnoreCase1() {
|
||||||
|
assertEquals(1, StringUtil.INSTANCE.indexOfIgnoreCase(text, search, Locale.ENGLISH));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testIndexOfIgnoreCase2() {
|
||||||
|
assertEquals(-1, StringUtil.INSTANCE.indexOfIgnoreCase(text, search, Locale.ENGLISH), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testIndexOfIgnoreCase3() {
|
||||||
|
assertEquals(-1, StringUtil.INSTANCE.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, StringUtil.INSTANCE.replaceAllIgnoresCase(repText, repSearch, repNew, Locale.ENGLISH));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testReplaceAllIgnoresCase2() {
|
||||||
|
assertEquals(repText, StringUtil.INSTANCE.replaceAllIgnoresCase(repText, repSearchBad, repNew, Locale.ENGLISH));
|
||||||
|
}
|
||||||
|
}
|
||||||
50
app/src/free/res/drawable-v24/ic_launcher_foreground.xml
Normal file
50
app/src/free/res/drawable-v24/ic_launcher_foreground.xml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp">
|
||||||
|
<group
|
||||||
|
android:translateY="-332">
|
||||||
|
<group
|
||||||
|
android:translateY="332">
|
||||||
|
<path
|
||||||
|
android:pathData="M65.728516 32.791016L58.052734 35.904297 56.173828 48.380859 35.306641 69.267578 35.238281 73.759766 69.478516 108 108 108 108 70.810547 73.09375 35.904297 65.728516 32.791016Z"
|
||||||
|
android:fillColor="@color/long_shadow"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeMiterLimit="4" />
|
||||||
|
</group>
|
||||||
|
<group
|
||||||
|
android:scaleX="0.3939503"
|
||||||
|
android:scaleY="0.3939503"
|
||||||
|
android:translateX="33.66343"
|
||||||
|
android:translateY="233.998">
|
||||||
|
<path
|
||||||
|
android:pathData="M88.76953 339.91602L4.1718754 424.59766 4.0000004 436 15.400391 435.82813 27.240234 424 40 424l0 -12 12 0 0 -12.73438 34.01172 -33.97656A8 8 0 0 1 84 360a8 8 0 0 1 8 -8 8 8 0 0 1 5.296882 2.01367l2.787098 -2.7832 -11.31445 -11.31445z"
|
||||||
|
android:fillColor="#eaeaea"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:strokeColor="#58000000" />
|
||||||
|
</group>
|
||||||
|
<group
|
||||||
|
android:scaleX="0.3939503"
|
||||||
|
android:scaleY="0.3939503"
|
||||||
|
android:translateX="33.66343"
|
||||||
|
android:translateY="233.998">
|
||||||
|
<path
|
||||||
|
android:pathData="M4.0000004 340L4.1718754 351.40137 88.59863 435.82812 100 436 99.828122 424.59863 15.401367 340.17188Z"
|
||||||
|
android:fillColor="#81c784" />
|
||||||
|
</group>
|
||||||
|
<group
|
||||||
|
android:scaleX="0.3939503"
|
||||||
|
android:scaleY="0.3939503"
|
||||||
|
android:translateX="33.66343"
|
||||||
|
android:translateY="233.998">
|
||||||
|
<path
|
||||||
|
android:pathData="M81.39454 332.00195a27 27 0 0 0 -19.48634 7.90625 27 27 0 0 0 0 38.1836 27 27 0 0 0 38.1836 0 27 27 0 0 0 0 -38.1836 27 27 0 0 0 -18.69726 -7.90625zM92 352a8 8 0 0 1 8 8 8 8 0 0 1 -8 8 8 8 0 0 1 -8 -8 8 8 0 0 1 8 -8z"
|
||||||
|
android:fillColor="#eaeaea"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:strokeColor="#58000000" />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
5
app/src/free/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
5
app/src/free/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/green" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
5
app/src/free/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
5
app/src/free/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/green" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
50
app/src/libre/res/drawable-v24/ic_launcher_foreground.xml
Normal file
50
app/src/libre/res/drawable-v24/ic_launcher_foreground.xml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp">
|
||||||
|
<group
|
||||||
|
android:translateY="-332">
|
||||||
|
<group
|
||||||
|
android:translateY="332">
|
||||||
|
<path
|
||||||
|
android:pathData="M65.728516 32.791016L58.052734 35.904297 56.173828 48.380859 35.306641 69.267578 35.238281 73.759766 69.478516 108 108 108 108 70.810547 73.09375 35.904297 65.728516 32.791016Z"
|
||||||
|
android:fillColor="@color/long_shadow"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeMiterLimit="4" />
|
||||||
|
</group>
|
||||||
|
<group
|
||||||
|
android:scaleX="0.3939503"
|
||||||
|
android:scaleY="0.3939503"
|
||||||
|
android:translateX="33.66343"
|
||||||
|
android:translateY="233.998">
|
||||||
|
<path
|
||||||
|
android:pathData="M88.76953 339.91602L4.1718754 424.59766 4.0000004 436 15.400391 435.82813 27.240234 424 40 424l0 -12 12 0 0 -12.73438 34.01172 -33.97656A8 8 0 0 1 84 360a8 8 0 0 1 8 -8 8 8 0 0 1 5.296882 2.01367l2.787098 -2.7832 -11.31445 -11.31445z"
|
||||||
|
android:fillColor="#eaeaea"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:strokeColor="#58000000"/>
|
||||||
|
</group>
|
||||||
|
<group
|
||||||
|
android:scaleX="0.3939503"
|
||||||
|
android:scaleY="0.3939503"
|
||||||
|
android:translateX="33.66343"
|
||||||
|
android:translateY="233.998">
|
||||||
|
<path
|
||||||
|
android:pathData="M4.0000004 340L4.1718754 351.40137 88.59863 435.82812 100 436 99.828122 424.59863 15.401367 340.17188Z"
|
||||||
|
android:fillColor="#64b5f6" />
|
||||||
|
</group>
|
||||||
|
<group
|
||||||
|
android:scaleX="0.3939503"
|
||||||
|
android:scaleY="0.3939503"
|
||||||
|
android:translateX="33.66343"
|
||||||
|
android:translateY="233.998">
|
||||||
|
<path
|
||||||
|
android:pathData="M81.39454 332.00195a27 27 0 0 0 -19.48634 7.90625 27 27 0 0 0 0 38.1836 27 27 0 0 0 38.1836 0 27 27 0 0 0 0 -38.1836 27 27 0 0 0 -18.69726 -7.90625zM92 352a8 8 0 0 1 8 8 8 8 0 0 1 -8 8 8 8 0 0 1 -8 -8 8 8 0 0 1 8 -8z"
|
||||||
|
android:fillColor="#eaeaea"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:strokeColor="#58000000" />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
5
app/src/libre/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
5
app/src/libre/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/green" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/green" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
@@ -20,19 +20,18 @@
|
|||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:fullBackupContent="@xml/backup"
|
android:fullBackupContent="@xml/backup"
|
||||||
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
|
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
|
||||||
android:theme="@style/KeepassDXStyle.Night"
|
android:theme="@style/KeepassDXStyle.Night">
|
||||||
tools:replace="android:theme">
|
|
||||||
<!-- TODO backup API Key -->
|
<!-- TODO backup API Key -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.backup.api_key"
|
android:name="com.google.android.backup.api_key"
|
||||||
android:value="" />
|
android:value="" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.fileselect.FileSelectActivity"
|
android:name="com.kunzisoft.keepass.activities.FileDatabaseSelectActivity"
|
||||||
android:theme="@style/KeepassDXStyle.SplashScreen"
|
android:theme="@style/KeepassDXStyle.SplashScreen"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTop"
|
||||||
android:configChanges="orientation|keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:windowSoftInputMode="stateHidden" >
|
android:windowSoftInputMode="stateHidden" >
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
@@ -40,12 +39,8 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
android:name="com.kunzisoft.keepass.activities.PasswordActivity"
|
||||||
android:launchMode="singleTask"
|
android:configChanges="keyboardHidden"
|
||||||
android:label="@string/menu_about" />
|
|
||||||
<activity
|
|
||||||
android:name="com.kunzisoft.keepass.password.PasswordActivity"
|
|
||||||
android:configChanges="orientation|keyboardHidden"
|
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
@@ -94,51 +89,67 @@
|
|||||||
android:resource="@xml/nnf_provider_paths" />
|
android:resource="@xml/nnf_provider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.fileselect.FilePickerStylishActivity"
|
android:name=".activities.stylish.FilePickerStylishActivity"
|
||||||
android:label="@string/app_name">
|
android:label="@string/app_name">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.GET_CONTENT" />
|
<action android:name="android.intent.action.GET_CONTENT" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<!-- Main Activity -->
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.GroupActivity"
|
android:name="com.kunzisoft.keepass.activities.GroupActivity"
|
||||||
android:configChanges="orientation|keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:windowSoftInputMode="adjustPan">
|
android:windowSoftInputMode="adjustPan"
|
||||||
|
android:launchMode="singleTask">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.default_searchable"
|
android:name="android.app.default_searchable"
|
||||||
android:value="com.kunzisoft.keepass.search.SearchResults"
|
android:value="com.kunzisoft.keepass.search.SearchResults"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name="com.kunzisoft.keepass.activities.EntryActivity"
|
|
||||||
android:configChanges="orientation|keyboardHidden"
|
|
||||||
android:windowSoftInputMode="stateHidden" />
|
|
||||||
<activity
|
|
||||||
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
|
|
||||||
android:configChanges="orientation|keyboardHidden"
|
|
||||||
android:windowSoftInputMode="stateHidden" />
|
|
||||||
<activity
|
|
||||||
android:name="com.kunzisoft.keepass.search.SearchResultsActivity"
|
|
||||||
android:launchMode="standard">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEARCH" />
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.searchable"
|
android:name="android.app.searchable"
|
||||||
android:resource="@xml/searchable" />
|
android:resource="@xml/searchable" />
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.EntryActivity"
|
||||||
|
android:configChanges="keyboardHidden" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
|
||||||
|
android:configChanges="keyboardHidden"
|
||||||
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
<!-- About and Settings -->
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:label="@string/menu_about" />
|
||||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
<activity android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
||||||
<activity android:name="com.kunzisoft.keepass.autofill.AutoFillAuthActivity"
|
<activity android:name="com.kunzisoft.keepass.autofill.AutofillLauncherActivity"
|
||||||
android:configChanges="orientation|keyboardHidden" />
|
android:configChanges="keyboardHidden" />
|
||||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsAutofillActivity" />
|
<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.timeout.TimeoutService" />
|
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.NotificationCopyingService"
|
android:name="com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
<service
|
||||||
|
android:name="com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false" />
|
||||||
|
<!-- Receiver for Autofill -->
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
|
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
|
||||||
android:label="@string/autofill_service_name"
|
android:label="@string/autofill_service_name"
|
||||||
@@ -146,11 +157,25 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.autofill"
|
android:name="android.autofill"
|
||||||
android:resource="@xml/dataset_service" />
|
android:resource="@xml/dataset_service" />
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.autofill.AutofillService" />
|
<action android:name="android.service.autofill.AutofillService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</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" />
|
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
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.
@@ -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 android.support.v7.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,472 +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.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.icons.IconPackChooser;
|
|
||||||
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;
|
|
||||||
|
|
||||||
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;
|
|
||||||
protected boolean readOnly = false;
|
|
||||||
|
|
||||||
private ClipboardHelper clipboardHelper;
|
|
||||||
private boolean firstLaunchOfActivity;
|
|
||||||
|
|
||||||
public static void launch(Activity act, PwEntry pw) {
|
|
||||||
if (LockingActivity.checkTimeIsAllowedOrFinish(act)) {
|
|
||||||
Intent intent = new Intent(act, EntryActivity.class);
|
|
||||||
intent.putExtra(KEY_ENTRY, Types.UUIDtoBytes(pw.getUUID()));
|
|
||||||
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();
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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());
|
|
||||||
|
|
||||||
// If notifications enabled in settings
|
|
||||||
// Don't if application timeout
|
|
||||||
if (firstLaunchOfActivity && !App.isShutdown() && isClipboardNotificationsEnable(getApplicationContext())) {
|
|
||||||
if (mEntry.getUsername().length() > 0
|
|
||||||
|| (mEntry.getPassword().length() > 0 && PreferencesUtil.allowCopyPassword(this))
|
|
||||||
|| mEntry.containsCustomFields()) {
|
|
||||||
// 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 (mEntry.getUsername().length() > 0)
|
|
||||||
notificationFields.add(
|
|
||||||
new NotificationField(
|
|
||||||
NotificationField.NotificationFieldId.USERNAME,
|
|
||||||
mEntry.getUsername(),
|
|
||||||
getResources()));
|
|
||||||
// Add password to notifications
|
|
||||||
if (PreferencesUtil.allowCopyPassword(this)) {
|
|
||||||
if (mEntry.getPassword().length() > 0)
|
|
||||||
notificationFields.add(
|
|
||||||
new NotificationField(
|
|
||||||
NotificationField.NotificationFieldId.PASSWORD,
|
|
||||||
mEntry.getPassword(),
|
|
||||||
getResources()));
|
|
||||||
}
|
|
||||||
// Add extra fields
|
|
||||||
if (mEntry.allowExtraFields()) {
|
|
||||||
try {
|
|
||||||
mEntry.getFields().doActionToAllCustomProtectedField(new ExtraFields.ActionProtected() {
|
|
||||||
private int anonymousFieldNumber = 0;
|
|
||||||
@Override
|
|
||||||
public void doAction(String key, ProtectedString value) {
|
|
||||||
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.endToManageFieldReferences();
|
|
||||||
}
|
|
||||||
firstLaunchOfActivity = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check and display learning views
|
|
||||||
* Displays the explanation for copying a field and editing an entry
|
|
||||||
*/
|
|
||||||
private void checkAndPerformedEducation(Menu menu) {
|
|
||||||
|
|
||||||
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
|
|
||||||
if (IconPackChooser.getSelectedIconPack(this).tintable()) {
|
|
||||||
// Retrieve the textColor to tint the icon
|
|
||||||
int[] attrs = {R.attr.textColorInverse};
|
|
||||||
TypedArray ta = getTheme().obtainStyledAttributes(attrs);
|
|
||||||
int iconColor = ta.getColor(0, Color.WHITE);
|
|
||||||
App.getDB().getDrawFactory().assignDatabaseIconTo(this, titleIconView, mEntry.getIcon(), true, iconColor);
|
|
||||||
} else {
|
|
||||||
App.getDB().getDrawFactory().assignDatabaseIconTo(this, titleIconView, mEntry.getIcon());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign title text
|
|
||||||
titleView.setText(mEntry.getTitle());
|
|
||||||
|
|
||||||
// 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)))
|
|
||||||
);
|
|
||||||
|
|
||||||
entryContentsView.assignPassword(mEntry.getPassword());
|
|
||||||
if (PreferencesUtil.allowCopyPassword(this)) {
|
|
||||||
entryContentsView.assignPasswordCopyListener(view ->
|
|
||||||
clipboardHelper.timeoutCopyToClipboard(mEntry.getPassword(),
|
|
||||||
getString(R.string.copy_field, getString(R.string.entry_password)))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
entryContentsView.assignURL(mEntry.getUrl());
|
|
||||||
|
|
||||||
entryContentsView.setHiddenPasswordStyle(!mShowPassword);
|
|
||||||
entryContentsView.assignComment(mEntry.getNotes());
|
|
||||||
|
|
||||||
// Assign custom fields
|
|
||||||
if (mEntry.allowExtraFields()) {
|
|
||||||
entryContentsView.clearExtraFields();
|
|
||||||
|
|
||||||
mEntry.getFields().doActionToAllCustomProtectedField((label, value) ->
|
|
||||||
|
|
||||||
entryContentsView.addExtraField(label, value, 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.endToManageFieldReferences();
|
|
||||||
}
|
|
||||||
|
|
||||||
@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,406 @@
|
|||||||
|
/*
|
||||||
|
* 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.ActivityNotFoundException
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.support.design.widget.CollapsingToolbarLayout
|
||||||
|
import android.support.v7.app.AlertDialog
|
||||||
|
import android.support.v7.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.PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields
|
||||||
|
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.Util
|
||||||
|
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()
|
||||||
|
readOnly = currentDatabase.isReadOnly || readOnly
|
||||||
|
|
||||||
|
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.textColorInverse))
|
||||||
|
iconColor = taIconColor.getColor(0, Color.WHITE)
|
||||||
|
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 {
|
||||||
|
clipboardHelper?.timeoutCopyToClipboard(entry.username,
|
||||||
|
getString(R.string.copy_field,
|
||||||
|
getString(R.string.entry_user_name)))
|
||||||
|
})
|
||||||
|
|
||||||
|
val allowCopyPassword = PreferencesUtil.allowCopyPasswordAndProtectedFields(this)
|
||||||
|
entryContentsView?.assignPassword(entry.password, allowCopyPassword)
|
||||||
|
if (allowCopyPassword) {
|
||||||
|
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
|
||||||
|
clipboardHelper?.timeoutCopyToClipboard(entry.password,
|
||||||
|
getString(R.string.copy_field,
|
||||||
|
getString(R.string.entry_password)))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// If dialog not already shown
|
||||||
|
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)) {
|
||||||
|
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
|
||||||
|
val message = getString(R.string.allow_copy_password_warning) +
|
||||||
|
"\n\n" +
|
||||||
|
getString(R.string.clipboard_warning)
|
||||||
|
val warningDialog = AlertDialog.Builder(this@EntryActivity)
|
||||||
|
.setMessage(message).create()
|
||||||
|
warningDialog.setButton(AlertDialog.BUTTON_POSITIVE, getText(android.R.string.ok)
|
||||||
|
) { dialog, _ ->
|
||||||
|
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true)
|
||||||
|
dialog.dismiss()
|
||||||
|
fillEntryDataInContentsView(entry)
|
||||||
|
}
|
||||||
|
warningDialog.setButton(AlertDialog.BUTTON_NEGATIVE, getText(android.R.string.cancel)
|
||||||
|
) { dialog, _ ->
|
||||||
|
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, false)
|
||||||
|
dialog.dismiss()
|
||||||
|
fillEntryDataInContentsView(entry)
|
||||||
|
}
|
||||||
|
warningDialog.show()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
entryContentsView?.assignPasswordCopyListener(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entryContentsView?.assignURL(entry.url)
|
||||||
|
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
|
||||||
|
entryContentsView?.assignComment(entry.notes)
|
||||||
|
|
||||||
|
// Assign custom fields
|
||||||
|
if (entry.allowExtraFields()) {
|
||||||
|
entryContentsView?.clearExtraFields()
|
||||||
|
|
||||||
|
entry.fields.doActionToAllCustomProtectedField { label, value ->
|
||||||
|
val showAction = !value.isProtected || PreferencesUtil.allowCopyPasswordAndProtectedFields(this@EntryActivity)
|
||||||
|
entryContentsView?.addExtraField(label, value, showAction, View.OnClickListener {
|
||||||
|
clipboardHelper?.timeoutCopyToClipboard(
|
||||||
|
value.toString(),
|
||||||
|
getString(R.string.copy_field, label)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (readOnly) {
|
||||||
|
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) {
|
||||||
|
if (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))
|
||||||
|
}))
|
||||||
|
else if (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,
|
||||||
|
Uri.parse(getString(R.string.field_references_url))))
|
||||||
|
}))
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.menu_contribute -> return MenuUtil.onContributionItemSelected(this)
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Util.gotoUrl(this, url)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
Toast.makeText(this, R.string.no_url_handler, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
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,583 +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.os.Handler;
|
|
||||||
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.action.AddEntryRunnable;
|
|
||||||
import com.kunzisoft.keepass.database.action.OnFinishRunnable;
|
|
||||||
import com.kunzisoft.keepass.database.action.RunnableOnFinish;
|
|
||||||
import com.kunzisoft.keepass.database.action.UpdateEntryRunnable;
|
|
||||||
import com.kunzisoft.keepass.database.security.ProtectedString;
|
|
||||||
import com.kunzisoft.keepass.dialogs.GeneratePasswordDialogFragment;
|
|
||||||
import com.kunzisoft.keepass.dialogs.IconPickerDialogFragment;
|
|
||||||
import com.kunzisoft.keepass.icons.IconPackChooser;
|
|
||||||
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 static com.kunzisoft.keepass.dialogs.IconPickerDialogFragment.UNDEFINED_ICON_ID;
|
|
||||||
|
|
||||||
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";
|
|
||||||
|
|
||||||
protected PwEntry mEntry;
|
|
||||||
protected PwEntry mCallbackNewEntry;
|
|
||||||
protected boolean mIsNew;
|
|
||||||
protected int mSelectedIconID = UNDEFINED_ICON_ID;
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
toolbar.setTitle(getString(R.string.app_name));
|
|
||||||
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 db = App.getDB();
|
|
||||||
if ( ! db.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);
|
|
||||||
|
|
||||||
PwDatabase pm = db.getPwDatabase();
|
|
||||||
if ( uuidBytes == null ) {
|
|
||||||
PwGroupId parentId = (PwGroupId) intent.getSerializableExtra(KEY_PARENT);
|
|
||||||
PwGroup parent = pm.getGroupByGroupId(parentId);
|
|
||||||
mEntry = db.createEntry(parent);
|
|
||||||
mIsNew = true;
|
|
||||||
// Add the default icon
|
|
||||||
if (IconPackChooser.getSelectedIconPack(this).tintable()) {
|
|
||||||
App.getDB().getDrawFactory().assignDefaultDatabaseIconTo(this, entryIconView, true, iconColor);
|
|
||||||
} else {
|
|
||||||
App.getDB().getDrawFactory().assignDefaultDatabaseIconTo(this, entryIconView);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
UUID uuid = Types.bytestoUUID(uuidBytes);
|
|
||||||
mEntry = pm.getEntryByUUIDId(uuid);
|
|
||||||
mIsNew = false;
|
|
||||||
fillData();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the icon after an orientation change
|
|
||||||
if (savedInstanceState != null && savedInstanceState.containsKey(IconPickerDialogFragment.KEY_ICON_ID)) {
|
|
||||||
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
|
|
||||||
OnFinishRunnable onFinish = new AfterSave();
|
|
||||||
EntryEditActivity act = EntryEditActivity.this;
|
|
||||||
RunnableOnFinish task;
|
|
||||||
if ( mIsNew ) {
|
|
||||||
task = new AddEntryRunnable(act, App.getDB(), mCallbackNewEntry, onFinish);
|
|
||||||
} else {
|
|
||||||
task = new UpdateEntryRunnable(act, App.getDB(), 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() {
|
|
||||||
|
|
||||||
// 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.setIcon(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.endToManageFieldReferences();
|
|
||||||
|
|
||||||
return newEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the icon by the selection, or the first icon in the list if the entry is new or the last one
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private PwIconStandard retrieveIcon() {
|
|
||||||
if(mSelectedIconID != UNDEFINED_ICON_ID)
|
|
||||||
return App.getDB().getPwDatabase().getIconFactory().getIcon(mSelectedIconID);
|
|
||||||
else {
|
|
||||||
if (mIsNew) {
|
|
||||||
return App.getDB().getPwDatabase().getIconFactory().getFirstIcon();
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void fillData() {
|
|
||||||
|
|
||||||
if (IconPackChooser.getSelectedIconPack(this).tintable()) {
|
|
||||||
App.getDB().getDrawFactory().assignDatabaseIconTo(this, entryIconView, mEntry.getIcon(), true, iconColor);
|
|
||||||
} else {
|
|
||||||
App.getDB().getDrawFactory().assignDatabaseIconTo(this, entryIconView, mEntry.getIcon());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't start the field reference manager, we want to see the raw ref
|
|
||||||
mEntry.endToManageFieldReferences();
|
|
||||||
|
|
||||||
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) {
|
|
||||||
mSelectedIconID = bundle.getInt(IconPickerDialogFragment.KEY_ICON_ID);
|
|
||||||
entryIconView.setImageResource(IconPackChooser.getSelectedIconPack(this).iconToResId(mSelectedIconID));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSaveInstanceState(Bundle outState) {
|
|
||||||
if (mSelectedIconID != UNDEFINED_ICON_ID) {
|
|
||||||
outState.putInt(IconPickerDialogFragment.KEY_ICON_ID, mSelectedIconID);
|
|
||||||
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.putSerializable(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 OnFinishRunnable {
|
|
||||||
|
|
||||||
AfterSave() {
|
|
||||||
super(new Handler());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
runOnUiThread(() -> {
|
|
||||||
if ( mSuccess ) {
|
|
||||||
finish();
|
|
||||||
} else {
|
|
||||||
displayMessage(EntryEditActivity.this);
|
|
||||||
}
|
|
||||||
|
|
||||||
SaveDatabaseProgressTaskDialogFragment.stop(EntryEditActivity.this);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,422 @@
|
|||||||
|
/*
|
||||||
|
* 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 android.support.v7.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?.allowExtraFields() == true) { addNewCustomField() }
|
||||||
|
|
||||||
|
// Verify the education views
|
||||||
|
entryEditActivityEducation = EntryEditActivityEducation(this)
|
||||||
|
entryEditActivityEducation?.let {
|
||||||
|
Handler().post { performedNextEducation(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
||||||
|
val passwordView = entryEditContentsView?.generatePasswordView
|
||||||
|
val addNewFieldView = entryEditContentsView?.addNewFieldView
|
||||||
|
|
||||||
|
if (passwordView != null
|
||||||
|
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
||||||
|
passwordView,
|
||||||
|
{
|
||||||
|
openPasswordGenerator()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
performedNextEducation(entryEditActivityEducation)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
else if (mNewEntry != null && mNewEntry!!.allowExtraFields() && !mNewEntry!!.containsCustomFields()
|
||||||
|
&& addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE
|
||||||
|
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
|
||||||
|
addNewFieldView,
|
||||||
|
{
|
||||||
|
addNewCustomField()
|
||||||
|
}))
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
newEntry.fields.doActionToAllCustomProtectedField { key, value ->
|
||||||
|
addNewCustomField(key, 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,
|
||||||
|
!readOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
mEntry?.let { oldEntry ->
|
||||||
|
actionRunnable = UpdateEntryRunnable(this@EntryEditActivity,
|
||||||
|
database,
|
||||||
|
oldEntry,
|
||||||
|
newEntry,
|
||||||
|
afterActionNodeFinishRunnable,
|
||||||
|
!readOnly)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.menu_lock -> {
|
||||||
|
lockAndExit()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.menu_contribute -> return MenuUtil.onContributionItemSelected(this)
|
||||||
|
|
||||||
|
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,569 @@
|
|||||||
|
/*
|
||||||
|
* 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.Manifest
|
||||||
|
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 android.support.annotation.RequiresApi
|
||||||
|
import android.support.v7.app.AlertDialog
|
||||||
|
import android.support.v7.widget.LinearLayoutManager
|
||||||
|
import android.support.v7.widget.RecyclerView
|
||||||
|
import android.support.v7.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 android.widget.Toast
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.CreateFileDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.FileInformationDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.KeyFileHelper
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
|
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||||
|
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.fileselect.DeleteFileHistoryAsyncTask
|
||||||
|
import com.kunzisoft.keepass.fileselect.FileDatabaseModel
|
||||||
|
import com.kunzisoft.keepass.fileselect.OpenFileHistoryAsyncTask
|
||||||
|
import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory
|
||||||
|
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
|
||||||
|
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 net.cachapa.expandablelayout.ExpandableLayout
|
||||||
|
import permissions.dispatcher.*
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.IOException
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
import java.net.URLDecoder
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@RuntimePermissions
|
||||||
|
class FileDatabaseSelectActivity : StylishActivity(),
|
||||||
|
CreateFileDialogFragment.DefinePathDialogListener,
|
||||||
|
AssignMasterKeyDialogFragment.AssignPasswordDialogListener,
|
||||||
|
FileDatabaseHistoryAdapter.FileItemOpenListener,
|
||||||
|
FileDatabaseHistoryAdapter.FileSelectClearListener,
|
||||||
|
FileDatabaseHistoryAdapter.FileInformationShowListener {
|
||||||
|
|
||||||
|
// 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 mFileDatabaseHistory: FileDatabaseHistory? = null
|
||||||
|
|
||||||
|
private var mDatabaseFileUri: Uri? = null
|
||||||
|
|
||||||
|
private var mKeyFileHelper: KeyFileHelper? = null
|
||||||
|
|
||||||
|
private var mDefaultPath: String? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
mFileDatabaseHistory = FileDatabaseHistory.getInstance(WeakReference(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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// History list
|
||||||
|
val databaseFileListView = findViewById<RecyclerView>(R.id.file_list)
|
||||||
|
databaseFileListView.layoutManager = LinearLayoutManager(this)
|
||||||
|
|
||||||
|
// Open button
|
||||||
|
openButtonView = findViewById(R.id.open_database)
|
||||||
|
openButtonView?.setOnClickListener { _ ->
|
||||||
|
var fileName = openFileNameView?.text?.toString() ?: ""
|
||||||
|
mDefaultPath?.let {
|
||||||
|
if (fileName.isEmpty())
|
||||||
|
fileName = it
|
||||||
|
}
|
||||||
|
launchPasswordActivityWithPath(fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create button
|
||||||
|
createButtonView = findViewById(R.id.create_database)
|
||||||
|
createButtonView?.setOnClickListener { openCreateFileDialogFragmentWithPermissionCheck() }
|
||||||
|
|
||||||
|
mKeyFileHelper = KeyFileHelper(this)
|
||||||
|
browseButtonView = findViewById(R.id.browse_button)
|
||||||
|
browseButtonView?.setOnClickListener(mKeyFileHelper!!.getOpenFileOnClickViewListener {
|
||||||
|
Uri.parse("file://" + openFileNameView!!.text.toString())
|
||||||
|
})
|
||||||
|
|
||||||
|
// Construct adapter with listeners
|
||||||
|
mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this@FileDatabaseSelectActivity,
|
||||||
|
mFileDatabaseHistory?.databaseUriList ?: ArrayList())
|
||||||
|
mAdapterDatabaseHistory?.setOnItemClickListener(this)
|
||||||
|
mAdapterDatabaseHistory?.setFileSelectClearListener(this)
|
||||||
|
mAdapterDatabaseHistory?.setFileInformationShowListener(this)
|
||||||
|
databaseFileListView.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 fileName = prefs.getString(PasswordActivity.KEY_DEFAULT_FILENAME, "")
|
||||||
|
|
||||||
|
if (fileName != null && fileName.isNotEmpty()) {
|
||||||
|
val dbUri = UriUtil.parseUriFile(fileName)
|
||||||
|
var scheme: String? = null
|
||||||
|
if (dbUri != null)
|
||||||
|
scheme = dbUri.scheme
|
||||||
|
|
||||||
|
if (scheme != null && scheme.isNotEmpty() && scheme.equals("file", ignoreCase = true)) {
|
||||||
|
val path = dbUri!!.path
|
||||||
|
val db = File(path!!)
|
||||||
|
|
||||||
|
if (db.exists()) {
|
||||||
|
launchPasswordActivityWithPath(path)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (dbUri != null)
|
||||||
|
launchPasswordActivityWithPath(dbUri.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Handler().post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
|
||||||
|
// If no recent files
|
||||||
|
if (createButtonView != null
|
||||||
|
&& mFileDatabaseHistory != null
|
||||||
|
&& !mFileDatabaseHistory!!.hasRecentFiles() && fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
||||||
|
createButtonView!!,
|
||||||
|
{
|
||||||
|
openCreateFileDialogFragmentWithPermissionCheck()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// But if the user cancel, it can also select a database
|
||||||
|
performedNextEducation(fileDatabaseSelectActivityEducation)
|
||||||
|
}))
|
||||||
|
else if (browseButtonView != null
|
||||||
|
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
||||||
|
browseButtonView!!,
|
||||||
|
{tapTargetView ->
|
||||||
|
tapTargetView?.let {
|
||||||
|
mKeyFileHelper?.openFileOnClickViewListener?.onClick(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileSelectExpandableButtonView?.let {
|
||||||
|
fileDatabaseSelectActivityEducation
|
||||||
|
.checkAndPerformedOpenLinkDatabaseEducation(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fileNoFoundAction(e: FileNotFoundException) {
|
||||||
|
val error = getString(R.string.file_not_found_content)
|
||||||
|
Toast.makeText(this@FileDatabaseSelectActivity,
|
||||||
|
error, Toast.LENGTH_LONG).show()
|
||||||
|
Log.e(TAG, error, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchPasswordActivity(fileName: String, keyFile: String) {
|
||||||
|
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
PasswordActivity.launch(this@FileDatabaseSelectActivity,
|
||||||
|
fileName, keyFile)
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
fileNoFoundAction(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
PasswordActivity.launchForKeyboardResult(this@FileDatabaseSelectActivity,
|
||||||
|
fileName, keyFile)
|
||||||
|
finish()
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
fileNoFoundAction(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ assistStructure ->
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
try {
|
||||||
|
PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
||||||
|
fileName, keyFile,
|
||||||
|
assistStructure)
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
fileNoFoundAction(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchPasswordActivityWithPath(path: String) {
|
||||||
|
launchPasswordActivity(path, "")
|
||||||
|
// 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()
|
||||||
|
updateFileListVisibility()
|
||||||
|
mAdapterDatabaseHistory!!.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
// only to keep the current activity
|
||||||
|
outState.putBoolean(EXTRA_STAY, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
// NOTE: delegate the permission handling to generated method
|
||||||
|
onRequestPermissionsResult(requestCode, grantResults)
|
||||||
|
}
|
||||||
|
|
||||||
|
@NeedsPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
|
fun openCreateFileDialogFragment() {
|
||||||
|
val createFileDialogFragment = CreateFileDialogFragment()
|
||||||
|
createFileDialogFragment.show(supportFragmentManager, "createFileDialogFragment")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateFileListVisibility() {
|
||||||
|
if (mAdapterDatabaseHistory?.itemCount == 0)
|
||||||
|
fileListContainer?.visibility = View.INVISIBLE
|
||||||
|
else
|
||||||
|
fileListContainer?.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create file for database
|
||||||
|
* @return If not created, return false
|
||||||
|
*/
|
||||||
|
private fun createDatabaseFile(path: Uri): Boolean {
|
||||||
|
|
||||||
|
val pathString = URLDecoder.decode(path.path, "UTF-8")
|
||||||
|
// Make sure file name exists
|
||||||
|
if (pathString.isEmpty()) {
|
||||||
|
Log.e(TAG, getString(R.string.error_filename_required))
|
||||||
|
Toast.makeText(this@FileDatabaseSelectActivity,
|
||||||
|
R.string.error_filename_required,
|
||||||
|
Toast.LENGTH_LONG).show()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to create the file
|
||||||
|
val file = File(pathString)
|
||||||
|
try {
|
||||||
|
if (file.exists()) {
|
||||||
|
Log.e(TAG, getString(R.string.error_database_exists) + " " + file)
|
||||||
|
Toast.makeText(this@FileDatabaseSelectActivity,
|
||||||
|
R.string.error_database_exists,
|
||||||
|
Toast.LENGTH_LONG).show()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val parent = file.parentFile
|
||||||
|
|
||||||
|
if (parent == null || parent.exists() && !parent.isDirectory) {
|
||||||
|
Log.e(TAG, getString(R.string.error_invalid_path) + " " + file)
|
||||||
|
Toast.makeText(this@FileDatabaseSelectActivity,
|
||||||
|
R.string.error_invalid_path,
|
||||||
|
Toast.LENGTH_LONG).show()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parent.exists()) {
|
||||||
|
// Create parent directory
|
||||||
|
if (!parent.mkdirs()) {
|
||||||
|
Log.e(TAG, getString(R.string.error_could_not_create_parent) + " " + parent)
|
||||||
|
Toast.makeText(this@FileDatabaseSelectActivity,
|
||||||
|
R.string.error_could_not_create_parent,
|
||||||
|
Toast.LENGTH_LONG).show()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return file.createNewFile()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, getString(R.string.error_could_not_create_parent) + " " + e.localizedMessage)
|
||||||
|
e.printStackTrace()
|
||||||
|
Toast.makeText(
|
||||||
|
this@FileDatabaseSelectActivity,
|
||||||
|
getText(R.string.error_file_not_create).toString() + " "
|
||||||
|
+ e.localizedMessage,
|
||||||
|
Toast.LENGTH_LONG).show()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDefinePathDialogPositiveClick(pathFile: Uri?): Boolean {
|
||||||
|
mDatabaseFileUri = pathFile
|
||||||
|
if (pathFile == null)
|
||||||
|
return false
|
||||||
|
return if (createDatabaseFile(pathFile)) {
|
||||||
|
AssignMasterKeyDialogFragment().show(supportFragmentManager, "passwordDialog")
|
||||||
|
true
|
||||||
|
} else
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDefinePathDialogNegativeClick(pathFile: Uri?): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAssignKeyDialogPositiveClick(
|
||||||
|
masterPasswordChecked: Boolean, masterPassword: String?,
|
||||||
|
keyFileChecked: Boolean, keyFile: Uri?) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
UriUtil.parseUriFile(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)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
R.string.progress_create)
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
val error = "Unable to create database with this password and key file"
|
||||||
|
Toast.makeText(this, error, Toast.LENGTH_LONG).show()
|
||||||
|
Log.e(TAG, error + " " + e.message)
|
||||||
|
// TODO remove
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class LaunchGroupActivityFinish internal constructor(private val fileURI: Uri) : ActionRunnable() {
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
finishRun(true, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFinishRun(result: Result) {
|
||||||
|
runOnUiThread {
|
||||||
|
if (result.isSuccess) {
|
||||||
|
// Add database to recent files
|
||||||
|
mFileDatabaseHistory?.addDatabaseUri(fileURI)
|
||||||
|
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 onFileItemOpenListener(itemPosition: Int) {
|
||||||
|
OpenFileHistoryAsyncTask({ fileName, keyFile ->
|
||||||
|
if (fileName != null && keyFile != null)
|
||||||
|
launchPasswordActivity(fileName, keyFile)
|
||||||
|
updateFileListVisibility()
|
||||||
|
}, mFileDatabaseHistory).execute(itemPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClickFileInformation(fileDatabaseModel: FileDatabaseModel) {
|
||||||
|
FileInformationDialogFragment.newInstance(fileDatabaseModel).show(supportFragmentManager, "fileInformation")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFileSelectClearListener(fileDatabaseModel: FileDatabaseModel): Boolean {
|
||||||
|
DeleteFileHistoryAsyncTask({
|
||||||
|
fileDatabaseModel.fileUri?.let {
|
||||||
|
mFileDatabaseHistory?.deleteDatabaseUri(it)
|
||||||
|
}
|
||||||
|
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||||
|
updateFileListVisibility()
|
||||||
|
}, mFileDatabaseHistory, mAdapterDatabaseHistory).execute(fileDatabaseModel)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
mKeyFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
||||||
|
) { uri ->
|
||||||
|
if (uri != null) {
|
||||||
|
if (PreferencesUtil.autoOpenSelectedFile(this@FileDatabaseSelectActivity)) {
|
||||||
|
launchPasswordActivityWithPath(uri.toString())
|
||||||
|
} else {
|
||||||
|
fileSelectExpandableLayout?.expand(false)
|
||||||
|
openFileNameView?.setText(uri.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnShowRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
|
internal fun showRationaleForExternalStorage(request: PermissionRequest) {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setMessage(R.string.permission_external_storage_rationale_write_database)
|
||||||
|
.setPositiveButton(R.string.allow) { _, _ -> request.proceed() }
|
||||||
|
.setNegativeButton(R.string.cancel) { _, _ -> request.cancel() }
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnPermissionDenied(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
|
internal fun showDeniedForExternalStorage() {
|
||||||
|
Toast.makeText(this, R.string.permission_external_storage_denied, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnNeverAskAgain(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
|
internal fun showNeverAskForExternalStorage() {
|
||||||
|
Toast.makeText(this, R.string.permission_external_storage_never_ask, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
super.onCreateOptionsMenu(menu)
|
||||||
|
MenuUtil.defaultMenuInflater(menuInflater, menu)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* No Standard Launch, pass by PasswordActivity
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Keyboard Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun launchForKeyboardSelection(activity: Activity) {
|
||||||
|
KeyboardHelper.startActivityForKeyboardSelection(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,640 +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.app.Dialog;
|
|
||||||
import android.app.SearchManager;
|
|
||||||
import android.app.assist.AssistStructure;
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.res.TypedArray;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.annotation.RequiresApi;
|
|
||||||
import android.support.v7.widget.RecyclerView;
|
|
||||||
import android.support.v7.widget.SearchView;
|
|
||||||
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 com.getkeepsafe.taptargetview.TapTarget;
|
|
||||||
import com.getkeepsafe.taptargetview.TapTargetView;
|
|
||||||
import com.kunzisoft.keepass.R;
|
|
||||||
import com.kunzisoft.keepass.adapters.NodeAdapter;
|
|
||||||
import com.kunzisoft.keepass.app.App;
|
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper;
|
|
||||||
import com.kunzisoft.keepass.database.Database;
|
|
||||||
import com.kunzisoft.keepass.database.PwEntry;
|
|
||||||
import com.kunzisoft.keepass.database.PwGroup;
|
|
||||||
import com.kunzisoft.keepass.database.PwGroupId;
|
|
||||||
import com.kunzisoft.keepass.database.PwIcon;
|
|
||||||
import com.kunzisoft.keepass.database.PwIconStandard;
|
|
||||||
import com.kunzisoft.keepass.database.PwNode;
|
|
||||||
import com.kunzisoft.keepass.database.SortNodeEnum;
|
|
||||||
import com.kunzisoft.keepass.database.action.AddGroupRunnable;
|
|
||||||
import com.kunzisoft.keepass.database.action.DeleteEntryRunnable;
|
|
||||||
import com.kunzisoft.keepass.database.action.DeleteGroupRunnable;
|
|
||||||
import com.kunzisoft.keepass.database.action.UpdateGroupRunnable;
|
|
||||||
import com.kunzisoft.keepass.dialogs.AssignMasterKeyDialogFragment;
|
|
||||||
import com.kunzisoft.keepass.dialogs.GroupEditDialogFragment;
|
|
||||||
import com.kunzisoft.keepass.dialogs.IconPickerDialogFragment;
|
|
||||||
import com.kunzisoft.keepass.dialogs.ReadOnlyDialog;
|
|
||||||
import com.kunzisoft.keepass.icons.IconPackChooser;
|
|
||||||
import com.kunzisoft.keepass.search.SearchResultsActivity;
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil;
|
|
||||||
import com.kunzisoft.keepass.tasks.SaveDatabaseProgressTaskDialogFragment;
|
|
||||||
import com.kunzisoft.keepass.tasks.UpdateProgressTaskStatus;
|
|
||||||
import com.kunzisoft.keepass.view.AddNodeButtonView;
|
|
||||||
|
|
||||||
public class GroupActivity extends ListNodesActivity
|
|
||||||
implements GroupEditDialogFragment.EditGroupListener, IconPickerDialogFragment.IconPickerListener {
|
|
||||||
|
|
||||||
private static final String GROUP_ID_KEY = "GROUP_ID_KEY";
|
|
||||||
|
|
||||||
private Toolbar toolbar;
|
|
||||||
|
|
||||||
private ImageView iconView;
|
|
||||||
private AddNodeButtonView addNodeButtonView;
|
|
||||||
|
|
||||||
protected boolean addGroupEnabled = false;
|
|
||||||
protected boolean addEntryEnabled = false;
|
|
||||||
protected boolean isRoot = false;
|
|
||||||
protected boolean readOnly = false;
|
|
||||||
|
|
||||||
private static final String TAG = "Group Activity:";
|
|
||||||
|
|
||||||
private static final String OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY";
|
|
||||||
private PwGroup oldGroupToUpdate;
|
|
||||||
|
|
||||||
public static void launch(Activity act) {
|
|
||||||
recordFirstTimeBeforeLaunch(act);
|
|
||||||
launch(act, (PwGroup) null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void launch(Activity act, PwGroup group) {
|
|
||||||
if (checkTimeIsAllowedOrFinish(act)) {
|
|
||||||
Intent intent = new Intent(act, GroupActivity.class);
|
|
||||||
if (group != null) {
|
|
||||||
intent.putExtra(GROUP_ID_KEY, group.getId());
|
|
||||||
}
|
|
||||||
act.startActivityForResult(intent, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
public static void launch(Activity act, AssistStructure assistStructure) {
|
|
||||||
if ( assistStructure != null ) {
|
|
||||||
recordFirstTimeBeforeLaunch(act);
|
|
||||||
launch(act, null, assistStructure);
|
|
||||||
} else {
|
|
||||||
launch(act);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
public static void launch(Activity act, PwGroup group, AssistStructure assistStructure) {
|
|
||||||
if ( assistStructure != null ) {
|
|
||||||
if (checkTimeIsAllowedOrFinish(act)) {
|
|
||||||
Intent intent = new Intent(act, GroupActivity.class);
|
|
||||||
if (group != null) {
|
|
||||||
intent.putExtra(GROUP_ID_KEY, group.getId());
|
|
||||||
}
|
|
||||||
AutofillHelper.addAssistStructureExtraInIntent(intent, assistStructure);
|
|
||||||
act.startActivityForResult(intent, AutofillHelper.AUTOFILL_RESPONSE_REQUEST_CODE);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
launch(act, group);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
Log.w(TAG, "Retrieved tree");
|
|
||||||
if ( mCurrentGroup == null ) {
|
|
||||||
Log.w(TAG, "Group was null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (savedInstanceState != null
|
|
||||||
&& savedInstanceState.containsKey(OLD_GROUP_TO_UPDATE_KEY)) {
|
|
||||||
oldGroupToUpdate = (PwGroup) savedInstanceState.getSerializable(OLD_GROUP_TO_UPDATE_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct main view
|
|
||||||
setContentView(getLayoutInflater().inflate(R.layout.list_nodes_with_add_button, null));
|
|
||||||
|
|
||||||
iconView = findViewById(R.id.icon);
|
|
||||||
addNodeButtonView = findViewById(R.id.add_node_button);
|
|
||||||
addNodeButtonView.enableAddGroup(addGroupEnabled);
|
|
||||||
addNodeButtonView.enableAddEntry(addEntryEnabled);
|
|
||||||
// Hide when scroll
|
|
||||||
RecyclerView recyclerView = findViewById(R.id.nodes_list);
|
|
||||||
recyclerView.addOnScrollListener(addNodeButtonView.hideButtonOnScrollListener());
|
|
||||||
|
|
||||||
toolbar = findViewById(R.id.toolbar);
|
|
||||||
toolbar.setTitle("");
|
|
||||||
setSupportActionBar(toolbar);
|
|
||||||
|
|
||||||
if ( mCurrentGroup.getParent() != null )
|
|
||||||
toolbar.setNavigationIcon(R.drawable.ic_arrow_up_white_24dp);
|
|
||||||
|
|
||||||
addNodeButtonView.setAddGroupClickListener(v -> {
|
|
||||||
GroupEditDialogFragment.build()
|
|
||||||
.show(getSupportFragmentManager(),
|
|
||||||
GroupEditDialogFragment.TAG_CREATE_GROUP);
|
|
||||||
});
|
|
||||||
addNodeButtonView.setAddEntryClickListener(v ->
|
|
||||||
EntryEditActivity.launch(GroupActivity.this, mCurrentGroup));
|
|
||||||
|
|
||||||
setGroupTitle();
|
|
||||||
|
|
||||||
Log.w(TAG, "Finished creating tree");
|
|
||||||
|
|
||||||
if (isRoot) {
|
|
||||||
showWarnings();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected PwGroup initCurrentGroup() {
|
|
||||||
PwGroup currentGroup;
|
|
||||||
Database db = App.getDB();
|
|
||||||
readOnly = db.isReadOnly();
|
|
||||||
PwGroup root = db.getPwDatabase().getRootGroup();
|
|
||||||
|
|
||||||
Log.w(TAG, "Creating tree view");
|
|
||||||
PwGroupId pwGroupId = (PwGroupId) getIntent().getSerializableExtra(GROUP_ID_KEY);
|
|
||||||
if ( pwGroupId == null ) {
|
|
||||||
currentGroup = root;
|
|
||||||
} else {
|
|
||||||
currentGroup = db.getPwDatabase().getGroupByGroupId(pwGroupId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentGroup != null) {
|
|
||||||
addGroupEnabled = !readOnly;
|
|
||||||
addEntryEnabled = !readOnly; // TODO ReadOnly
|
|
||||||
isRoot = (currentGroup == root);
|
|
||||||
if (!currentGroup.allowAddEntryIfIsRoot())
|
|
||||||
addEntryEnabled = !isRoot && addEntryEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentGroup;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected RecyclerView defineNodeList() {
|
|
||||||
return (RecyclerView) findViewById(R.id.nodes_list);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void addOptionsToAdapter(NodeAdapter nodeAdapter) {
|
|
||||||
super.addOptionsToAdapter(nodeAdapter);
|
|
||||||
|
|
||||||
nodeAdapter.setActivateContextMenu(true);
|
|
||||||
nodeAdapter.setNodeMenuListener(new NodeAdapter.NodeMenuListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onOpenMenuClick(PwNode node) {
|
|
||||||
switch (node.getType()) {
|
|
||||||
case GROUP:
|
|
||||||
GroupActivity.launch(GroupActivity.this, (PwGroup) node);
|
|
||||||
break;
|
|
||||||
case ENTRY:
|
|
||||||
EntryActivity.launch(GroupActivity.this, (PwEntry) node);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onEditMenuClick(PwNode node) {
|
|
||||||
switch (node.getType()) {
|
|
||||||
case GROUP:
|
|
||||||
oldGroupToUpdate = (PwGroup) node;
|
|
||||||
GroupEditDialogFragment.build(node)
|
|
||||||
.show(getSupportFragmentManager(),
|
|
||||||
GroupEditDialogFragment.TAG_CREATE_GROUP);
|
|
||||||
break;
|
|
||||||
case ENTRY:
|
|
||||||
EntryEditActivity.launch(GroupActivity.this, (PwEntry) node);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onDeleteMenuClick(PwNode node) {
|
|
||||||
switch (node.getType()) {
|
|
||||||
case GROUP:
|
|
||||||
deleteGroup((PwGroup) node);
|
|
||||||
break;
|
|
||||||
case ENTRY:
|
|
||||||
deleteEntry((PwEntry) node);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
// Refresh the group icon
|
|
||||||
assignGroupIcon();
|
|
||||||
// Show button on resume
|
|
||||||
addNodeButtonView.showButton();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check and display learning views
|
|
||||||
* Displays the explanation for a add, search, sort a new node and lock the database
|
|
||||||
*/
|
|
||||||
private void checkAndPerformedEducation(Menu menu) {
|
|
||||||
|
|
||||||
// If no node, show education to add new one
|
|
||||||
if (mAdapter.getItemCount() <= 0) {
|
|
||||||
if (!PreferencesUtil.isEducationNewNodePerformed(this)) {
|
|
||||||
|
|
||||||
TapTargetView.showFor(this,
|
|
||||||
TapTarget.forView(findViewById(R.id.add_button),
|
|
||||||
getString(R.string.education_new_node_title),
|
|
||||||
getString(R.string.education_new_node_summary))
|
|
||||||
.textColorInt(Color.WHITE)
|
|
||||||
.tintTarget(false)
|
|
||||||
.cancelable(true),
|
|
||||||
new TapTargetView.Listener() {
|
|
||||||
@Override
|
|
||||||
public void onTargetClick(TapTargetView view) {
|
|
||||||
super.onTargetClick(view);
|
|
||||||
addNodeButtonView.openButtonIfClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOuterCircleClick(TapTargetView view) {
|
|
||||||
super.onOuterCircleClick(view);
|
|
||||||
view.dismiss(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
PreferencesUtil.saveEducationPreference(this,
|
|
||||||
R.string.education_new_node_key);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Else show the search education
|
|
||||||
else if (!PreferencesUtil.isEducationSearchPerformed(this)) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
TapTargetView.showFor(this,
|
|
||||||
TapTarget.forToolbarMenuItem(toolbar, R.id.menu_search,
|
|
||||||
getString(R.string.education_search_title),
|
|
||||||
getString(R.string.education_search_summary))
|
|
||||||
.textColorInt(Color.WHITE)
|
|
||||||
.tintTarget(true)
|
|
||||||
.cancelable(true),
|
|
||||||
new TapTargetView.Listener() {
|
|
||||||
@Override
|
|
||||||
public void onTargetClick(TapTargetView view) {
|
|
||||||
super.onTargetClick(view);
|
|
||||||
MenuItem searchItem = menu.findItem(R.id.menu_search);
|
|
||||||
searchItem.expandActionView();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOuterCircleClick(TapTargetView view) {
|
|
||||||
super.onOuterCircleClick(view);
|
|
||||||
view.dismiss(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
PreferencesUtil.saveEducationPreference(this,
|
|
||||||
R.string.education_search_key);
|
|
||||||
} catch (Exception e) {
|
|
||||||
// If icon not visible
|
|
||||||
Log.w(TAG, "Can't performed education for search");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Else show the sort education
|
|
||||||
else if (!PreferencesUtil.isEducationSortPerformed(this)) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
TapTargetView.showFor(this,
|
|
||||||
TapTarget.forToolbarMenuItem(toolbar, R.id.menu_sort,
|
|
||||||
getString(R.string.education_sort_title),
|
|
||||||
getString(R.string.education_sort_summary))
|
|
||||||
.textColorInt(Color.WHITE)
|
|
||||||
.tintTarget(true)
|
|
||||||
.cancelable(true),
|
|
||||||
new TapTargetView.Listener() {
|
|
||||||
@Override
|
|
||||||
public void onTargetClick(TapTargetView view) {
|
|
||||||
super.onTargetClick(view);
|
|
||||||
MenuItem sortItem = menu.findItem(R.id.menu_sort);
|
|
||||||
onOptionsItemSelected(sortItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOuterCircleClick(TapTargetView view) {
|
|
||||||
super.onOuterCircleClick(view);
|
|
||||||
view.dismiss(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
PreferencesUtil.saveEducationPreference(this,
|
|
||||||
R.string.education_sort_key);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.w(TAG, "Can't performed education for sort");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Else show the lock education
|
|
||||||
else if (!PreferencesUtil.isEducationLockPerformed(this)) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
TapTargetView.showFor(this,
|
|
||||||
TapTarget.forToolbarMenuItem(toolbar, R.id.menu_lock,
|
|
||||||
getString(R.string.education_lock_title),
|
|
||||||
getString(R.string.education_lock_summary))
|
|
||||||
.textColorInt(Color.WHITE)
|
|
||||||
.tintTarget(true)
|
|
||||||
.cancelable(true),
|
|
||||||
new TapTargetView.Listener() {
|
|
||||||
@Override
|
|
||||||
public void onTargetClick(TapTargetView view) {
|
|
||||||
super.onTargetClick(view);
|
|
||||||
MenuItem lockItem = menu.findItem(R.id.menu_lock);
|
|
||||||
onOptionsItemSelected(lockItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOuterCircleClick(TapTargetView view) {
|
|
||||||
super.onOuterCircleClick(view);
|
|
||||||
view.dismiss(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
PreferencesUtil.saveEducationPreference(this,
|
|
||||||
R.string.education_lock_key);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.w(TAG, "Can't performed education for lock");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
// Hide button
|
|
||||||
addNodeButtonView.hideButton();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSortSelected(SortNodeEnum sortNodeEnum, boolean ascending, boolean groupsBefore, boolean recycleBinBottom) {
|
|
||||||
super.onSortSelected(sortNodeEnum, ascending, groupsBefore, recycleBinBottom);
|
|
||||||
// Show button if hide after sort
|
|
||||||
addNodeButtonView.showButton();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assign the group icon depending of IconPack or custom icon
|
|
||||||
*/
|
|
||||||
protected void assignGroupIcon() {
|
|
||||||
if (mCurrentGroup != null) {
|
|
||||||
if (IconPackChooser.getSelectedIconPack(this).tintable()) {
|
|
||||||
// Retrieve the textColor to tint the icon
|
|
||||||
int[] attrs = {R.attr.textColorInverse};
|
|
||||||
TypedArray ta = getTheme().obtainStyledAttributes(attrs);
|
|
||||||
int iconColor = ta.getColor(0, Color.WHITE);
|
|
||||||
App.getDB().getDrawFactory().assignDatabaseIconTo(this, iconView, mCurrentGroup.getIcon(), true, iconColor);
|
|
||||||
} else {
|
|
||||||
App.getDB().getDrawFactory().assignDatabaseIconTo(this, iconView, mCurrentGroup.getIcon());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteEntry(PwEntry entry) {
|
|
||||||
Handler handler = new Handler();
|
|
||||||
DeleteEntryRunnable task = new DeleteEntryRunnable(this, App.getDB(), entry,
|
|
||||||
new AfterDeleteNode(handler, entry));
|
|
||||||
task.setUpdateProgressTaskStatus(
|
|
||||||
new UpdateProgressTaskStatus(this,
|
|
||||||
SaveDatabaseProgressTaskDialogFragment.start(
|
|
||||||
getSupportFragmentManager())
|
|
||||||
));
|
|
||||||
new Thread(task).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteGroup(PwGroup group) {
|
|
||||||
//TODO Verify trash recycle bin
|
|
||||||
Handler handler = new Handler();
|
|
||||||
DeleteGroupRunnable task = new DeleteGroupRunnable(this, App.getDB(), group,
|
|
||||||
new AfterDeleteNode(handler, group));
|
|
||||||
task.setUpdateProgressTaskStatus(
|
|
||||||
new UpdateProgressTaskStatus(this,
|
|
||||||
SaveDatabaseProgressTaskDialogFragment.start(
|
|
||||||
getSupportFragmentManager())
|
|
||||||
));
|
|
||||||
new Thread(task).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
|
||||||
|
|
||||||
MenuInflater inflater = getMenuInflater();
|
|
||||||
inflater.inflate(R.menu.search, menu);
|
|
||||||
inflater.inflate(R.menu.database_master_key, menu);
|
|
||||||
inflater.inflate(R.menu.database_lock, menu);
|
|
||||||
|
|
||||||
// Get the SearchView and set the searchable configuration
|
|
||||||
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
|
|
||||||
assert searchManager != null;
|
|
||||||
|
|
||||||
MenuItem searchItem = menu.findItem(R.id.menu_search);
|
|
||||||
SearchView searchView = null;
|
|
||||||
if (searchItem != null) {
|
|
||||||
searchView = (SearchView) searchItem.getActionView();
|
|
||||||
}
|
|
||||||
if (searchView != null) {
|
|
||||||
// TODO Flickering when locking, will be better with content provider
|
|
||||||
searchView.setSearchableInfo(searchManager.getSearchableInfo(new ComponentName(this, SearchResultsActivity.class)));
|
|
||||||
searchView.setIconifiedByDefault(false); // Do not iconify the widget; expand it by default
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onCreateOptionsMenu(menu);
|
|
||||||
|
|
||||||
// Launch education screen
|
|
||||||
new Handler().post(() -> checkAndPerformedEducation(menu));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void startActivity(Intent intent) {
|
|
||||||
boolean customSearchQueryExecuted = false;
|
|
||||||
|
|
||||||
// Get the intent, verify the action and get the query
|
|
||||||
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
|
|
||||||
String query = intent.getStringExtra(SearchManager.QUERY);
|
|
||||||
// manually launch the real search activity
|
|
||||||
final Intent searchIntent = new Intent(getApplicationContext(), SearchResultsActivity.class);
|
|
||||||
// add query to the Intent Extras
|
|
||||||
searchIntent.putExtra(SearchManager.QUERY, query);
|
|
||||||
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
|
||||||
&& autofillHelper.getAssistStructure() != null ) {
|
|
||||||
AutofillHelper.addAssistStructureExtraInIntent(searchIntent, autofillHelper.getAssistStructure());
|
|
||||||
startActivityForResult(searchIntent, AutofillHelper.AUTOFILL_RESPONSE_REQUEST_CODE);
|
|
||||||
customSearchQueryExecuted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!customSearchQueryExecuted) {
|
|
||||||
super.startActivity(intent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
|
|
||||||
case android.R.id.home:
|
|
||||||
onBackPressed();
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case R.id.menu_search:
|
|
||||||
onSearchRequested();
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case R.id.menu_lock:
|
|
||||||
lockAndExit();
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case R.id.menu_change_master_key:
|
|
||||||
setPassword();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setPassword() {
|
|
||||||
AssignMasterKeyDialogFragment dialog = new AssignMasterKeyDialogFragment();
|
|
||||||
dialog.show(getSupportFragmentManager(), "passwordDialog");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void approveEditGroup(GroupEditDialogFragment.EditGroupDialogAction action,
|
|
||||||
String name,
|
|
||||||
PwIcon icon) {
|
|
||||||
Database database = App.getDB();
|
|
||||||
PwIconStandard iconStandard = database.getPwDatabase().getIconFactory().getFirstIcon();
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case CREATION:
|
|
||||||
// If group creation
|
|
||||||
// Build the group
|
|
||||||
PwGroup newGroup = database.createGroup(mCurrentGroup);
|
|
||||||
newGroup.setName(name);
|
|
||||||
try {
|
|
||||||
iconStandard = (PwIconStandard) icon;
|
|
||||||
} catch (Exception ignored) {} // TODO custom icon
|
|
||||||
newGroup.setIcon(iconStandard);
|
|
||||||
|
|
||||||
// If group created save it in the database
|
|
||||||
AddGroupRunnable addGroupRunnable = new AddGroupRunnable(this,
|
|
||||||
App.getDB(),
|
|
||||||
newGroup,
|
|
||||||
new AfterAddNode(new Handler()));
|
|
||||||
addGroupRunnable.setUpdateProgressTaskStatus(
|
|
||||||
new UpdateProgressTaskStatus(this,
|
|
||||||
SaveDatabaseProgressTaskDialogFragment.start(
|
|
||||||
getSupportFragmentManager())
|
|
||||||
));
|
|
||||||
new Thread(addGroupRunnable).start();
|
|
||||||
|
|
||||||
break;
|
|
||||||
case UPDATE:
|
|
||||||
// If update add new elements
|
|
||||||
if (oldGroupToUpdate != null) {
|
|
||||||
PwGroup updateGroup = oldGroupToUpdate.clone();
|
|
||||||
updateGroup.setName(name);
|
|
||||||
try {
|
|
||||||
iconStandard = (PwIconStandard) icon;
|
|
||||||
} catch (Exception ignored) {} // TODO custom icon
|
|
||||||
updateGroup.setIcon(iconStandard);
|
|
||||||
|
|
||||||
mAdapter.removeNode(oldGroupToUpdate);
|
|
||||||
|
|
||||||
// If group updated save it in the database
|
|
||||||
UpdateGroupRunnable updateGroupRunnable = new UpdateGroupRunnable(this,
|
|
||||||
App.getDB(),
|
|
||||||
oldGroupToUpdate,
|
|
||||||
updateGroup,
|
|
||||||
new AfterUpdateNode(new Handler()));
|
|
||||||
updateGroupRunnable.setUpdateProgressTaskStatus(
|
|
||||||
new UpdateProgressTaskStatus(this,
|
|
||||||
SaveDatabaseProgressTaskDialogFragment.start(
|
|
||||||
getSupportFragmentManager())
|
|
||||||
));
|
|
||||||
new Thread(updateGroupRunnable).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSaveInstanceState(Bundle outState) {
|
|
||||||
outState.putSerializable(OLD_GROUP_TO_UPDATE_KEY, oldGroupToUpdate);
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void cancelEditGroup(GroupEditDialogFragment.EditGroupDialogAction action,
|
|
||||||
String name,
|
|
||||||
PwIcon iconId) {
|
|
||||||
// Do nothing here
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
// For icon in create tree dialog
|
|
||||||
public void iconPicked(Bundle bundle) {
|
|
||||||
GroupEditDialogFragment groupEditDialogFragment =
|
|
||||||
(GroupEditDialogFragment) getSupportFragmentManager()
|
|
||||||
.findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP);
|
|
||||||
if (groupEditDialogFragment != null) {
|
|
||||||
groupEditDialogFragment.iconPicked(bundle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void showWarnings() {
|
|
||||||
if (App.getDB().isReadOnly()) {
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
|
||||||
|
|
||||||
if (prefs.getBoolean(getString(R.string.show_read_only_warning), true)) {
|
|
||||||
Dialog dialog = new ReadOnlyDialog(this);
|
|
||||||
dialog.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,998 @@
|
|||||||
|
/*
|
||||||
|
* 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 android.preference.PreferenceManager
|
||||||
|
import android.support.annotation.RequiresApi
|
||||||
|
import android.support.v4.app.FragmentManager
|
||||||
|
import android.support.v7.widget.SearchView
|
||||||
|
import android.support.v7.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.KeyboardHelper
|
||||||
|
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, readOnly, 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) {
|
||||||
|
Log.d(TAG, "setNewIntent: $intent")
|
||||||
|
setIntent(intent)
|
||||||
|
mCurrentGroupIsASearch = if (Intent.ACTION_SEARCH == intent.action) {
|
||||||
|
// only one instance of search in backstack
|
||||||
|
openSearchGroup(retrieveCurrentGroup(intent, 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, readOnly, 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? {
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
readOnly = mDatabase?.isReadOnly == true || readOnly // Force read only if the database is like that
|
||||||
|
|
||||||
|
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 (selectionMode) {
|
||||||
|
modeTitleView?.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
modeTitleView?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show button if allowed
|
||||||
|
addNodeButtonView?.apply {
|
||||||
|
|
||||||
|
// To enable add button
|
||||||
|
val addGroupEnabled = !readOnly && !mCurrentGroupIsASearch
|
||||||
|
var addEntryEnabled = !readOnly && !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, readOnly)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// 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(),
|
||||||
|
!readOnly)
|
||||||
|
}.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(),
|
||||||
|
!readOnly)
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun moveEntry(entryToMove: EntryVersioned, newParent: GroupVersioned) {
|
||||||
|
ProgressDialogSaveDatabaseThread(this) {
|
||||||
|
MoveEntryRunnable(
|
||||||
|
this,
|
||||||
|
Database.getInstance(),
|
||||||
|
entryToMove,
|
||||||
|
newParent,
|
||||||
|
AfterAddNodeRunnable(),
|
||||||
|
!readOnly)
|
||||||
|
}.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(),
|
||||||
|
!readOnly)
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteEntry(entry: EntryVersioned) {
|
||||||
|
ProgressDialogSaveDatabaseThread(this) {
|
||||||
|
DeleteEntryRunnable(
|
||||||
|
this,
|
||||||
|
Database.getInstance(),
|
||||||
|
entry,
|
||||||
|
AfterDeleteNodeRunnable(),
|
||||||
|
!readOnly)
|
||||||
|
}.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 (!selectionMode) {
|
||||||
|
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
|
||||||
|
if (mListNodesFragment != null
|
||||||
|
&& mListNodesFragment!!.isEmpty
|
||||||
|
&& addNodeButtonView?.addButtonView != null
|
||||||
|
&& addNodeButtonView!!.isEnable
|
||||||
|
&& groupActivityEducation.checkAndPerformedAddNodeButtonEducation(
|
||||||
|
addNodeButtonView?.addButtonView!!,
|
||||||
|
{
|
||||||
|
addNodeButtonView?.openButtonIfClose()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
performedNextEducation(groupActivityEducation, menu)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
else if (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)
|
||||||
|
}))
|
||||||
|
else if (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)
|
||||||
|
}))
|
||||||
|
else if (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 startActivity(intent: Intent) {
|
||||||
|
|
||||||
|
// Get the intent, verify the action and get the query
|
||||||
|
if (Intent.ACTION_SEARCH == intent.action) {
|
||||||
|
val query = intent.getStringExtra(SearchManager.QUERY)
|
||||||
|
// manually launch the real search activity
|
||||||
|
val searchIntent = Intent(applicationContext, GroupActivity::class.java)
|
||||||
|
// add query to the Intent Extras
|
||||||
|
searchIntent.action = Intent.ACTION_SEARCH
|
||||||
|
searchIntent.putExtra(SearchManager.QUERY, query)
|
||||||
|
|
||||||
|
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||||
|
{
|
||||||
|
super@GroupActivity.startActivity(intent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
KeyboardHelper.startActivityForKeyboardSelection(
|
||||||
|
this@GroupActivity,
|
||||||
|
searchIntent)
|
||||||
|
finish()
|
||||||
|
},
|
||||||
|
{ assistStructure ->
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
AutofillHelper.startActivityForAutofillResult(
|
||||||
|
this@GroupActivity,
|
||||||
|
searchIntent,
|
||||||
|
assistStructure)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
super.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, readOnly, 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(),
|
||||||
|
!readOnly)
|
||||||
|
}.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(),
|
||||||
|
!readOnly)
|
||||||
|
}.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() {
|
||||||
|
// TODO Preferences
|
||||||
|
if (Database.getInstance().isReadOnly) {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
if (prefs.getBoolean(getString(R.string.show_read_only_warning), true)) {
|
||||||
|
ReadOnlyDialog(this).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSortSelected(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) {
|
||||||
|
mListNodesFragment?.onSortSelected(sortNodeEnum, ascending, groupsBefore, recycleBinBottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||||
|
super.startActivityForResult(intent, requestCode, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 launchForKeyboardSelection(activity: Activity, readOnly: Boolean) {
|
||||||
|
TimeoutHelper.recordTime(activity)
|
||||||
|
buildAndLaunchIntent(activity, null, readOnly) { intent ->
|
||||||
|
KeyboardHelper.startActivityForKeyboardSelection(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,383 +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.annotation.SuppressLint;
|
|
||||||
import android.app.assist.AssistStructure;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.SharedPreferences.Editor;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
|
||||||
import android.support.v7.widget.RecyclerView;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R;
|
|
||||||
import com.kunzisoft.keepass.adapters.NodeAdapter;
|
|
||||||
import com.kunzisoft.keepass.app.App;
|
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper;
|
|
||||||
import com.kunzisoft.keepass.compat.EditorCompat;
|
|
||||||
import com.kunzisoft.keepass.database.Database;
|
|
||||||
import com.kunzisoft.keepass.database.PwDatabase;
|
|
||||||
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.database.action.AfterActionNodeOnFinish;
|
|
||||||
import com.kunzisoft.keepass.database.action.OnFinishRunnable;
|
|
||||||
import com.kunzisoft.keepass.dialogs.AssignMasterKeyDialogFragment;
|
|
||||||
import com.kunzisoft.keepass.dialogs.SortDialogFragment;
|
|
||||||
import com.kunzisoft.keepass.password.AssignPasswordHelper;
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil;
|
|
||||||
import com.kunzisoft.keepass.tasks.SaveDatabaseProgressTaskDialogFragment;
|
|
||||||
import com.kunzisoft.keepass.tasks.UIToastTask;
|
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil;
|
|
||||||
|
|
||||||
public abstract class ListNodesActivity extends LockingActivity
|
|
||||||
implements AssignMasterKeyDialogFragment.AssignPasswordDialogListener,
|
|
||||||
NodeAdapter.OnNodeClickCallback,
|
|
||||||
SortDialogFragment.SortSelectionListener {
|
|
||||||
|
|
||||||
protected PwGroup mCurrentGroup;
|
|
||||||
protected NodeAdapter mAdapter;
|
|
||||||
|
|
||||||
private SharedPreferences prefs;
|
|
||||||
|
|
||||||
protected AutofillHelper autofillHelper;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
if ( isFinishing() ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Likely the app has been killed exit the activity
|
|
||||||
if ( ! App.getDB().getLoaded() ) {
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
|
||||||
|
|
||||||
invalidateOptionsMenu();
|
|
||||||
|
|
||||||
// TODO Move in search
|
|
||||||
setContentView(R.layout.list_nodes);
|
|
||||||
|
|
||||||
mCurrentGroup = initCurrentGroup();
|
|
||||||
|
|
||||||
mAdapter = new NodeAdapter(this);
|
|
||||||
addOptionsToAdapter(mAdapter);
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
autofillHelper = new AutofillHelper();
|
|
||||||
autofillHelper.retrieveAssistStructure(getIntent());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract PwGroup initCurrentGroup();
|
|
||||||
|
|
||||||
protected abstract RecyclerView defineNodeList();
|
|
||||||
|
|
||||||
protected void addOptionsToAdapter(NodeAdapter nodeAdapter) {
|
|
||||||
mAdapter.setOnNodeClickListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
// Add elements to the list
|
|
||||||
mAdapter.rebuildList(mCurrentGroup);
|
|
||||||
assignListToNodeAdapter(defineNodeList());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setGroupTitle() {
|
|
||||||
if ( mCurrentGroup != null ) {
|
|
||||||
String name = mCurrentGroup.getName();
|
|
||||||
TextView tv = findViewById(R.id.group_name);
|
|
||||||
if ( name != null && name.length() > 0 ) {
|
|
||||||
if ( tv != null ) {
|
|
||||||
tv.setText(name);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ( tv != null ) {
|
|
||||||
tv.setText(getText(R.string.root));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void assignListToNodeAdapter(RecyclerView recyclerView) {
|
|
||||||
recyclerView.setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET);
|
|
||||||
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
|
||||||
recyclerView.setAdapter(mAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNodeClick(PwNode node) {
|
|
||||||
|
|
||||||
// Add event when we have Autofill
|
|
||||||
AssistStructure assistStructure = null;
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
assistStructure = autofillHelper.getAssistStructure();
|
|
||||||
if (assistStructure != null) {
|
|
||||||
switch (node.getType()) {
|
|
||||||
case GROUP:
|
|
||||||
GroupActivity.launch(this, (PwGroup) node, assistStructure);
|
|
||||||
break;
|
|
||||||
case ENTRY:
|
|
||||||
// Build response with the entry selected
|
|
||||||
autofillHelper.buildResponseWhenEntrySelected(this, (PwEntry) node);
|
|
||||||
finish();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ( assistStructure == null ){
|
|
||||||
switch (node.getType()) {
|
|
||||||
case GROUP:
|
|
||||||
GroupActivity.launch(this, (PwGroup) node);
|
|
||||||
break;
|
|
||||||
case ENTRY:
|
|
||||||
EntryActivity.launch(this, (PwEntry) node);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
|
||||||
super.onCreateOptionsMenu(menu);
|
|
||||||
|
|
||||||
MenuInflater inflater = getMenuInflater();
|
|
||||||
MenuUtil.contributionMenuInflater(inflater, menu);
|
|
||||||
inflater.inflate(R.menu.tree, menu);
|
|
||||||
inflater.inflate(R.menu.default_menu, menu);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSortSelected(SortNodeEnum sortNodeEnum, boolean ascending, boolean groupsBefore, boolean recycleBinBottom) {
|
|
||||||
// Toggle setting
|
|
||||||
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);
|
|
||||||
EditorCompat.apply(editor);
|
|
||||||
|
|
||||||
// Tell the adapter to refresh it's list
|
|
||||||
mAdapter.notifyChangeSort(sortNodeEnum, ascending, groupsBefore);
|
|
||||||
mAdapter.rebuildList(mCurrentGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
@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(this),
|
|
||||||
PreferencesUtil.getAscendingSort(this),
|
|
||||||
PreferencesUtil.getGroupsBeforeSort(this));
|
|
||||||
//}
|
|
||||||
|
|
||||||
sortDialogFragment.show(getSupportFragmentManager(), "sortDialog");
|
|
||||||
return true;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Check the time lock before launching settings
|
|
||||||
MenuUtil.onDefaultMenuOptionsItemSelected(this, item, true);
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAssignKeyDialogPositiveClick(
|
|
||||||
boolean masterPasswordChecked, String masterPassword,
|
|
||||||
boolean keyFileChecked, Uri keyFile) {
|
|
||||||
|
|
||||||
AssignPasswordHelper assignPasswordHelper =
|
|
||||||
new AssignPasswordHelper(this,
|
|
||||||
masterPasswordChecked, masterPassword, keyFileChecked, keyFile);
|
|
||||||
assignPasswordHelper.assignPasswordInDatabase(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAssignKeyDialogNegativeClick(
|
|
||||||
boolean masterPasswordChecked, String masterPassword,
|
|
||||||
boolean keyFileChecked, Uri keyFile) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@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:
|
|
||||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE ||
|
|
||||||
resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
|
|
||||||
PwNode newNode = (PwNode) data.getSerializableExtra(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(mCurrentGroup);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.e(this.getClass().getName(), "New node can be retrieve in Activity Result");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
@Override
|
|
||||||
public void startActivityForResult(Intent intent, int requestCode, Bundle options) {
|
|
||||||
/*
|
|
||||||
* 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.equals(intent.getAction())) {
|
|
||||||
int flags = intent.getFlags();
|
|
||||||
flags &= ~Intent.FLAG_ACTIVITY_NEW_TASK;
|
|
||||||
intent.setFlags(flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
|
||||||
super.startActivityForResult(intent, requestCode, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AfterAddNode extends AfterActionNodeOnFinish {
|
|
||||||
AfterAddNode(Handler handler) {
|
|
||||||
super(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void run(PwNode oldNode, PwNode newNode) {
|
|
||||||
super.run();
|
|
||||||
|
|
||||||
runOnUiThread(() -> {
|
|
||||||
if (mSuccess) {
|
|
||||||
mAdapter.addNode(newNode);
|
|
||||||
} else {
|
|
||||||
displayMessage(ListNodesActivity.this);
|
|
||||||
}
|
|
||||||
|
|
||||||
SaveDatabaseProgressTaskDialogFragment.stop(ListNodesActivity.this);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AfterUpdateNode extends AfterActionNodeOnFinish {
|
|
||||||
AfterUpdateNode(Handler handler) {
|
|
||||||
super(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void run(PwNode oldNode, PwNode newNode) {
|
|
||||||
super.run();
|
|
||||||
|
|
||||||
runOnUiThread(() -> {
|
|
||||||
if (mSuccess) {
|
|
||||||
mAdapter.updateNode(oldNode, newNode);
|
|
||||||
} else {
|
|
||||||
displayMessage(ListNodesActivity.this);
|
|
||||||
}
|
|
||||||
|
|
||||||
SaveDatabaseProgressTaskDialogFragment.stop(ListNodesActivity.this);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AfterDeleteNode extends OnFinishRunnable {
|
|
||||||
private PwNode pwNode;
|
|
||||||
|
|
||||||
AfterDeleteNode(Handler handler, PwNode pwNode) {
|
|
||||||
super(handler);
|
|
||||||
this.pwNode = pwNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
super.run();
|
|
||||||
|
|
||||||
runOnUiThread(() -> {
|
|
||||||
if ( mSuccess) {
|
|
||||||
mAdapter.removeNode(pwNode);
|
|
||||||
PwGroup parent = pwNode.getParent();
|
|
||||||
Database db = App.getDB();
|
|
||||||
PwDatabase database = db.getPwDatabase();
|
|
||||||
if (db.isRecycleBinAvailable() &&
|
|
||||||
db.isRecycleBinEnabled()) {
|
|
||||||
PwGroup recycleBin = database.getRecycleBin();
|
|
||||||
// Add trash if it doesn't exists
|
|
||||||
if (parent.equals(recycleBin)
|
|
||||||
&& mCurrentGroup != null
|
|
||||||
&& mCurrentGroup.getParent() == null
|
|
||||||
&& !mCurrentGroup.equals(recycleBin)) {
|
|
||||||
mAdapter.addNode(parent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mHandler.post(new UIToastTask(ListNodesActivity.this, "Unrecoverable error: " + mMessage));
|
|
||||||
App.setShutdown();
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
SaveDatabaseProgressTaskDialogFragment.stop(ListNodesActivity.this);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 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.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,141 +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.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentFilter;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.app.App;
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil;
|
|
||||||
import com.kunzisoft.keepass.stylish.StylishActivity;
|
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper;
|
|
||||||
|
|
||||||
public abstract class LockingActivity extends StylishActivity {
|
|
||||||
|
|
||||||
private static final String TAG = LockingActivity.class.getName();
|
|
||||||
|
|
||||||
public static final int RESULT_EXIT_LOCK = 1450;
|
|
||||||
|
|
||||||
private static final String AT_LEAST_SECOND_SHOWN_KEY = "AT_LEAST_SECOND_SHOWN_KEY";
|
|
||||||
|
|
||||||
private ScreenReceiver screenReceiver;
|
|
||||||
private boolean exitLock;
|
|
||||||
|
|
||||||
protected static void recordFirstTimeBeforeLaunch(Activity activity) {
|
|
||||||
TimeoutHelper.recordTime(activity);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static boolean checkTimeIsAllowedOrFinish(Activity activity) {
|
|
||||||
return TimeoutHelper.checkTime(activity);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(this)) {
|
|
||||||
screenReceiver = new ScreenReceiver();
|
|
||||||
registerReceiver(screenReceiver, new IntentFilter((Intent.ACTION_SCREEN_OFF)));
|
|
||||||
} else
|
|
||||||
screenReceiver = null;
|
|
||||||
|
|
||||||
exitLock = false;
|
|
||||||
|
|
||||||
// WARNING TODO recordTime is not called after a back if was in backstack
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void checkShutdown(Activity act) {
|
|
||||||
if (App.isShutdown() && App.getDB().getLoaded()) {
|
|
||||||
Log.i(TAG, "Shutdown " + act.getLocalClassName() +
|
|
||||||
" after inactivity or manual lock");
|
|
||||||
act.setResult(RESULT_EXIT_LOCK);
|
|
||||||
act.finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void lockAndExit() {
|
|
||||||
App.setShutdown();
|
|
||||||
setResult(LockingActivity.RESULT_EXIT_LOCK);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
|
||||||
if (resultCode == RESULT_EXIT_LOCK) {
|
|
||||||
exitLock = true;
|
|
||||||
checkShutdown(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
// After the first creation
|
|
||||||
// or If simply swipe with another application
|
|
||||||
// If the time is out -> close the Activity
|
|
||||||
TimeoutHelper.checkTime(this);
|
|
||||||
// If onCreate already record time
|
|
||||||
if (!exitLock)
|
|
||||||
TimeoutHelper.recordTime(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
// If the time is out during our navigation in activity -> close the Activity
|
|
||||||
TimeoutHelper.checkTime(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
if(screenReceiver != null)
|
|
||||||
unregisterReceiver(screenReceiver);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSaveInstanceState(Bundle outState) {
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
outState.putBoolean(AT_LEAST_SECOND_SHOWN_KEY, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ScreenReceiver extends BroadcastReceiver {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
|
|
||||||
if(intent.getAction() != null) {
|
|
||||||
if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
|
|
||||||
if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(LockingActivity.this)) {
|
|
||||||
App.setShutdown();
|
|
||||||
checkShutdown(LockingActivity.this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +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.ActivityNotFoundException;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.compat.BuildCompat;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Locking Hide Activity that sets FLAG_SECURE to prevent screenshots, and from
|
|
||||||
* appearing in the recent app preview
|
|
||||||
*/
|
|
||||||
public abstract class LockingHideActivity extends LockingActivity {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
// Several gingerbread devices have problems with FLAG_SECURE
|
|
||||||
int ver = BuildCompat.getSdkVersion();
|
|
||||||
if (ver >= BuildCompat.VERSION_CODE_ICE_CREAM_SANDWICH || ver < BuildCompat.VERSION_CODE_GINGERBREAD) {
|
|
||||||
getWindow().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
|
|
||||||
public void startActivity(Intent intent) {
|
|
||||||
try {
|
|
||||||
if (intent.getComponent() != null && intent.getComponent().getShortClassName().equals(".HtcLinkifyDispatcherActivity")) {
|
|
||||||
intent.setComponent(null);
|
|
||||||
}
|
|
||||||
super.startActivity(intent);
|
|
||||||
} catch (ActivityNotFoundException e) {
|
|
||||||
/* Catch the bad HTC implementation case */
|
|
||||||
super.startActivity(Intent.createChooser(intent, null));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,680 @@
|
|||||||
|
/*
|
||||||
|
* 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.Manifest
|
||||||
|
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.hardware.fingerprint.FingerprintManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.preference.PreferenceManager
|
||||||
|
import android.support.annotation.RequiresApi
|
||||||
|
import android.support.v7.app.AlertDialog
|
||||||
|
import android.support.v7.widget.Toolbar
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
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 com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.*
|
||||||
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
|
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.fingerprint.FingerPrintHelper
|
||||||
|
import com.kunzisoft.keepass.fingerprint.FingerPrintViewsManager
|
||||||
|
import com.kunzisoft.keepass.magikeyboard.KeyboardHelper
|
||||||
|
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.FingerPrintInfoView
|
||||||
|
import permissions.dispatcher.*
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
@RuntimePermissions
|
||||||
|
class PasswordActivity : StylishActivity(),
|
||||||
|
UriIntentInitTaskCallback {
|
||||||
|
|
||||||
|
// 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 fingerPrintInfoView: FingerPrintInfoView? = null
|
||||||
|
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
|
||||||
|
|
||||||
|
private var mDatabaseFileUri: Uri? = null
|
||||||
|
private var prefs: SharedPreferences? = null
|
||||||
|
|
||||||
|
private var mRememberKeyFile: Boolean = false
|
||||||
|
private var mKeyFileHelper: KeyFileHelper? = null
|
||||||
|
|
||||||
|
private var readOnly: Boolean = false
|
||||||
|
|
||||||
|
private var fingerPrintViewsManager: FingerPrintViewsManager? = 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)
|
||||||
|
fingerPrintInfoView = findViewById(R.id.fingerprint_info)
|
||||||
|
|
||||||
|
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
||||||
|
|
||||||
|
val browseView = findViewById<View>(R.id.browse_button)
|
||||||
|
mKeyFileHelper = KeyFileHelper(this@PasswordActivity)
|
||||||
|
browseView.setOnClickListener(mKeyFileHelper!!.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()
|
||||||
|
|
||||||
|
UriIntentInitTask(WeakReference(this), this, mRememberKeyFile)
|
||||||
|
.execute(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostInitTask(databaseFileUri: Uri?, keyFileUri: Uri?, errorStringId: Int?) {
|
||||||
|
mDatabaseFileUri = databaseFileUri
|
||||||
|
|
||||||
|
if (errorStringId != null) {
|
||||||
|
Toast.makeText(this@PasswordActivity, errorStringId, Toast.LENGTH_LONG).show()
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify permission to read file
|
||||||
|
if (databaseFileUri != null && !databaseFileUri.scheme!!.contains("content"))
|
||||||
|
doNothingWithPermissionCheck()
|
||||||
|
|
||||||
|
// Define title
|
||||||
|
val dbUriString = databaseFileUri?.toString() ?: ""
|
||||||
|
if (dbUriString.isNotEmpty()) {
|
||||||
|
if (PreferencesUtil.isFullFilePathEnable(this))
|
||||||
|
filenameView?.text = dbUriString
|
||||||
|
else
|
||||||
|
filenameView?.text = File(databaseFileUri!!.path!!).name // TODO Encapsulate
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = ""
|
||||||
|
if (isChecked) {
|
||||||
|
newDefaultFileName = databaseFileUri?.toString() ?: newDefaultFileName
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs?.edit()?.apply() {
|
||||||
|
putString(KEY_DEFAULT_FILENAME, newDefaultFileName)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
val backupManager = BackupManager(this@PasswordActivity)
|
||||||
|
backupManager.dataChanged()
|
||||||
|
}
|
||||||
|
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
|
||||||
|
|
||||||
|
// Retrieve settings for default database
|
||||||
|
val defaultFilename = prefs?.getString(KEY_DEFAULT_FILENAME, "")
|
||||||
|
if (databaseFileUri != null
|
||||||
|
&& databaseFileUri.path != null && databaseFileUri.path!!.isNotEmpty()
|
||||||
|
&& databaseFileUri == UriUtil.parseUriFile(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.isFingerprintEnable(this)) {
|
||||||
|
if (fingerPrintViewsManager == null) {
|
||||||
|
fingerPrintViewsManager = FingerPrintViewsManager(this,
|
||||||
|
databaseFileUri,
|
||||||
|
fingerPrintInfoView,
|
||||||
|
checkboxPasswordView,
|
||||||
|
enableButtonOnCheckedChangeListener,
|
||||||
|
passwordView) { passwordRetrieve ->
|
||||||
|
// Load the database if password is registered or retrieve
|
||||||
|
passwordRetrieve?.let {
|
||||||
|
// Retrieve from fingerprint
|
||||||
|
verifyKeyFileCheckboxAndLoadDatabase(it)
|
||||||
|
} ?: run {
|
||||||
|
// Register with fingerprint
|
||||||
|
verifyCheckboxesAndLoadDatabase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fingerPrintViewsManager?.initFingerprint()
|
||||||
|
// checks if fingerprint is available, will also start listening for fingerprints when available
|
||||||
|
fingerPrintViewsManager?.checkFingerprintAvailability()
|
||||||
|
fingerPrintInit = true
|
||||||
|
} else {
|
||||||
|
fingerPrintViewsManager?.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) {
|
||||||
|
fingerPrintViewsManager?.stopListening()
|
||||||
|
}
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
fingerPrintViewsManager?.destroy()
|
||||||
|
}
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyCheckboxesAndLoadDatabase(password: String? = passwordView?.text?.toString(),
|
||||||
|
keyFile: Uri? = UriUtil.parseUriFile(keyFileView?.text?.toString())) {
|
||||||
|
val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password
|
||||||
|
val keyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
|
||||||
|
loadDatabase(keyPassword, keyFileUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String? = passwordView?.text?.toString(),
|
||||||
|
keyFile: Uri? = UriUtil.parseUriFile(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?) {
|
||||||
|
|
||||||
|
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))
|
||||||
|
},
|
||||||
|
R.string.loading_database).start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after verify and try to opening the database
|
||||||
|
*/
|
||||||
|
private inner class AfterLoadingDatabase internal constructor(var database: Database,
|
||||||
|
val password: String?)
|
||||||
|
: ActionRunnable() {
|
||||||
|
|
||||||
|
override fun onFinishRun(result: Result) {
|
||||||
|
runOnUiThread {
|
||||||
|
// Recheck fingerprint if error
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
if (PreferencesUtil.isFingerprintEnable(this@PasswordActivity)) {
|
||||||
|
// Stay with the same mode
|
||||||
|
fingerPrintViewsManager?.reInitWithFingerprintMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.isSuccess) {
|
||||||
|
// Remove the password in view in all cases
|
||||||
|
removePassword()
|
||||||
|
|
||||||
|
if (database.validatePasswordEncoding(password)) {
|
||||||
|
launchGroupActivity()
|
||||||
|
} else {
|
||||||
|
PasswordEncodingDialogFragment().apply {
|
||||||
|
positiveButtonClickListener = DialogInterface.OnClickListener { _, _ ->
|
||||||
|
launchGroupActivity()
|
||||||
|
}
|
||||||
|
show(supportFragmentManager, "passwordEncodingTag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (result.message != null && result.message!!.isNotEmpty()) {
|
||||||
|
Toast.makeText(this@PasswordActivity, result.message, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchGroupActivity() {
|
||||||
|
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||||
|
{
|
||||||
|
GroupActivity.launch(this@PasswordActivity, readOnly)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
GroupActivity.launchForKeyboardSelection(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
|
||||||
|
fingerPrintViewsManager?.inflateOptionsMenu(inflater, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onCreateOptionsMenu(menu)
|
||||||
|
|
||||||
|
// Show education views
|
||||||
|
Handler().post { performedNextEducation(PasswordActivityEducation(this), menu) }
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
|
||||||
|
menu: Menu) {
|
||||||
|
if (toolbar != null
|
||||||
|
&& passwordActivityEducation.checkAndPerformedFingerprintUnlockEducation(
|
||||||
|
toolbar!!,
|
||||||
|
{
|
||||||
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
|
}))
|
||||||
|
else if (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)
|
||||||
|
}))
|
||||||
|
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||||
|
&& PreferencesUtil.isFingerprintEnable(applicationContext)
|
||||||
|
&& FingerPrintHelper.isFingerprintSupported(getSystemService(FingerprintManager::class.java))
|
||||||
|
&& fingerPrintInfoView != null && fingerPrintInfoView?.fingerPrintImageView != null
|
||||||
|
&& passwordActivityEducation.checkAndPerformedFingerprintEducation(fingerPrintInfoView?.fingerPrintImageView!!))
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
fingerPrintViewsManager?.deleteEntryKey()
|
||||||
|
}
|
||||||
|
else -> return MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
// NOTE: delegate the permission handling to generated method
|
||||||
|
onRequestPermissionsResult(requestCode, grantResults)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
mKeyFileHelper?.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NeedsPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
|
fun doNothing() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnShowRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
|
internal fun showRationaleForExternalStorage(request: PermissionRequest) {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setMessage(R.string.permission_external_storage_rationale_read_database)
|
||||||
|
.setPositiveButton(R.string.allow) { _, _ -> request.proceed() }
|
||||||
|
.setNegativeButton(R.string.cancel) { _, _ -> request.cancel() }
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnPermissionDenied(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
|
internal fun showDeniedForExternalStorage() {
|
||||||
|
Toast.makeText(this, R.string.permission_external_storage_denied, Toast.LENGTH_SHORT).show()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnNeverAskAgain(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
|
internal fun showNeverAskForExternalStorage() {
|
||||||
|
Toast.makeText(this, R.string.permission_external_storage_never_ask, Toast.LENGTH_SHORT).show()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val TAG = PasswordActivity::class.java.name
|
||||||
|
|
||||||
|
const val KEY_DEFAULT_FILENAME = "defaultFileName"
|
||||||
|
|
||||||
|
private const val KEY_PASSWORD = "password"
|
||||||
|
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||||
|
|
||||||
|
private fun buildAndLaunchIntent(activity: Activity, fileName: String, keyFile: String,
|
||||||
|
intentBuildLauncher: (Intent) -> Unit) {
|
||||||
|
val intent = Intent(activity, PasswordActivity::class.java)
|
||||||
|
intent.putExtra(UriIntentInitTask.KEY_FILENAME, fileName)
|
||||||
|
intent.putExtra(UriIntentInitTask.KEY_KEYFILE, keyFile)
|
||||||
|
intentBuildLauncher.invoke(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
private fun verifyFileNameUriFromLaunch(fileName: String) {
|
||||||
|
if (fileName.isEmpty()) {
|
||||||
|
throw FileNotFoundException()
|
||||||
|
}
|
||||||
|
|
||||||
|
val uri = UriUtil.parseUriFile(fileName)
|
||||||
|
val scheme = uri?.scheme
|
||||||
|
if (scheme != null && scheme.isNotEmpty() && scheme.equals("file", ignoreCase = true)) {
|
||||||
|
val dbFile = File(uri.path!!)
|
||||||
|
if (!dbFile.exists()) {
|
||||||
|
throw FileNotFoundException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Standard Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launch(
|
||||||
|
activity: Activity,
|
||||||
|
fileName: String,
|
||||||
|
keyFile: String) {
|
||||||
|
verifyFileNameUriFromLaunch(fileName)
|
||||||
|
buildAndLaunchIntent(activity, fileName, keyFile) { intent ->
|
||||||
|
activity.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Keyboard Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launchForKeyboardResult(
|
||||||
|
activity: Activity,
|
||||||
|
fileName: String,
|
||||||
|
keyFile: String) {
|
||||||
|
verifyFileNameUriFromLaunch(fileName)
|
||||||
|
|
||||||
|
buildAndLaunchIntent(activity, fileName, keyFile) { intent ->
|
||||||
|
KeyboardHelper.startActivityForKeyboardSelection(activity, intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Autofill Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launchForAutofillResult(
|
||||||
|
activity: Activity,
|
||||||
|
fileName: String,
|
||||||
|
keyFile: String,
|
||||||
|
assistStructure: AssistStructure?) {
|
||||||
|
verifyFileNameUriFromLaunch(fileName)
|
||||||
|
|
||||||
|
if (assistStructure != null) {
|
||||||
|
buildAndLaunchIntent(activity, fileName, keyFile) { intent ->
|
||||||
|
AutofillHelper.startActivityForAutofillResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
assistStructure)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
launch(activity, fileName, keyFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
/*
|
||||||
|
* 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 android.support.v4.app.DialogFragment
|
||||||
|
import android.support.v7.app.AlertDialog
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.KeyFileHelper
|
||||||
|
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 passView: TextView? = null
|
||||||
|
private var passConfView: TextView? = null
|
||||||
|
private var keyFileCheckBox: CompoundButton? = null
|
||||||
|
private var keyFileView: TextView? = null
|
||||||
|
|
||||||
|
private var mListener: AssignPasswordDialogListener? = null
|
||||||
|
|
||||||
|
private var mKeyFileHelper: KeyFileHelper? = null
|
||||||
|
|
||||||
|
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)
|
||||||
|
passView = rootView?.findViewById(R.id.pass_password)
|
||||||
|
passView?.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) {
|
||||||
|
passwordCheckBox?.isChecked = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
passConfView = rootView?.findViewById(R.id.pass_conf_password)
|
||||||
|
|
||||||
|
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
||||||
|
keyFileView = rootView?.findViewById(R.id.pass_keyfile)
|
||||||
|
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) {
|
||||||
|
keyFileCheckBox?.isChecked = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mKeyFileHelper = KeyFileHelper(this)
|
||||||
|
rootView?.findViewById<View>(R.id.browse_button)?.setOnClickListener { view ->
|
||||||
|
mKeyFileHelper?.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyPassword(): Boolean {
|
||||||
|
var error = false
|
||||||
|
if (passwordCheckBox != null
|
||||||
|
&& passwordCheckBox!!.isChecked
|
||||||
|
&& passView != null
|
||||||
|
&& passConfView != null) {
|
||||||
|
mMasterPassword = passView!!.text.toString()
|
||||||
|
val confPassword = passConfView!!.text.toString()
|
||||||
|
|
||||||
|
// Verify that passwords match
|
||||||
|
if (mMasterPassword != confPassword) {
|
||||||
|
error = true
|
||||||
|
// Passwords do not match
|
||||||
|
Toast.makeText(context, R.string.error_pass_match, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mMasterPassword == null || mMasterPassword!!.isEmpty()) {
|
||||||
|
error = true
|
||||||
|
showEmptyPasswordConfirmationDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyFile(): Boolean {
|
||||||
|
var error = false
|
||||||
|
if (keyFileCheckBox != null
|
||||||
|
&& keyFileCheckBox!!.isChecked) {
|
||||||
|
val keyFile = UriUtil.parseUriFile(keyFileView?.text?.toString())
|
||||||
|
mKeyFile = keyFile
|
||||||
|
|
||||||
|
// Verify that a keyfile is set
|
||||||
|
if (keyFile == null || keyFile.toString().isEmpty()) {
|
||||||
|
error = true
|
||||||
|
Toast.makeText(context, R.string.error_nokeyfile, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
|
||||||
|
mKeyFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
||||||
|
) { uri ->
|
||||||
|
UriUtil.parseUriFile(uri)?.let { pathUri ->
|
||||||
|
keyFileCheckBox?.isChecked = true
|
||||||
|
keyFileView?.text = pathUri.toString()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.Bundle
|
||||||
|
import android.support.v4.app.DialogFragment
|
||||||
|
import android.support.v7.app.AlertDialog
|
||||||
|
import android.widget.Button
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.utils.Util
|
||||||
|
|
||||||
|
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 market = root.findViewById<Button>(R.id.install_market)
|
||||||
|
market.setOnClickListener {
|
||||||
|
Util.gotoUrl(context!!, R.string.filemanager_play_store)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
val web = root.findViewById<Button>(R.id.install_web)
|
||||||
|
web.setOnClickListener {
|
||||||
|
Util.gotoUrl(context!!, R.string.filemanager_f_droid)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
/*
|
||||||
|
* 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.Activity
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Environment
|
||||||
|
import android.support.v4.app.DialogFragment
|
||||||
|
import android.support.v7.app.AlertDialog
|
||||||
|
import android.view.ActionMode
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.Spinner
|
||||||
|
import android.widget.TextView
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.FilePickerStylishActivity
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.nononsenseapps.filepicker.FilePickerActivity
|
||||||
|
import com.nononsenseapps.filepicker.Utils
|
||||||
|
|
||||||
|
class CreateFileDialogFragment : DialogFragment(), AdapterView.OnItemSelectedListener {
|
||||||
|
|
||||||
|
private val FILE_CODE = 3853
|
||||||
|
|
||||||
|
private var folderPathView: EditText? = null
|
||||||
|
private var fileNameView: EditText? = null
|
||||||
|
private var positiveButton: Button? = null
|
||||||
|
private var negativeButton: Button? = null
|
||||||
|
|
||||||
|
private var mDefinePathDialogListener: DefinePathDialogListener? = null
|
||||||
|
|
||||||
|
private var mDatabaseFileExtension: String? = null
|
||||||
|
private var mUriPath: Uri? = null
|
||||||
|
|
||||||
|
interface DefinePathDialogListener {
|
||||||
|
fun onDefinePathDialogPositiveClick(pathFile: Uri?): Boolean
|
||||||
|
fun onDefinePathDialogNegativeClick(pathFile: Uri?): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttach(activity: Context?) {
|
||||||
|
super.onAttach(activity)
|
||||||
|
try {
|
||||||
|
mDefinePathDialogListener = activity as DefinePathDialogListener?
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(activity?.toString()
|
||||||
|
+ " must implement " + DefinePathDialogListener::class.java.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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_file_creation, null)
|
||||||
|
builder.setView(rootView)
|
||||||
|
.setTitle(R.string.create_keepass_file)
|
||||||
|
// Add action buttons
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||||
|
.setNegativeButton(R.string.cancel) { _, _ -> }
|
||||||
|
|
||||||
|
// To prevent crash issue #69 https://github.com/Kunzisoft/KeePassDX/issues/69
|
||||||
|
val actionCopyBarCallback = object : ActionMode.Callback {
|
||||||
|
|
||||||
|
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
positiveButton?.isEnabled = false
|
||||||
|
negativeButton?.isEnabled = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyActionMode(mode: ActionMode) {
|
||||||
|
positiveButton?.isEnabled = true
|
||||||
|
negativeButton?.isEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Folder selection
|
||||||
|
val browseView = rootView.findViewById<View>(R.id.browse_button)
|
||||||
|
folderPathView = rootView.findViewById(R.id.folder_path)
|
||||||
|
folderPathView?.customSelectionActionModeCallback = actionCopyBarCallback
|
||||||
|
fileNameView = rootView.findViewById(R.id.filename)
|
||||||
|
fileNameView?.customSelectionActionModeCallback = actionCopyBarCallback
|
||||||
|
|
||||||
|
val defaultPath = Environment.getExternalStorageDirectory().path + getString(R.string.database_file_path_default)
|
||||||
|
folderPathView?.setText(defaultPath)
|
||||||
|
browseView.setOnClickListener { _ ->
|
||||||
|
Intent(context, FilePickerStylishActivity::class.java).apply {
|
||||||
|
putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
|
||||||
|
putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
|
||||||
|
putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
|
||||||
|
putExtra(FilePickerActivity.EXTRA_START_PATH,
|
||||||
|
Environment.getExternalStorageDirectory().path)
|
||||||
|
startActivityForResult(this, FILE_CODE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init path
|
||||||
|
mUriPath = null
|
||||||
|
|
||||||
|
// Extension
|
||||||
|
mDatabaseFileExtension = getString(R.string.database_file_extension_default)
|
||||||
|
val spinner = rootView.findViewById<Spinner>(R.id.file_types)
|
||||||
|
spinner.onItemSelectedListener = this
|
||||||
|
|
||||||
|
// Spinner Drop down elements
|
||||||
|
val fileTypes = resources.getStringArray(R.array.file_types)
|
||||||
|
val dataAdapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, fileTypes)
|
||||||
|
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
|
spinner.adapter = dataAdapter
|
||||||
|
// Or text if only one item https://github.com/Kunzisoft/KeePassDX/issues/105
|
||||||
|
if (fileTypes.size == 1) {
|
||||||
|
val params = spinner.layoutParams
|
||||||
|
spinner.visibility = View.GONE
|
||||||
|
val extensionTextView = TextView(context)
|
||||||
|
extensionTextView.text = mDatabaseFileExtension
|
||||||
|
extensionTextView.layoutParams = params
|
||||||
|
val parentView = spinner.parent as ViewGroup
|
||||||
|
parentView.addView(extensionTextView)
|
||||||
|
}
|
||||||
|
|
||||||
|
val dialog = builder.create()
|
||||||
|
|
||||||
|
dialog.setOnShowListener { _ ->
|
||||||
|
positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
|
||||||
|
negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
|
||||||
|
positiveButton?.setOnClickListener { _ ->
|
||||||
|
mDefinePathDialogListener?.let {
|
||||||
|
if (it.onDefinePathDialogPositiveClick(buildPath()))
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
negativeButton?.setOnClickListener { _->
|
||||||
|
mDefinePathDialogListener?.let {
|
||||||
|
if (it.onDefinePathDialogNegativeClick(buildPath())) {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildPath(): Uri? {
|
||||||
|
if (folderPathView != null && fileNameView != null && mDatabaseFileExtension != null) {
|
||||||
|
var path = Uri.Builder().path(folderPathView!!.text.toString())
|
||||||
|
.appendPath(fileNameView!!.text.toString() + mDatabaseFileExtension!!)
|
||||||
|
.build()
|
||||||
|
context?.let { context ->
|
||||||
|
path = UriUtil.translateUri(context, path)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
if (requestCode == FILE_CODE && resultCode == Activity.RESULT_OK) {
|
||||||
|
mUriPath = data?.data
|
||||||
|
mUriPath?.let {
|
||||||
|
val file = Utils.getFileForUri(it)
|
||||||
|
folderPathView?.setText(file.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemSelected(adapterView: AdapterView<*>, view: View, position: Int, id: Long) {
|
||||||
|
mDatabaseFileExtension = adapterView.getItemAtPosition(position).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNothingSelected(adapterView: AdapterView<*>) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* 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.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v4.app.DialogFragment
|
||||||
|
import android.support.v7.app.AlertDialog
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.fileselect.FileDatabaseModel
|
||||||
|
import java.text.DateFormat
|
||||||
|
|
||||||
|
class FileInformationDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
|
private var fileSizeContainerView: View? = null
|
||||||
|
private var fileModificationContainerView: View? = null
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
val inflater = activity.layoutInflater
|
||||||
|
val root = inflater.inflate(R.layout.fragment_file_selection_information, null)
|
||||||
|
val fileNameView = root.findViewById<TextView>(R.id.file_filename)
|
||||||
|
val filePathView = root.findViewById<TextView>(R.id.file_path)
|
||||||
|
fileSizeContainerView = root.findViewById(R.id.file_size_container)
|
||||||
|
val fileSizeView = root.findViewById<TextView>(R.id.file_size)
|
||||||
|
fileModificationContainerView = root.findViewById(R.id.file_modification_container)
|
||||||
|
val fileModificationView = root.findViewById<TextView>(R.id.file_modification)
|
||||||
|
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(FILE_SELECT_BEEN_ARG)) {
|
||||||
|
(getSerializable(FILE_SELECT_BEEN_ARG) as FileDatabaseModel?)?.let { fileDatabaseModel ->
|
||||||
|
fileDatabaseModel.fileUri?.let { fileUri ->
|
||||||
|
filePathView.text = Uri.decode(fileUri.toString())
|
||||||
|
}
|
||||||
|
fileNameView.text = fileDatabaseModel.fileName
|
||||||
|
|
||||||
|
if (fileDatabaseModel.notFound()) {
|
||||||
|
hideFileInfo()
|
||||||
|
} else {
|
||||||
|
showFileInfo()
|
||||||
|
fileSizeView.text = fileDatabaseModel.size.toString()
|
||||||
|
fileModificationView.text = DateFormat.getDateTimeInstance()
|
||||||
|
.format(fileDatabaseModel.lastModification)
|
||||||
|
}
|
||||||
|
} ?: hideFileInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setView(root)
|
||||||
|
builder.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showFileInfo() {
|
||||||
|
fileSizeContainerView?.visibility = View.VISIBLE
|
||||||
|
fileModificationContainerView?.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideFileInfo() {
|
||||||
|
fileSizeContainerView?.visibility = View.GONE
|
||||||
|
fileModificationContainerView?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val FILE_SELECT_BEEN_ARG = "FILE_SELECT_BEEN_ARG"
|
||||||
|
|
||||||
|
fun newInstance(fileDatabaseModel: FileDatabaseModel): FileInformationDialogFragment {
|
||||||
|
val fileInformationDialogFragment = FileInformationDialogFragment()
|
||||||
|
val args = Bundle()
|
||||||
|
args.putSerializable(FILE_SELECT_BEEN_ARG, fileDatabaseModel)
|
||||||
|
fileInformationDialogFragment.arguments = args
|
||||||
|
return fileInformationDialogFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
/*
|
||||||
|
* 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 android.support.v4.app.DialogFragment
|
||||||
|
import android.support.v7.app.AlertDialog
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.*
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.password.PasswordGenerator
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.applyFontVisibility
|
||||||
|
|
||||||
|
class GeneratePasswordDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
|
private var mListener: GeneratePasswordListener? = null
|
||||||
|
|
||||||
|
private var root: View? = null
|
||||||
|
private var lengthTextView: EditText? = 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)
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
|
val generator = PasswordGenerator(resources)
|
||||||
|
password = generator.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) {
|
||||||
|
Toast.makeText(context, R.string.error_wrong_length, Toast.LENGTH_LONG).show()
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 android.support.design.widget.TextInputLayout
|
||||||
|
import android.support.v4.app.DialogFragment
|
||||||
|
import android.support.v7.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 android.support.v4.app.DialogFragment
|
||||||
|
import android.support.v4.widget.ImageViewCompat
|
||||||
|
import android.support.v7.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,67 @@
|
|||||||
|
/*
|
||||||
|
* 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 android.support.v4.app.DialogFragment
|
||||||
|
import android.support.v7.app.AlertDialog
|
||||||
|
import android.view.View
|
||||||
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.utils.Util
|
||||||
|
|
||||||
|
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_setting_path1_text)
|
||||||
|
.setOnClickListener { launchActivateKeyboardSetting() }
|
||||||
|
rootView.findViewById<View>(R.id.keyboards_activate_setting_path2_text)
|
||||||
|
.setOnClickListener { launchActivateKeyboardSetting() }
|
||||||
|
|
||||||
|
val containerKeyboardSwitcher = rootView.findViewById<View>(R.id.container_keyboard_switcher)
|
||||||
|
if (BuildConfig.CLOSED_STORE) {
|
||||||
|
containerKeyboardSwitcher.setOnClickListener { Util.gotoUrl(context!!, R.string.keyboard_switcher_play_store) }
|
||||||
|
} else {
|
||||||
|
containerKeyboardSwitcher.setOnClickListener { Util.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 android.support.v4.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,75 @@
|
|||||||
|
/*
|
||||||
|
* 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.ActivityNotFoundException
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v4.app.DialogFragment
|
||||||
|
import android.support.v7.app.AlertDialog
|
||||||
|
import android.text.Html
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.widget.Toast
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.utils.Util
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
// TODO HtmlCompat with androidX
|
||||||
|
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_ad_free))).append("\n\n")
|
||||||
|
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_buy_pro)))
|
||||||
|
builder.setPositiveButton(R.string.download) { _, _ ->
|
||||||
|
try {
|
||||||
|
Util.gotoUrl(context!!, R.string.app_pro_url)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_feature_generosity))).append("\n\n")
|
||||||
|
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_donation)))
|
||||||
|
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||||
|
try {
|
||||||
|
Util.gotoUrl(context!!, R.string.contribution_url)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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,51 @@
|
|||||||
|
/*
|
||||||
|
* 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.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.preference.PreferenceManager
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
|
class ReadOnlyDialog(context: Context) : AlertDialog(context) {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle) {
|
||||||
|
val ctx = context
|
||||||
|
var warning = ctx.getString(R.string.read_only_warning)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
warning = warning + "\n\n" + context.getString(R.string.read_only_kitkat_warning)
|
||||||
|
}
|
||||||
|
setMessage(warning)
|
||||||
|
|
||||||
|
setButton(BUTTON_POSITIVE, ctx.getText(android.R.string.ok)) { _, _ -> dismiss() }
|
||||||
|
setButton(BUTTON_NEGATIVE, ctx.getText(R.string.beta_dontask)) { _, _ ->
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||||
|
val edit = prefs.edit()
|
||||||
|
edit.putBoolean(ctx.getString(R.string.show_read_only_warning), false)
|
||||||
|
edit.apply()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onCreate(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 android.support.annotation.IdRes
|
||||||
|
import android.support.v4.app.DialogFragment
|
||||||
|
import android.support.v7.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,125 @@
|
|||||||
|
/*
|
||||||
|
* 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.support.v4.app.DialogFragment
|
||||||
|
import android.support.v7.app.AlertDialog
|
||||||
|
import android.text.Html
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import android.widget.TextView
|
||||||
|
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(Html.fromHtml("<a href=\"https://source.android.com/setup/build-numbers\">CodeNames</a>"))
|
||||||
|
} 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,85 @@
|
|||||||
|
/*
|
||||||
|
* 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.ActivityNotFoundException
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v4.app.DialogFragment
|
||||||
|
import android.support.v7.app.AlertDialog
|
||||||
|
import android.text.Html
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.widget.Toast
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.utils.Util
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(Html.fromHtml(getString(R.string.html_text_dev_feature_thanks))).append("\n\n")
|
||||||
|
.append(Html.fromHtml(getString(R.string.html_rose))).append("\n\n")
|
||||||
|
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_work_hard))).append("\n")
|
||||||
|
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_upgrade))).append(" ")
|
||||||
|
builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() }
|
||||||
|
} else {
|
||||||
|
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_dev_feature))).append("\n\n")
|
||||||
|
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_buy_pro))).append("\n")
|
||||||
|
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_encourage)))
|
||||||
|
builder.setPositiveButton(R.string.download) { _, _ ->
|
||||||
|
try {
|
||||||
|
Util.gotoUrl(context!!, R.string.app_pro_url)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stringBuilder.append(Html.fromHtml(getString(R.string.html_text_dev_feature))).append("\n\n")
|
||||||
|
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_contibute))).append(" ")
|
||||||
|
.append(Html.fromHtml(getString(R.string.html_text_dev_feature_encourage)))
|
||||||
|
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||||
|
try {
|
||||||
|
Util.gotoUrl(context!!, R.string.contribution_url)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
Toast.makeText(context, R.string.error_failed_to_launch_link, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||||
|
}
|
||||||
|
builder.setMessage(stringBuilder)
|
||||||
|
// Create the AlertDialog object and return it
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.compat;
|
package com.kunzisoft.keepass.activities.helpers;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
@@ -25,9 +25,6 @@ import android.net.Uri;
|
|||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
public class ClipDataCompat {
|
public class ClipDataCompat {
|
||||||
private static Class clipData;
|
|
||||||
private static Class clipDataItem;
|
|
||||||
private static Class clipDescription;
|
|
||||||
private static Method getClipDataFromIntent;
|
private static Method getClipDataFromIntent;
|
||||||
private static Method getDescription;
|
private static Method getDescription;
|
||||||
private static Method getItemCount;
|
private static Method getItemCount;
|
||||||
@@ -39,14 +36,14 @@ public class ClipDataCompat {
|
|||||||
|
|
||||||
static {
|
static {
|
||||||
try {
|
try {
|
||||||
clipData = Class.forName("android.content.ClipData");
|
Class clipData = Class.forName("android.content.ClipData");
|
||||||
getDescription = clipData.getMethod("getDescription", (Class[])null);
|
getDescription = clipData.getMethod("getDescription", (Class[])null);
|
||||||
getItemCount = clipData.getMethod("getItemCount", (Class[])null);
|
getItemCount = clipData.getMethod("getItemCount", (Class[])null);
|
||||||
getItemAt = clipData.getMethod("getItemAt", new Class[]{int.class});
|
getItemAt = clipData.getMethod("getItemAt", new Class[]{int.class});
|
||||||
clipDescription = Class.forName("android.content.ClipDescription");
|
Class clipDescription = Class.forName("android.content.ClipDescription");
|
||||||
getLabel = clipDescription.getMethod("getLabel", (Class[])null);
|
getLabel = clipDescription.getMethod("getLabel", (Class[])null);
|
||||||
|
|
||||||
clipDataItem = Class.forName("android.content.ClipData$Item");
|
Class clipDataItem = Class.forName("android.content.ClipData$Item");
|
||||||
getUri = clipDataItem.getMethod("getUri", (Class[])null);
|
getUri = clipDataItem.getMethod("getUri", (Class[])null);
|
||||||
|
|
||||||
getClipDataFromIntent = Intent.class.getMethod("getClipData", (Class[])null);
|
getClipDataFromIntent = Intent.class.getMethod("getClipData", (Class[])null);
|
||||||
@@ -58,7 +55,6 @@ public class ClipDataCompat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Uri getUriFromIntent(Intent i, String key) {
|
public static Uri getUriFromIntent(Intent i, String key) {
|
||||||
boolean clipDataSucceeded = false;
|
|
||||||
if (initSucceded) {
|
if (initSucceded) {
|
||||||
try {
|
try {
|
||||||
Object clip = getClipDataFromIntent.invoke(i);
|
Object clip = getClipDataFromIntent.invoke(i);
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.helpers
|
||||||
|
|
||||||
|
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 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,232 @@
|
|||||||
|
/*
|
||||||
|
* 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 android.support.v4.app.Fragment
|
||||||
|
import android.support.v4.app.FragmentActivity
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
|
||||||
|
import com.kunzisoft.keepass.fileselect.StorageAF
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
|
class KeyFileHelper {
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if (activity != null && StorageAF.useStorageFramework(activity!!)) {
|
||||||
|
openActivityWithActionOpenDocument()
|
||||||
|
} else {
|
||||||
|
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(StorageAF.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 if (activity!!.fragmentManager != null)
|
||||||
|
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.parseUriFile(filename)
|
||||||
|
}
|
||||||
|
keyFileCallback?.invoke(keyUri)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
GET_CONTENT, OPEN_DOC -> {
|
||||||
|
if (resultCode == RESULT_OK) {
|
||||||
|
if (data != null) {
|
||||||
|
var uri = data.data
|
||||||
|
if (uri != null) {
|
||||||
|
if (StorageAF.useStorageFramework(activity!!)) {
|
||||||
|
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 = "KeyFileHelper"
|
||||||
|
|
||||||
|
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,88 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.helpers
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.AsyncTask
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.fileselect.database.FileDatabaseHistory
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
class UriIntentInitTask(private val weakContext: WeakReference<Context>,
|
||||||
|
private val uriIntentInitTaskCallback: UriIntentInitTaskCallback,
|
||||||
|
private val isKeyFileNeeded: Boolean)
|
||||||
|
: AsyncTask<Intent, Void, Int>() {
|
||||||
|
|
||||||
|
private var databaseUri: Uri? = null
|
||||||
|
private var keyFileUri: Uri? = null
|
||||||
|
|
||||||
|
override fun doInBackground(vararg args: Intent): Int? {
|
||||||
|
val intent = args[0]
|
||||||
|
val action = intent.action
|
||||||
|
|
||||||
|
// If is a view intent
|
||||||
|
if (action != null && action == VIEW_INTENT) {
|
||||||
|
val incoming = intent.data
|
||||||
|
databaseUri = incoming
|
||||||
|
keyFileUri = ClipDataCompat.getUriFromIntent(intent, KEY_KEYFILE)
|
||||||
|
|
||||||
|
if (incoming == null) {
|
||||||
|
return R.string.error_can_not_handle_uri
|
||||||
|
} else if (incoming.scheme == "file") {
|
||||||
|
val fileName = incoming.path
|
||||||
|
|
||||||
|
if (fileName?.isNotEmpty() == true) {
|
||||||
|
// No file name
|
||||||
|
return R.string.file_not_found
|
||||||
|
}
|
||||||
|
|
||||||
|
val dbFile = File(fileName)
|
||||||
|
if (!dbFile.exists()) {
|
||||||
|
// File does not exist
|
||||||
|
return R.string.file_not_found
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyFileUri == null) {
|
||||||
|
keyFileUri = getKeyFileUri(databaseUri)
|
||||||
|
}
|
||||||
|
} else if (incoming.scheme == "content") {
|
||||||
|
if (keyFileUri == null) {
|
||||||
|
keyFileUri = getKeyFileUri(databaseUri)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return R.string.error_can_not_handle_uri
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
databaseUri = UriUtil.parseUriFile(intent.getStringExtra(KEY_FILENAME))
|
||||||
|
keyFileUri = UriUtil.parseUriFile(intent.getStringExtra(KEY_KEYFILE))
|
||||||
|
|
||||||
|
if (keyFileUri == null || keyFileUri!!.toString().isEmpty()) {
|
||||||
|
keyFileUri = getKeyFileUri(databaseUri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
public override fun onPostExecute(result: Int?) {
|
||||||
|
uriIntentInitTaskCallback.onPostInitTask(databaseUri, keyFileUri, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getKeyFileUri(databaseUri: Uri?): Uri? {
|
||||||
|
return if (isKeyFileNeeded) {
|
||||||
|
FileDatabaseHistory.getInstance(weakContext).getKeyFileUriByDatabaseUri(databaseUri!!)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KEY_FILENAME = "fileName"
|
||||||
|
const val KEY_KEYFILE = "keyFile"
|
||||||
|
private const val VIEW_INTENT = "android.intent.action.VIEW"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2018 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
@@ -17,10 +17,10 @@
|
|||||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.password;
|
package com.kunzisoft.keepass.activities.helpers
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri
|
||||||
|
|
||||||
interface UriIntentInitTaskCallback {
|
interface UriIntentInitTaskCallback {
|
||||||
void onPostInitTask(Uri dbUri, Uri keyFileUri, Integer errorStringId);
|
fun onPostInitTask(databaseFileUri: Uri?, keyFileUri: Uri?, errorStringId: Int?)
|
||||||
}
|
}
|
||||||
@@ -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 timeoutEnable: Boolean = true
|
||||||
|
|
||||||
|
private var lockReceiver: LockReceiver? = null
|
||||||
|
private var exitLock: Boolean = false
|
||||||
|
|
||||||
|
// Force readOnly if Entry Selection mode
|
||||||
|
protected var readOnly: Boolean = false
|
||||||
|
get() {
|
||||||
|
return field || selectionMode
|
||||||
|
}
|
||||||
|
protected var selectionMode: Boolean = false
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
if (savedInstanceState != null
|
||||||
|
&& savedInstanceState.containsKey(TIMEOUT_ENABLE_KEY)) {
|
||||||
|
timeoutEnable = savedInstanceState.getBoolean(TIMEOUT_ENABLE_KEY)
|
||||||
|
} else {
|
||||||
|
if (intent != null)
|
||||||
|
timeoutEnable = intent.getBooleanExtra(TIMEOUT_ENABLE_KEY, TIMEOUT_ENABLE_KEY_DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeoutEnable) {
|
||||||
|
lockReceiver = LockReceiver()
|
||||||
|
val intentFilter = IntentFilter().apply {
|
||||||
|
addAction(Intent.ACTION_SCREEN_OFF)
|
||||||
|
addAction(LOCK_ACTION)
|
||||||
|
}
|
||||||
|
registerReceiver(lockReceiver, intentFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
exitLock = false
|
||||||
|
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState, intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
if (resultCode == RESULT_EXIT_LOCK) {
|
||||||
|
exitLock = true
|
||||||
|
if (Database.getInstance().loaded) {
|
||||||
|
lockAndExit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// To refresh when back to normal workflow from selection workflow
|
||||||
|
selectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(intent)
|
||||||
|
|
||||||
|
if (timeoutEnable) {
|
||||||
|
// 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 (!exitLock)
|
||||||
|
TimeoutHelper.recordTime(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
||||||
|
outState.putBoolean(TIMEOUT_ENABLE_KEY, timeoutEnable)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
|
||||||
|
if (timeoutEnable) {
|
||||||
|
// If the time is out during our navigation in activity -> close the Activity
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
if (lockReceiver != null)
|
||||||
|
unregisterReceiver(lockReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (timeoutEnable) {
|
||||||
|
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,49 @@
|
|||||||
|
/*
|
||||||
|
* 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 android.support.annotation.StyleRes
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
|
import com.nononsenseapps.filepicker.FilePickerActivity
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FilePickerActivity class with a style compatibility
|
||||||
|
*/
|
||||||
|
class FilePickerStylishActivity : FilePickerActivity() {
|
||||||
|
|
||||||
|
@StyleRes
|
||||||
|
private var themeId: Int = 0
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
this.themeId = Stylish.getFilePickerThemeId(this)
|
||||||
|
setTheme(themeId)
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
if (Stylish.getFilePickerThemeId(this) != this.themeId) {
|
||||||
|
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
|
||||||
|
this.recreate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* 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.support.annotation.StyleRes
|
||||||
|
import android.support.v7.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@StyleRes
|
||||||
|
fun getFilePickerThemeId(context: Context): Int {
|
||||||
|
return when {
|
||||||
|
themeString.equals(context.getString(R.string.list_style_name_dark)) -> R.style.KeepassDXStyle_FilePickerStyle_Dark
|
||||||
|
themeString.equals(context.getString(R.string.list_style_name_blue)) -> R.style.KeepassDXStyle_FilePickerStyle_Blue
|
||||||
|
themeString.equals(context.getString(R.string.list_style_name_red)) -> R.style.KeepassDXStyle_FilePickerStyle_Red
|
||||||
|
themeString.equals(context.getString(R.string.list_style_name_purple)) -> R.style.KeepassDXStyle_FilePickerStyle_Purple
|
||||||
|
else -> R.style.KeepassDXStyle_FilePickerStyle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 android.support.annotation.StyleRes
|
||||||
|
import android.support.v7.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,66 @@
|
|||||||
|
/*
|
||||||
|
* 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 android.support.annotation.StyleRes
|
||||||
|
import android.support.v4.app.Fragment
|
||||||
|
import android.support.v7.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)
|
||||||
|
if (context != null) {
|
||||||
|
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,150 @@
|
|||||||
|
/*
|
||||||
|
* 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.net.Uri
|
||||||
|
import android.support.annotation.ColorInt
|
||||||
|
import android.support.v7.widget.RecyclerView
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.*
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.fileselect.FileDatabaseModel
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|
||||||
|
class FileDatabaseHistoryAdapter(private val context: Context, private val listFiles: List<String>)
|
||||||
|
: RecyclerView.Adapter<FileDatabaseHistoryAdapter.FileDatabaseHistoryViewHolder>() {
|
||||||
|
|
||||||
|
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
|
private var fileItemOpenListener: FileItemOpenListener? = null
|
||||||
|
private var fileSelectClearListener: FileSelectClearListener? = null
|
||||||
|
private var fileInformationShowListener: FileInformationShowListener? = null
|
||||||
|
|
||||||
|
@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) {
|
||||||
|
val fileDatabaseModel = FileDatabaseModel(context, listFiles[position])
|
||||||
|
// Context menu creation
|
||||||
|
holder.fileContainer.setOnCreateContextMenuListener(ContextMenuBuilder(fileDatabaseModel))
|
||||||
|
// Click item to open file
|
||||||
|
if (fileItemOpenListener != null)
|
||||||
|
holder.fileContainer.setOnClickListener(FileItemClickListener(position))
|
||||||
|
// Assign file name
|
||||||
|
if (PreferencesUtil.isFullFilePathEnable(context))
|
||||||
|
holder.fileName.text = Uri.decode(fileDatabaseModel.fileUri.toString())
|
||||||
|
else
|
||||||
|
holder.fileName.text = fileDatabaseModel.fileName
|
||||||
|
holder.fileName.textSize = PreferencesUtil.getListTextSize(context)
|
||||||
|
// Click on information
|
||||||
|
if (fileInformationShowListener != null)
|
||||||
|
holder.fileInformation.setOnClickListener(FileInformationClickListener(fileDatabaseModel))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return listFiles.size
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOnItemClickListener(fileItemOpenListener: FileItemOpenListener) {
|
||||||
|
this.fileItemOpenListener = fileItemOpenListener
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFileSelectClearListener(fileSelectClearListener: FileSelectClearListener) {
|
||||||
|
this.fileSelectClearListener = fileSelectClearListener
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFileInformationShowListener(fileInformationShowListener: FileInformationShowListener) {
|
||||||
|
this.fileInformationShowListener = fileInformationShowListener
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileItemOpenListener {
|
||||||
|
fun onFileItemOpenListener(itemPosition: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileSelectClearListener {
|
||||||
|
fun onFileSelectClearListener(fileDatabaseModel: FileDatabaseModel): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileInformationShowListener {
|
||||||
|
fun onClickFileInformation(fileDatabaseModel: FileDatabaseModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class FileItemClickListener(private val position: Int) : View.OnClickListener {
|
||||||
|
|
||||||
|
override fun onClick(v: View) {
|
||||||
|
fileItemOpenListener?.onFileItemOpenListener(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class FileInformationClickListener(private val fileDatabaseModel: FileDatabaseModel) : View.OnClickListener {
|
||||||
|
|
||||||
|
override fun onClick(view: View) {
|
||||||
|
fileInformationShowListener?.onClickFileInformation(fileDatabaseModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ContextMenuBuilder(private val fileDatabaseModel: FileDatabaseModel) : View.OnCreateContextMenuListener {
|
||||||
|
|
||||||
|
private val mOnMyActionClickListener = MenuItem.OnMenuItemClickListener { item ->
|
||||||
|
if (fileSelectClearListener == null)
|
||||||
|
return@OnMenuItemClickListener false
|
||||||
|
when (item.itemId) {
|
||||||
|
MENU_CLEAR -> fileSelectClearListener!!.onFileSelectClearListener(fileDatabaseModel)
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateContextMenu(contextMenu: ContextMenu?, view: View?, contextMenuInfo: ContextMenu.ContextMenuInfo?) {
|
||||||
|
contextMenu?.add(Menu.NONE, MENU_CLEAR, Menu.NONE, R.string.remove_from_filelist)
|
||||||
|
?.setOnMenuItemClickListener(mOnMyActionClickListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
|
var fileContainer: View = itemView.findViewById(R.id.file_container)
|
||||||
|
var fileName: TextView = itemView.findViewById(R.id.file_filename)
|
||||||
|
var fileInformation: ImageView = itemView.findViewById(R.id.file_information)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val MENU_CLEAR = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,304 +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.view.ContextMenu;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R;
|
|
||||||
import com.kunzisoft.keepass.app.App;
|
|
||||||
import com.kunzisoft.keepass.database.PwGroup;
|
|
||||||
import com.kunzisoft.keepass.database.PwNode;
|
|
||||||
import com.kunzisoft.keepass.database.SortNodeEnum;
|
|
||||||
import com.kunzisoft.keepass.icons.IconPackChooser;
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil;
|
|
||||||
|
|
||||||
public class NodeAdapter extends RecyclerView.Adapter<BasicViewHolder> {
|
|
||||||
|
|
||||||
private SortedList<PwNode> nodeSortedList;
|
|
||||||
|
|
||||||
private Context context;
|
|
||||||
private LayoutInflater inflater;
|
|
||||||
private float textSize;
|
|
||||||
private SortNodeEnum listSort;
|
|
||||||
private boolean groupsBeforeSort;
|
|
||||||
private boolean ascendingSort;
|
|
||||||
|
|
||||||
private OnNodeClickCallback onNodeClickCallback;
|
|
||||||
private NodeMenuListener nodeMenuListener;
|
|
||||||
private boolean activateContextMenu;
|
|
||||||
|
|
||||||
private int iconGroupColor;
|
|
||||||
private int iconEntryColor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create node list adapter with contextMenu or not
|
|
||||||
* @param context Context to use
|
|
||||||
*/
|
|
||||||
public NodeAdapter(final Context context) {
|
|
||||||
this.inflater = LayoutInflater.from(context);
|
|
||||||
this.context = context;
|
|
||||||
this.textSize = PreferencesUtil.getListTextSize(context);
|
|
||||||
this.listSort = PreferencesUtil.getListSort(context);
|
|
||||||
this.groupsBeforeSort = PreferencesUtil.getGroupsBeforeSort(context);
|
|
||||||
this.ascendingSort = PreferencesUtil.getAscendingSort(context);
|
|
||||||
this.activateContextMenu = 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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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 setActivateContextMenu(boolean activate) {
|
|
||||||
this.activateContextMenu = activate;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rebuild the list by clear and build children from the group
|
|
||||||
*/
|
|
||||||
public void rebuildList(PwGroup group) {
|
|
||||||
this.nodeSortedList.clear();
|
|
||||||
if (group != null) {
|
|
||||||
this.nodeSortedList.addAll(group.getDirectChildren());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
if (IconPackChooser.getSelectedIconPack(context).tintable()) {
|
|
||||||
int iconColor = Color.BLACK;
|
|
||||||
switch (subNode.getType()) {
|
|
||||||
case GROUP:
|
|
||||||
iconColor = iconGroupColor;
|
|
||||||
break;
|
|
||||||
case ENTRY:
|
|
||||||
iconColor = iconEntryColor;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
App.getDB().getDrawFactory().assignDatabaseIconTo(context, holder.icon, subNode.getIcon(), true, iconColor);
|
|
||||||
} else {
|
|
||||||
App.getDB().getDrawFactory().assignDatabaseIconTo(context, holder.icon, subNode.getIcon());
|
|
||||||
}
|
|
||||||
// Assign text
|
|
||||||
holder.text.setText(subNode.getDisplayTitle());
|
|
||||||
// Assign click
|
|
||||||
holder.container.setOnClickListener(
|
|
||||||
new OnNodeClickListener(subNode));
|
|
||||||
// Context menu
|
|
||||||
if (activateContextMenu) {
|
|
||||||
holder.container.setOnCreateContextMenuListener(
|
|
||||||
new ContextMenuBuilder(subNode, nodeMenuListener));
|
|
||||||
}
|
|
||||||
// Assign text size
|
|
||||||
holder.text.setTextSize(textSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return nodeSortedList.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assign a listener when a node is clicked
|
|
||||||
*/
|
|
||||||
public void setOnNodeClickListener(OnNodeClickCallback onNodeClickCallback) {
|
|
||||||
this.onNodeClickCallback = onNodeClickCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 OnNodeClickCallback {
|
|
||||||
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 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 (onNodeClickCallback != null)
|
|
||||||
onNodeClickCallback.onNodeClick(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility class for menu listener
|
|
||||||
*/
|
|
||||||
private class ContextMenuBuilder implements View.OnCreateContextMenuListener {
|
|
||||||
|
|
||||||
private static final int MENU_OPEN = Menu.FIRST;
|
|
||||||
private static final int MENU_EDIT = MENU_OPEN + 1;
|
|
||||||
private static final int MENU_DELETE = MENU_EDIT + 1;
|
|
||||||
|
|
||||||
private PwNode node;
|
|
||||||
private NodeMenuListener menuListener;
|
|
||||||
|
|
||||||
ContextMenuBuilder(PwNode node, NodeMenuListener menuListener) {
|
|
||||||
this.menuListener = menuListener;
|
|
||||||
this.node = node;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateContextMenu(ContextMenu contextMenu, View view, ContextMenu.ContextMenuInfo contextMenuInfo) {
|
|
||||||
MenuItem clearMenu = contextMenu.add(Menu.NONE, MENU_OPEN, Menu.NONE, R.string.menu_open);
|
|
||||||
clearMenu.setOnMenuItemClickListener(mOnMyActionClickListener);
|
|
||||||
if (!App.getDB().isReadOnly() && !node.equals(App.getDB().getPwDatabase().getRecycleBin())) {
|
|
||||||
// Edition
|
|
||||||
clearMenu = contextMenu.add(Menu.NONE, MENU_EDIT, Menu.NONE, R.string.menu_edit);
|
|
||||||
clearMenu.setOnMenuItemClickListener(mOnMyActionClickListener);
|
|
||||||
// Deletion
|
|
||||||
clearMenu = contextMenu.add(Menu.NONE, MENU_DELETE, Menu.NONE, R.string.menu_delete);
|
|
||||||
clearMenu.setOnMenuItemClickListener(mOnMyActionClickListener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private MenuItem.OnMenuItemClickListener mOnMyActionClickListener = new MenuItem.OnMenuItemClickListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onMenuItemClick(MenuItem item) {
|
|
||||||
if (menuListener == null)
|
|
||||||
return false;
|
|
||||||
switch ( item.getItemId() ) {
|
|
||||||
case MENU_OPEN:
|
|
||||||
return menuListener.onOpenMenuClick(node);
|
|
||||||
case MENU_EDIT:
|
|
||||||
return menuListener.onEditMenuClick(node);
|
|
||||||
case MENU_DELETE:
|
|
||||||
return menuListener.onDeleteMenuClick(node);
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
392
app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt
Normal file
392
app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
/*
|
||||||
|
* 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 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.*
|
||||||
|
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 textSize: Float = 0.toFloat()
|
||||||
|
private var subtextSize: Float = 0.toFloat()
|
||||||
|
private var infoTextSize: Float = 0.toFloat()
|
||||||
|
private var iconSize: Float = 0.toFloat()
|
||||||
|
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() {
|
||||||
|
val textSizeDefault = java.lang.Float.parseFloat(context.getString(R.string.list_size_default))
|
||||||
|
this.textSize = PreferencesUtil.getListTextSize(context)
|
||||||
|
this.subtextSize = context.resources.getInteger(R.integer.list_small_size_default) * textSize / textSizeDefault
|
||||||
|
this.infoTextSize = context.resources.getInteger(R.integer.list_tiny_size_default) * textSize / textSizeDefault
|
||||||
|
// Retrieve the icon size
|
||||||
|
val iconDefaultSize = context.resources.getDimension(R.dimen.list_icon_size_default)
|
||||||
|
this.iconSize = iconDefaultSize * textSize / textSizeDefault
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
|
||||||
|
// Assign text
|
||||||
|
holder.text.text = subNode.title
|
||||||
|
// Assign click
|
||||||
|
holder.container.setOnClickListener { nodeClickCallback?.onNodeClick(subNode) }
|
||||||
|
// Context menu
|
||||||
|
if (activateContextMenu) {
|
||||||
|
holder.container.setOnCreateContextMenuListener(
|
||||||
|
ContextMenuBuilder(menuInflater, subNode, readOnly, isASearchResult, nodeMenuListener))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add username
|
||||||
|
holder.subText.text = ""
|
||||||
|
holder.subText.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()) {
|
||||||
|
holder.subText.visibility = View.VISIBLE
|
||||||
|
holder.subText.text = username
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabase.stopManageEntry(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign image and text size
|
||||||
|
// Relative size of the icon
|
||||||
|
holder.icon.layoutParams?.height = iconSize.toInt()
|
||||||
|
holder.icon.layoutParams?.width = iconSize.toInt()
|
||||||
|
holder.text.textSize = textSize
|
||||||
|
holder.subText.textSize = subtextSize
|
||||||
|
if (subNode.type == Type.GROUP) {
|
||||||
|
if (showNumberEntries) {
|
||||||
|
holder.numberChildren?.apply {
|
||||||
|
text = (subNode as GroupVersioned).getChildEntries(true).size.toString()
|
||||||
|
textSize = infoTextSize
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 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.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)
|
||||||
|
: 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.
|
* This file is part of KeePass DX.
|
||||||
*
|
*
|
||||||
@@ -17,20 +17,23 @@
|
|||||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
* 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 android.support.multidex.MultiDexApplication
|
||||||
import android.widget.ImageView;
|
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||||
import android.widget.TextView;
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R;
|
class App : MultiDexApplication() {
|
||||||
|
|
||||||
class EntryViewHolder extends BasicViewHolder {
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
EntryViewHolder(View itemView) {
|
Stylish.init(this)
|
||||||
super(itemView);
|
PRNGFixes.apply()
|
||||||
container = itemView.findViewById(R.id.entry_container);
|
}
|
||||||
icon = (ImageView) itemView.findViewById(R.id.entry_icon);
|
|
||||||
text = (TextView) itemView.findViewById(R.id.entry_text);
|
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
|
* 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.Build;
|
||||||
import android.os.Process;
|
import android.os.Process;
|
||||||
|
|
||||||
import com.kunzisoft.keepass.utils.StrUtil;
|
import com.kunzisoft.keepass.utils.StringUtil;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
@@ -44,7 +44,6 @@ public final class PRNGFixes {
|
|||||||
|
|
||||||
private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL =
|
private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL =
|
||||||
getBuildFingerprintAndDeviceSerial();
|
getBuildFingerprintAndDeviceSerial();
|
||||||
private static int sdkVersion = BuildCompat.getSdkVersion();
|
|
||||||
|
|
||||||
/** Hidden constructor to prevent instantiation. */
|
/** Hidden constructor to prevent instantiation. */
|
||||||
private PRNGFixes() {}
|
private PRNGFixes() {}
|
||||||
@@ -67,11 +66,11 @@ public final class PRNGFixes {
|
|||||||
|
|
||||||
private static boolean supportedOnThisDevice() {
|
private static boolean supportedOnThisDevice() {
|
||||||
// Blacklist on samsung devices
|
// Blacklist on samsung devices
|
||||||
if (StrUtil.indexOfIgnoreCase(BuildCompat.getManufacturer(), "samsung", Locale.ENGLISH) >= 0) {
|
if (StringUtil.INSTANCE.indexOfIgnoreCase(Build.MANUFACTURER, "samsung", Locale.ENGLISH) >= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sdkVersion > BuildCompat.VERSION_CODE_JELLY_BEAN_MR2) {
|
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,8 +125,8 @@ public final class PRNGFixes {
|
|||||||
* @throws SecurityException if the fix is needed but could not be applied.
|
* @throws SecurityException if the fix is needed but could not be applied.
|
||||||
*/
|
*/
|
||||||
private static void applyOpenSSLFix() throws SecurityException {
|
private static void applyOpenSSLFix() throws SecurityException {
|
||||||
if ((sdkVersion < BuildCompat.VERSION_CODE_JELLY_BEAN)
|
if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
|
||||||
|| (sdkVersion > BuildCompat.VERSION_CODE_JELLY_BEAN_MR2)) {
|
|| (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2)) {
|
||||||
// No need to apply the fix
|
// No need to apply the fix
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -162,7 +161,7 @@ public final class PRNGFixes {
|
|||||||
*/
|
*/
|
||||||
private static void installLinuxPRNGSecureRandom()
|
private static void installLinuxPRNGSecureRandom()
|
||||||
throws SecurityException {
|
throws SecurityException {
|
||||||
if (sdkVersion > BuildCompat.VERSION_CODE_JELLY_BEAN_MR2) {
|
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||||
// No need to apply the fix
|
// No need to apply the fix
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 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.autofill;
|
|
||||||
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.app.assist.AssistStructure;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentSender;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.support.annotation.RequiresApi;
|
|
||||||
import android.support.v7.app.AppCompatActivity;
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.fileselect.FileSelectActivity;
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
public class AutoFillAuthActivity extends AppCompatActivity {
|
|
||||||
|
|
||||||
private AutofillHelper autofillHelper;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
autofillHelper = new AutofillHelper();
|
|
||||||
startFileSelectActivity();
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IntentSender getAuthIntentSenderForResponse(Context context) {
|
|
||||||
final Intent intent = new Intent(context, AutoFillAuthActivity.class);
|
|
||||||
return PendingIntent.getActivity(context, 0,
|
|
||||||
intent, PendingIntent.FLAG_CANCEL_CURRENT).getIntentSender();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void startFileSelectActivity() {
|
|
||||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
|
||||||
AssistStructure assistStructure = autofillHelper.retrieveAssistStructure(getIntent());
|
|
||||||
if (assistStructure != null) {
|
|
||||||
FileSelectActivity.launch(this, assistStructure);
|
|
||||||
} else {
|
|
||||||
setResult(RESULT_CANCELED);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,174 +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.autofill;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.assist.AssistStructure;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.service.autofill.Dataset;
|
|
||||||
import android.service.autofill.FillResponse;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.support.annotation.RequiresApi;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.autofill.AutofillId;
|
|
||||||
import android.view.autofill.AutofillManager;
|
|
||||||
import android.view.autofill.AutofillValue;
|
|
||||||
import android.widget.RemoteViews;
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R;
|
|
||||||
import com.kunzisoft.keepass.database.PwEntry;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
public class AutofillHelper {
|
|
||||||
|
|
||||||
public static final int AUTOFILL_RESPONSE_REQUEST_CODE = 8165;
|
|
||||||
|
|
||||||
private AssistStructure assistStructure = null;
|
|
||||||
|
|
||||||
public AssistStructure retrieveAssistStructure(Intent intent) {
|
|
||||||
if (intent != null && intent.getExtras() != null) {
|
|
||||||
assistStructure = intent.getParcelableExtra(android.view.autofill.AutofillManager.EXTRA_ASSIST_STRUCTURE);
|
|
||||||
}
|
|
||||||
return assistStructure;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call retrieveAssistStructure before
|
|
||||||
*/
|
|
||||||
public AssistStructure getAssistStructure() {
|
|
||||||
return assistStructure;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void addAssistStructureExtraInIntent(Intent intent, AssistStructure assistStructure) {
|
|
||||||
if (assistStructure != null) {
|
|
||||||
intent.putExtra(android.view.autofill.AutofillManager.EXTRA_ASSIST_STRUCTURE, assistStructure);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Define if android.view.autofill.AutofillManager.EXTRA_ASSIST_STRUCTURE is an extra bundle key present in the Intent
|
|
||||||
*/
|
|
||||||
public static boolean isIntentContainsExtraAssistStructureKey(Intent intent) {
|
|
||||||
return (intent != null
|
|
||||||
&& intent.getExtras() != null
|
|
||||||
&& intent.getExtras().containsKey(android.view.autofill.AutofillManager.EXTRA_ASSIST_STRUCTURE));
|
|
||||||
}
|
|
||||||
|
|
||||||
private @Nullable Dataset buildDataset(Context context, PwEntry entry,
|
|
||||||
StructureParser.Result struct) {
|
|
||||||
String title = makeEntryTitle(entry);
|
|
||||||
RemoteViews views = newRemoteViews(context.getPackageName(), title);
|
|
||||||
Dataset.Builder builder = new Dataset.Builder(views);
|
|
||||||
builder.setId(entry.getUUID().toString());
|
|
||||||
|
|
||||||
if (entry.getPassword() != null) {
|
|
||||||
AutofillValue value = AutofillValue.forText(entry.getPassword());
|
|
||||||
struct.password.forEach(id -> builder.setValue(id, value));
|
|
||||||
}
|
|
||||||
if (entry.getUsername() != null) {
|
|
||||||
AutofillValue value = AutofillValue.forText(entry.getUsername());
|
|
||||||
List<AutofillId> ids = new ArrayList<>(struct.username);
|
|
||||||
if (entry.getUsername().contains("@") || struct.username.isEmpty())
|
|
||||||
ids.addAll(struct.email);
|
|
||||||
ids.forEach(id -> builder.setValue(id, value));
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return builder.build();
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// if not value be set
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static private String makeEntryTitle(PwEntry entry) {
|
|
||||||
if (!entry.getTitle().isEmpty() && !entry.getUsername().isEmpty())
|
|
||||||
return String.format("%s (%s)", entry.getTitle(), entry.getUsername());
|
|
||||||
if (!entry.getTitle().isEmpty())
|
|
||||||
return entry.getTitle();
|
|
||||||
if (!entry.getUsername().isEmpty())
|
|
||||||
return entry.getUsername();
|
|
||||||
if (!entry.getNotes().isEmpty())
|
|
||||||
return entry.getNotes().trim();
|
|
||||||
return ""; // TODO No title
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to hit when right key is selected
|
|
||||||
*/
|
|
||||||
public void buildResponseWhenEntrySelected(Activity activity, PwEntry entry) {
|
|
||||||
Intent mReplyIntent;
|
|
||||||
Intent intent = activity.getIntent();
|
|
||||||
if (isIntentContainsExtraAssistStructureKey(intent)) {
|
|
||||||
AssistStructure structure = intent.getParcelableExtra(android.view.autofill.AutofillManager.EXTRA_ASSIST_STRUCTURE);
|
|
||||||
StructureParser.Result result = new StructureParser(structure).parse();
|
|
||||||
|
|
||||||
// New Response
|
|
||||||
FillResponse.Builder responseBuilder = new FillResponse.Builder();
|
|
||||||
Dataset dataset = buildDataset(activity, entry, result);
|
|
||||||
responseBuilder.addDataset(dataset);
|
|
||||||
mReplyIntent = new Intent();
|
|
||||||
Log.d(activity.getClass().getName(), "Successed Autofill auth.");
|
|
||||||
mReplyIntent.putExtra(
|
|
||||||
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
|
||||||
responseBuilder.build());
|
|
||||||
activity.setResult(Activity.RESULT_OK, mReplyIntent);
|
|
||||||
} else {
|
|
||||||
Log.w(activity.getClass().getName(), "Failed Autofill auth.");
|
|
||||||
activity.setResult(Activity.RESULT_CANCELED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility method to loop and close each activity with return data
|
|
||||||
*/
|
|
||||||
public static void onActivityResultSetResult(Activity activity, int requestCode, int resultCode, Intent data) {
|
|
||||||
if (requestCode == AUTOFILL_RESPONSE_REQUEST_CODE) {
|
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
|
||||||
activity.setResult(resultCode, data);
|
|
||||||
}
|
|
||||||
if (resultCode == Activity.RESULT_CANCELED) {
|
|
||||||
activity.setResult(Activity.RESULT_CANCELED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility method to loop and close each activity with return data
|
|
||||||
*/
|
|
||||||
public static void onActivityResultSetResultAndFinish(Activity activity, int requestCode, int resultCode, Intent data) {
|
|
||||||
if (requestCode == AUTOFILL_RESPONSE_REQUEST_CODE) {
|
|
||||||
onActivityResultSetResult(activity, requestCode, resultCode, data);
|
|
||||||
activity.finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static RemoteViews newRemoteViews(String packageName, String remoteViewsText) {
|
|
||||||
RemoteViews presentation =
|
|
||||||
new RemoteViews(packageName, R.layout.autofill_service_list_item);
|
|
||||||
presentation.setTextViewText(R.id.text, remoteViewsText);
|
|
||||||
return presentation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
* 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.autofill
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.assist.AssistStructure
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.service.autofill.Dataset
|
||||||
|
import android.service.autofill.FillResponse
|
||||||
|
import android.support.annotation.RequiresApi
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.autofill.AutofillManager
|
||||||
|
import android.view.autofill.AutofillValue
|
||||||
|
import android.widget.RemoteViews
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
|
object AutofillHelper {
|
||||||
|
|
||||||
|
private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165
|
||||||
|
|
||||||
|
private const val ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
|
||||||
|
|
||||||
|
fun retrieveAssistStructure(intent: Intent?): AssistStructure? {
|
||||||
|
intent?.let {
|
||||||
|
return it.getParcelableExtra(ASSIST_STRUCTURE)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeEntryTitle(entryInfo: EntryInfo): String {
|
||||||
|
if (entryInfo.title.isNotEmpty() && entryInfo.username.isNotEmpty())
|
||||||
|
return String.format("%s (%s)", entryInfo.title, entryInfo.username)
|
||||||
|
if (entryInfo.title.isNotEmpty())
|
||||||
|
return entryInfo.title
|
||||||
|
if (entryInfo.username.isNotEmpty())
|
||||||
|
return entryInfo.username
|
||||||
|
if (entryInfo.url.isNotEmpty())
|
||||||
|
return entryInfo.url
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildDataset(context: Context,
|
||||||
|
entryInfo: EntryInfo,
|
||||||
|
struct: StructureParser.Result): Dataset? {
|
||||||
|
val title = makeEntryTitle(entryInfo)
|
||||||
|
val views = newRemoteViews(context.packageName, title)
|
||||||
|
val builder = Dataset.Builder(views)
|
||||||
|
builder.setId(entryInfo.id)
|
||||||
|
|
||||||
|
struct.password.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.password)) }
|
||||||
|
|
||||||
|
val ids = ArrayList(struct.username)
|
||||||
|
if (entryInfo.username.contains("@") || struct.username.isEmpty())
|
||||||
|
ids.addAll(struct.email)
|
||||||
|
ids.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.username)) }
|
||||||
|
|
||||||
|
return try {
|
||||||
|
builder.build()
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
// if not value be set
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to hit when right key is selected
|
||||||
|
*/
|
||||||
|
fun buildResponseWhenEntrySelected(activity: Activity, entryInfo: EntryInfo) {
|
||||||
|
var setResultOk = false
|
||||||
|
activity.intent?.extras?.let { extras ->
|
||||||
|
if (extras.containsKey(ASSIST_STRUCTURE)) {
|
||||||
|
activity.intent?.getParcelableExtra<AssistStructure>(ASSIST_STRUCTURE)?.let { structure ->
|
||||||
|
StructureParser(structure).parse()?.let { result ->
|
||||||
|
// New Response
|
||||||
|
val responseBuilder = FillResponse.Builder()
|
||||||
|
val dataset = buildDataset(activity, entryInfo, result)
|
||||||
|
responseBuilder.addDataset(dataset)
|
||||||
|
val mReplyIntent = Intent()
|
||||||
|
Log.d(activity.javaClass.name, "Successed Autofill auth.")
|
||||||
|
mReplyIntent.putExtra(
|
||||||
|
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
||||||
|
responseBuilder.build())
|
||||||
|
setResultOk = true
|
||||||
|
activity.setResult(Activity.RESULT_OK, mReplyIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!setResultOk) {
|
||||||
|
Log.w(activity.javaClass.name, "Failed Autofill auth.")
|
||||||
|
activity.setResult(Activity.RESULT_CANCELED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to start an activity with an Autofill for result
|
||||||
|
*/
|
||||||
|
fun startActivityForAutofillResult(activity: Activity, intent: Intent, assistStructure: AssistStructure) {
|
||||||
|
EntrySelectionHelper.addEntrySelectionModeExtraInIntent(intent)
|
||||||
|
intent.putExtra(ASSIST_STRUCTURE, assistStructure)
|
||||||
|
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to loop and close each activity with return data
|
||||||
|
*/
|
||||||
|
fun onActivityResultSetResultAndFinish(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
if (requestCode == AUTOFILL_RESPONSE_REQUEST_CODE) {
|
||||||
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
|
activity.setResult(resultCode, data)
|
||||||
|
}
|
||||||
|
if (resultCode == Activity.RESULT_CANCELED) {
|
||||||
|
activity.setResult(Activity.RESULT_CANCELED)
|
||||||
|
}
|
||||||
|
activity.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newRemoteViews(packageName: String, remoteViewsText: String): RemoteViews {
|
||||||
|
val presentation = RemoteViews(packageName, R.layout.item_autofill_service)
|
||||||
|
presentation.setTextViewText(R.id.text, remoteViewsText)
|
||||||
|
return presentation
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* 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.autofill
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentSender
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.annotation.RequiresApi
|
||||||
|
import android.support.v7.app.AppCompatActivity
|
||||||
|
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||||
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
|
class AutofillLauncherActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
||||||
|
val assistStructure = AutofillHelper.retrieveAssistStructure(intent)
|
||||||
|
if (assistStructure != null) {
|
||||||
|
if (Database.getInstance().loaded && TimeoutHelper.checkTime(this))
|
||||||
|
GroupActivity.launchForAutofillResult(this, assistStructure, PreferencesUtil.enableReadOnlyDatabase(this))
|
||||||
|
else {
|
||||||
|
FileDatabaseSelectActivity.launchForAutofillResult(this, assistStructure)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun getAuthIntentSenderForResponse(context: Context): IntentSender {
|
||||||
|
val intent = Intent(context, AutofillLauncherActivity::class.java)
|
||||||
|
return PendingIntent.getActivity(context, 0,
|
||||||
|
intent, PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 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.autofill;
|
|
||||||
|
|
||||||
import android.app.assist.AssistStructure;
|
|
||||||
import android.content.IntentSender;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.CancellationSignal;
|
|
||||||
import android.service.autofill.AutofillService;
|
|
||||||
import android.service.autofill.FillCallback;
|
|
||||||
import android.service.autofill.FillContext;
|
|
||||||
import android.service.autofill.FillRequest;
|
|
||||||
import android.service.autofill.FillResponse;
|
|
||||||
import android.service.autofill.SaveCallback;
|
|
||||||
import android.service.autofill.SaveRequest;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.RequiresApi;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.autofill.AutofillId;
|
|
||||||
import android.widget.RemoteViews;
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
public class KeeAutofillService extends AutofillService {
|
|
||||||
private static final String TAG = "KeeAutofillService";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFillRequest(@NonNull FillRequest request, @NonNull CancellationSignal cancellationSignal,
|
|
||||||
@NonNull FillCallback callback) {
|
|
||||||
List<FillContext> fillContexts = request.getFillContexts();
|
|
||||||
AssistStructure latestStructure = fillContexts.get(fillContexts.size() - 1).getStructure();
|
|
||||||
|
|
||||||
cancellationSignal.setOnCancelListener(() ->
|
|
||||||
Log.e(TAG, "Cancel autofill not implemented in this sample.")
|
|
||||||
);
|
|
||||||
|
|
||||||
FillResponse.Builder responseBuilder = new FillResponse.Builder();
|
|
||||||
// Check user's settings for authenticating Responses and Datasets.
|
|
||||||
StructureParser.Result parseResult = new StructureParser(latestStructure).parse();
|
|
||||||
AutofillId[] autofillIds = parseResult.allAutofillIds();
|
|
||||||
if (!Arrays.asList(autofillIds).isEmpty()) {
|
|
||||||
// If the entire Autofill Response is authenticated, AuthActivity is used
|
|
||||||
// to generate Response.
|
|
||||||
IntentSender sender = AutoFillAuthActivity.getAuthIntentSenderForResponse(this);
|
|
||||||
RemoteViews presentation = new RemoteViews(getPackageName(), R.layout.autofill_service_unlock);
|
|
||||||
responseBuilder.setAuthentication(autofillIds, sender, presentation);
|
|
||||||
callback.onSuccess(responseBuilder.build());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSaveRequest(@NonNull SaveRequest request, @NonNull SaveCallback callback) {
|
|
||||||
// TODO Save autofill
|
|
||||||
//callback.onFailure(getString(R.string.autofill_not_support_save));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onConnected() {
|
|
||||||
Log.d(TAG, "onConnected");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDisconnected() {
|
|
||||||
Log.d(TAG, "onDisconnected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.autofill
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.CancellationSignal
|
||||||
|
import android.service.autofill.*
|
||||||
|
import android.support.annotation.RequiresApi
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.RemoteViews
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
|
class KeeAutofillService : AutofillService() {
|
||||||
|
|
||||||
|
override fun onFillRequest(request: FillRequest, cancellationSignal: CancellationSignal,
|
||||||
|
callback: FillCallback) {
|
||||||
|
val fillContexts = request.fillContexts
|
||||||
|
val latestStructure = fillContexts[fillContexts.size - 1].structure
|
||||||
|
|
||||||
|
cancellationSignal.setOnCancelListener { Log.e(TAG, "Cancel autofill not implemented in this sample.") }
|
||||||
|
|
||||||
|
val responseBuilder = FillResponse.Builder()
|
||||||
|
// Check user's settings for authenticating Responses and Datasets.
|
||||||
|
val parseResult = StructureParser(latestStructure).parse()
|
||||||
|
parseResult?.allAutofillIds()?.let { autofillIds ->
|
||||||
|
if (listOf(*autofillIds).isNotEmpty()) {
|
||||||
|
// If the entire Autofill Response is authenticated, AuthActivity is used
|
||||||
|
// to generate Response.
|
||||||
|
val sender = AutofillLauncherActivity.getAuthIntentSenderForResponse(this)
|
||||||
|
val presentation = RemoteViews(packageName, R.layout.item_autofill_service_unlock)
|
||||||
|
responseBuilder.setAuthentication(autofillIds, sender, presentation)
|
||||||
|
callback.onSuccess(responseBuilder.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
|
||||||
|
// TODO Save autofill
|
||||||
|
//callback.onFailure(getString(R.string.autofill_not_support_save));
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConnected() {
|
||||||
|
Log.d(TAG, "onConnected")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDisconnected() {
|
||||||
|
Log.d(TAG, "onDisconnected")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = KeeAutofillService::class.java.name
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,115 +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.autofill;
|
|
||||||
|
|
||||||
import android.app.assist.AssistStructure;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.support.annotation.RequiresApi;
|
|
||||||
import android.text.InputType;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.autofill.AutofillId;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse AssistStructure and guess username and password fields.
|
|
||||||
*/
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
class StructureParser {
|
|
||||||
static private final String TAG = StructureParser.class.getName();
|
|
||||||
|
|
||||||
final private AssistStructure structure;
|
|
||||||
private Result result;
|
|
||||||
private AutofillId usernameCandidate;
|
|
||||||
|
|
||||||
StructureParser(AssistStructure structure) {
|
|
||||||
this.structure = structure;
|
|
||||||
}
|
|
||||||
|
|
||||||
Result parse() {
|
|
||||||
result = new Result();
|
|
||||||
usernameCandidate = null;
|
|
||||||
for (int i=0; i<structure.getWindowNodeCount(); ++i) {
|
|
||||||
AssistStructure.WindowNode windowNode = structure.getWindowNodeAt(i);
|
|
||||||
result.title.add(windowNode.getTitle());
|
|
||||||
result.webDomain.add(windowNode.getRootViewNode().getWebDomain());
|
|
||||||
parseViewNode(windowNode.getRootViewNode());
|
|
||||||
}
|
|
||||||
// If not explicit username field found, add the field just before password field.
|
|
||||||
if (result.username.isEmpty() && result.email.isEmpty()
|
|
||||||
&& !result.password.isEmpty() && usernameCandidate != null)
|
|
||||||
result.username.add(usernameCandidate);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parseViewNode(AssistStructure.ViewNode node) {
|
|
||||||
String[] hints = node.getAutofillHints();
|
|
||||||
if (hints != null && hints.length > 0) {
|
|
||||||
if (Arrays.stream(hints).anyMatch(View.AUTOFILL_HINT_USERNAME::equals))
|
|
||||||
result.username.add(node.getAutofillId());
|
|
||||||
else if (Arrays.stream(hints).anyMatch(View.AUTOFILL_HINT_EMAIL_ADDRESS::equals))
|
|
||||||
result.email.add(node.getAutofillId());
|
|
||||||
else if (Arrays.stream(hints).anyMatch(View.AUTOFILL_HINT_PASSWORD::equals))
|
|
||||||
result.password.add(node.getAutofillId());
|
|
||||||
else
|
|
||||||
Log.d(TAG, "unsupported hints");
|
|
||||||
} else if (node.getAutofillType() == View.AUTOFILL_TYPE_TEXT) {
|
|
||||||
int inputType = node.getInputType();
|
|
||||||
if ((inputType & InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) > 0)
|
|
||||||
result.email.add(node.getAutofillId());
|
|
||||||
else if ((inputType & InputType.TYPE_TEXT_VARIATION_PASSWORD) > 0)
|
|
||||||
result.password.add(node.getAutofillId());
|
|
||||||
else if (result.password.isEmpty())
|
|
||||||
usernameCandidate = node.getAutofillId();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i=0; i<node.getChildCount(); ++i)
|
|
||||||
parseViewNode(node.getChildAt(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
static class Result {
|
|
||||||
final List<CharSequence> title;
|
|
||||||
final List<String> webDomain;
|
|
||||||
final List<AutofillId> username;
|
|
||||||
final List<AutofillId> email;
|
|
||||||
final List<AutofillId> password;
|
|
||||||
|
|
||||||
private Result() {
|
|
||||||
title = new ArrayList<>();
|
|
||||||
webDomain = new ArrayList<>();
|
|
||||||
username = new ArrayList<>();
|
|
||||||
email = new ArrayList<>();
|
|
||||||
password = new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
AutofillId[] allAutofillIds() {
|
|
||||||
ArrayList<AutofillId> all = new ArrayList<>();
|
|
||||||
all.addAll(username);
|
|
||||||
all.addAll(email);
|
|
||||||
all.addAll(password);
|
|
||||||
return all.toArray(new AutofillId[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* 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.autofill
|
||||||
|
|
||||||
|
import android.app.assist.AssistStructure
|
||||||
|
import android.os.Build
|
||||||
|
import android.support.annotation.RequiresApi
|
||||||
|
import android.text.InputType
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.view.autofill.AutofillId
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse AssistStructure and guess username and password fields.
|
||||||
|
*/
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
|
internal class StructureParser(private val structure: AssistStructure) {
|
||||||
|
private var result: Result? = null
|
||||||
|
private var usernameCandidate: AutofillId? = null
|
||||||
|
|
||||||
|
fun parse(): Result? {
|
||||||
|
result = Result()
|
||||||
|
result?.apply {
|
||||||
|
usernameCandidate = null
|
||||||
|
for (i in 0 until structure.windowNodeCount) {
|
||||||
|
val windowNode = structure.getWindowNodeAt(i)
|
||||||
|
title.add(windowNode.title)
|
||||||
|
windowNode.rootViewNode.webDomain?.let {
|
||||||
|
webDomain.add(it)
|
||||||
|
}
|
||||||
|
parseViewNode(windowNode.rootViewNode)
|
||||||
|
}
|
||||||
|
// If not explicit username field found, add the field just before password field.
|
||||||
|
if (username.isEmpty() && email.isEmpty()
|
||||||
|
&& password.isNotEmpty() && usernameCandidate != null)
|
||||||
|
username.add(usernameCandidate!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseViewNode(node: AssistStructure.ViewNode) {
|
||||||
|
val hints = node.autofillHints
|
||||||
|
val autofillId = node.autofillId
|
||||||
|
if (autofillId != null) {
|
||||||
|
if (hints != null && hints.isNotEmpty()) {
|
||||||
|
when {
|
||||||
|
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_USERNAME == it } -> result?.username?.add(autofillId)
|
||||||
|
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_EMAIL_ADDRESS == it } -> result?.email?.add(autofillId)
|
||||||
|
Arrays.stream(hints).anyMatch { View.AUTOFILL_HINT_PASSWORD == it } -> result?.password?.add(autofillId)
|
||||||
|
else -> Log.d(TAG, "unsupported hints")
|
||||||
|
}
|
||||||
|
} else if (node.autofillType == View.AUTOFILL_TYPE_TEXT) {
|
||||||
|
val inputType = node.inputType
|
||||||
|
when {
|
||||||
|
inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS > 0 -> result?.email?.add(autofillId)
|
||||||
|
inputType and InputType.TYPE_TEXT_VARIATION_PASSWORD > 0 -> result?.password?.add(autofillId)
|
||||||
|
result?.password?.isEmpty() == true -> usernameCandidate = autofillId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in 0 until node.childCount)
|
||||||
|
parseViewNode(node.getChildAt(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
|
internal class Result {
|
||||||
|
val title: MutableList<CharSequence>
|
||||||
|
val webDomain: MutableList<String>
|
||||||
|
val username: MutableList<AutofillId>
|
||||||
|
val email: MutableList<AutofillId>
|
||||||
|
val password: MutableList<AutofillId>
|
||||||
|
|
||||||
|
init {
|
||||||
|
title = ArrayList()
|
||||||
|
webDomain = ArrayList()
|
||||||
|
username = ArrayList()
|
||||||
|
email = ArrayList()
|
||||||
|
password = ArrayList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun allAutofillIds(): Array<AutofillId> {
|
||||||
|
val all = ArrayList<AutofillId>()
|
||||||
|
all.addAll(username)
|
||||||
|
all.addAll(email)
|
||||||
|
all.addAll(password)
|
||||||
|
return all.toTypedArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = StructureParser::class.java.name
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,39 +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.backup;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.backup.BackupAgentHelper;
|
|
||||||
import android.app.backup.SharedPreferencesBackupHelper;
|
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
public class SettingsBackupAgent extends BackupAgentHelper {
|
|
||||||
|
|
||||||
private static final String PREFS_BACKUP_KEY = "prefs";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
String defaultPrefs = this.getPackageName() + "_preferences";
|
|
||||||
|
|
||||||
SharedPreferencesBackupHelper prefHelper = new SharedPreferencesBackupHelper(this, defaultPrefs);
|
|
||||||
addHelper(PREFS_BACKUP_KEY, prefHelper);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* 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.backup
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.backup.BackupAgentHelper
|
||||||
|
import android.app.backup.SharedPreferencesBackupHelper
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
class SettingsBackupAgent : BackupAgentHelper() {
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
val defaultPrefs = this.packageName + "_preferences"
|
||||||
|
val prefHelper = SharedPreferencesBackupHelper(this, defaultPrefs)
|
||||||
|
addHelper(PREFS_BACKUP_KEY, prefHelper)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PREFS_BACKUP_KEY = "prefs"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.compat;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import java.lang.reflect.Constructor;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
|
||||||
public class BackupManagerCompat {
|
|
||||||
private static Class classBackupManager;
|
|
||||||
private static Constructor constructorBackupManager;
|
|
||||||
private static Method dataChanged;
|
|
||||||
|
|
||||||
private Object backupManager;
|
|
||||||
|
|
||||||
static {
|
|
||||||
try {
|
|
||||||
classBackupManager = Class.forName("android.app.backup.BackupManager");
|
|
||||||
constructorBackupManager = classBackupManager.getConstructor(Context.class);
|
|
||||||
dataChanged = classBackupManager.getMethod("dataChanged", (Class[]) null);
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Do nothing, class does not exist
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public BackupManagerCompat(Context ctx) {
|
|
||||||
if (constructorBackupManager != null) {
|
|
||||||
try {
|
|
||||||
backupManager = constructorBackupManager.newInstance(ctx);
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void dataChanged() {
|
|
||||||
if (backupManager != null && dataChanged != null) {
|
|
||||||
try {
|
|
||||||
dataChanged.invoke(backupManager, (Object[]) null);
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,53 +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.compat;
|
|
||||||
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.drawable.BitmapDrawable;
|
|
||||||
|
|
||||||
import java.lang.reflect.Constructor;
|
|
||||||
|
|
||||||
// This compatiblity hack can go away when support for Android 1.5 api level 3 is dropped
|
|
||||||
public class BitmapDrawableCompat {
|
|
||||||
private static Constructor<BitmapDrawable> constResBitmap;
|
|
||||||
|
|
||||||
static {
|
|
||||||
try {
|
|
||||||
constResBitmap = BitmapDrawable.class.getConstructor(Resources.class, Bitmap.class);
|
|
||||||
// This constructor is support in this api version
|
|
||||||
} catch (Exception e) {
|
|
||||||
// This constructor is not supported
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static BitmapDrawable getBitmapDrawable(Resources res, Bitmap bitmap) {
|
|
||||||
if (constResBitmap != null) {
|
|
||||||
try {
|
|
||||||
return constResBitmap.newInstance(res, bitmap);
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Do nothing, fall through to the safe constructor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new BitmapDrawable(bitmap);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user