mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
1437 Commits
feature/Ca
...
2.8.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5cbba5971 | ||
|
|
aea5493180 | ||
|
|
431832e50e | ||
|
|
daf239c93c | ||
|
|
2c669deae5 | ||
|
|
81969de76e | ||
|
|
5a29e7a83e | ||
|
|
9ed5aef40f | ||
|
|
9e025b4329 | ||
|
|
543da4fe24 | ||
|
|
e82e50a57a | ||
|
|
288d62d666 | ||
|
|
8ed224eeab | ||
|
|
22bafff943 | ||
|
|
02403cf566 | ||
|
|
eabbc6f037 | ||
|
|
27e60edaf4 | ||
|
|
f7d0b64dea | ||
|
|
b874484101 | ||
|
|
0ee72db2e9 | ||
|
|
4f07bac9ec | ||
|
|
80abaf70f2 | ||
|
|
d6acbc2fb1 | ||
|
|
caf93c019a | ||
|
|
7044efa7c3 | ||
|
|
5b4d8f971f | ||
|
|
75e8feb9b8 | ||
|
|
5d59419a06 | ||
|
|
ff2e47bf70 | ||
|
|
4721ed7c15 | ||
|
|
d9da328088 | ||
|
|
2ee506ac06 | ||
|
|
16c4e4dc5b | ||
|
|
28b8fb1b97 | ||
|
|
c8bae9fba6 | ||
|
|
920764ad33 | ||
|
|
a45d114527 | ||
|
|
62cca09045 | ||
|
|
ea126b90e2 | ||
|
|
777a20182e | ||
|
|
df62a9d32b | ||
|
|
47e211bf3c | ||
|
|
650ca3844e | ||
|
|
8de49fa027 | ||
|
|
35b7633f4d | ||
|
|
2c96a80280 | ||
|
|
c7000c8ac6 | ||
|
|
1dca2b4387 | ||
|
|
ccbe9ad21e | ||
|
|
1fe6f11ef4 | ||
|
|
9d4ab60fc4 | ||
|
|
ea8c5ad6e1 | ||
|
|
63a0f5f91c | ||
|
|
e8bacbdb6f | ||
|
|
bcc8226ccc | ||
|
|
ddec91a0c5 | ||
|
|
2c262bb29d | ||
|
|
ae0afb7b53 | ||
|
|
84e7fe5b60 | ||
|
|
ccb32b045a | ||
|
|
b2e29ac4bd | ||
|
|
7df6309a68 | ||
|
|
a203ad9f64 | ||
|
|
44d6e09337 | ||
|
|
7c59ec019a | ||
|
|
b457f64ec8 | ||
|
|
e152e59a61 | ||
|
|
2c620ad69a | ||
|
|
c08b405fc2 | ||
|
|
585e39c591 | ||
|
|
60e857dba9 | ||
|
|
9a4aa2b08f | ||
|
|
2900b08b70 | ||
|
|
b0936563a2 | ||
|
|
3d327b245a | ||
|
|
9a14de3448 | ||
|
|
59f83545cf | ||
|
|
d9da1ef085 | ||
|
|
279a27f740 | ||
|
|
6ba822ee48 | ||
|
|
e4445949c8 | ||
|
|
8178ff583d | ||
|
|
0e0e19a802 | ||
|
|
1e2e7d841f | ||
|
|
263c2f00eb | ||
|
|
39c0b57652 | ||
|
|
677baf549c | ||
|
|
848478c28b | ||
|
|
98ad33a589 | ||
|
|
3f33733f40 | ||
|
|
cdc4ae4fb3 | ||
|
|
3edfa8a6ce | ||
|
|
6e99b667af | ||
|
|
23c8735568 | ||
|
|
f39065044f | ||
|
|
7d2ae47b0f | ||
|
|
7247de6908 | ||
|
|
4c2aef8504 | ||
|
|
ac8717cf1f | ||
|
|
7761928064 | ||
|
|
198047406b | ||
|
|
1d57309db9 | ||
|
|
07af9f36b2 | ||
|
|
d157fea9be | ||
|
|
7692caf622 | ||
|
|
dc65a8823f | ||
|
|
2210932fdf | ||
|
|
0cb1bf4b7f | ||
|
|
44d175dd40 | ||
|
|
097feb6437 | ||
|
|
250c7e5f20 | ||
|
|
83740f77bb | ||
|
|
8267e13d22 | ||
|
|
a7e9396f35 | ||
|
|
0f6c8601d2 | ||
|
|
66e68092d5 | ||
|
|
09616a594c | ||
|
|
7ae58ea7ea | ||
|
|
9f1f85a3c4 | ||
|
|
9992cdfce0 | ||
|
|
f5fa08ce94 | ||
|
|
67f70f7f85 | ||
|
|
840bd56a3d | ||
|
|
c1e2d31cfd | ||
|
|
c35322d44e | ||
|
|
9867e46c3f | ||
|
|
946a120038 | ||
|
|
2cd1a17aa2 | ||
|
|
6e29fe2932 | ||
|
|
e6c6bf6613 | ||
|
|
72c53169df | ||
|
|
b78cce7b4f | ||
|
|
c40499ec31 | ||
|
|
ee158e517e | ||
|
|
70aebcf9aa | ||
|
|
b15870d441 | ||
|
|
fe7074736a | ||
|
|
69c523ffad | ||
|
|
6aeefdf43d | ||
|
|
5f4c8be3d3 | ||
|
|
3092e4c557 | ||
|
|
ea119068da | ||
|
|
14371ecf94 | ||
|
|
45b0fcfe15 | ||
|
|
00aa5f5586 | ||
|
|
79fd53fd4c | ||
|
|
357ee3daf0 | ||
|
|
c2f7897f10 | ||
|
|
75455c0c48 | ||
|
|
a394bb9f8e | ||
|
|
2f5a846493 | ||
|
|
90376b361d | ||
|
|
229cf6bf5f | ||
|
|
bc46737353 | ||
|
|
1db2243a2e | ||
|
|
7cf836b3cb | ||
|
|
f7bbd295d6 | ||
|
|
62dbd95b48 | ||
|
|
df07e9c719 | ||
|
|
9aaf72726e | ||
|
|
5289927619 | ||
|
|
fa8c686f75 | ||
|
|
df5f28b7c4 | ||
|
|
280d8368fa | ||
|
|
80dbff1f21 | ||
|
|
7ee68a8481 | ||
|
|
ac8dd42c45 | ||
|
|
eed2148b2a | ||
|
|
dc5345b6d3 | ||
|
|
221af0b5bb | ||
|
|
a10ccc1eb0 | ||
|
|
b72d858480 | ||
|
|
60412cc90b | ||
|
|
512ac87dc9 | ||
|
|
d97020d1c5 | ||
|
|
f82cb617ba | ||
|
|
f79281a1a0 | ||
|
|
7be1dbb78b | ||
|
|
f875787799 | ||
|
|
f82c208556 | ||
|
|
f2150e3d85 | ||
|
|
ecc198e8a0 | ||
|
|
949bc58a80 | ||
|
|
7d79fff16f | ||
|
|
3aeb678292 | ||
|
|
160ac41bed | ||
|
|
9dfbcbe89c | ||
|
|
30da529348 | ||
|
|
dd8d114711 | ||
|
|
2191a4a848 | ||
|
|
8b9ea8d988 | ||
|
|
46dda8567d | ||
|
|
6953da4d9a | ||
|
|
e987d6647e | ||
|
|
359d85727e | ||
|
|
a994bf9dd8 | ||
|
|
14c4e095f6 | ||
|
|
59dce0e56f | ||
|
|
1f54a893a7 | ||
|
|
9489f1ee3d | ||
|
|
dc3d720e8d | ||
|
|
efe30b598b | ||
|
|
42515bfb2d | ||
|
|
acb3657d95 | ||
|
|
e7159c9d36 | ||
|
|
f3fdca368b | ||
|
|
4ea3e08a45 | ||
|
|
1eebc72b21 | ||
|
|
9a91be7e36 | ||
|
|
48476f9b88 | ||
|
|
68c991eb9b | ||
|
|
b51c77b01b | ||
|
|
57105db554 | ||
|
|
4bd3bdaddf | ||
|
|
9cf59b8d73 | ||
|
|
a793b0bb42 | ||
|
|
4d9e2e1471 | ||
|
|
1719887e55 | ||
|
|
2be00aca9d | ||
|
|
6fd05c5ad7 | ||
|
|
65e404374f | ||
|
|
0b78731bb3 | ||
|
|
65d318ed88 | ||
|
|
1bfcea55a9 | ||
|
|
6780eb004d | ||
|
|
0e12b2d021 | ||
|
|
1b356f87ec | ||
|
|
bf24d0bae1 | ||
|
|
97c831d4bb | ||
|
|
e3e48ffa6d | ||
|
|
b678416122 | ||
|
|
df722925fa | ||
|
|
4cbc0d9806 | ||
|
|
b0f3711b4e | ||
|
|
14ec6579b2 | ||
|
|
30bf039473 | ||
|
|
33bea317b0 | ||
|
|
c6814dc05e | ||
|
|
16808069ec | ||
|
|
e813974e29 | ||
|
|
7e0010b536 | ||
|
|
93171adcb3 | ||
|
|
9501cc76a4 | ||
|
|
5d7db046ac | ||
|
|
46c259bc3e | ||
|
|
3bacff91d3 | ||
|
|
bd79d483d2 | ||
|
|
16e31f4881 | ||
|
|
afa23c393d | ||
|
|
54d23cb781 | ||
|
|
c757e410e9 | ||
|
|
39dd25567d | ||
|
|
32cd998c2a | ||
|
|
691fc6335e | ||
|
|
b0025d1416 | ||
|
|
2467d8b0e7 | ||
|
|
28993c53e7 | ||
|
|
efdea870f0 | ||
|
|
b2995ec862 | ||
|
|
2bcc84dbb2 | ||
|
|
70cc98ce33 | ||
|
|
6e055f398d | ||
|
|
9f6234f032 | ||
|
|
6135544b72 | ||
|
|
5f32ec218b | ||
|
|
3b9a884db2 | ||
|
|
ada6b85868 | ||
|
|
2f8a4f447c | ||
|
|
9bd6499271 | ||
|
|
76fcc919ef | ||
|
|
a382297edf | ||
|
|
4987dfe4f6 | ||
|
|
d1a2e50b8d | ||
|
|
b9e44b6166 | ||
|
|
af601edc94 | ||
|
|
6640dcf9cd | ||
|
|
9b2734ed38 | ||
|
|
afcfce056f | ||
|
|
8f3af2f27b | ||
|
|
706aae47b3 | ||
|
|
d494295b21 | ||
|
|
c1600a253b | ||
|
|
a3bc29ad8f | ||
|
|
83225ed157 | ||
|
|
f13f6dc01f | ||
|
|
2d2489443a | ||
|
|
0e5b7fbfa2 | ||
|
|
d16a8068f7 | ||
|
|
c5ef11febc | ||
|
|
027f31447a | ||
|
|
41d1a4e5fb | ||
|
|
924e3191cb | ||
|
|
ebdc6b8fd9 | ||
|
|
5c05128cd7 | ||
|
|
5bf5685a12 | ||
|
|
f7391cb4c4 | ||
|
|
bad7bd8884 | ||
|
|
e0524a1656 | ||
|
|
f59af9baa3 | ||
|
|
b36890ca82 | ||
|
|
22703af08b | ||
|
|
c4108269b3 | ||
|
|
94c9d090cf | ||
|
|
28b9235a43 | ||
|
|
3aebae5e15 | ||
|
|
2631cb75d6 | ||
|
|
07b8b4156f | ||
|
|
7978967c1a | ||
|
|
3f24ff4de3 | ||
|
|
7253dd82a6 | ||
|
|
2b8eb3ae7e | ||
|
|
429eae71cd | ||
|
|
e5c552defb | ||
|
|
5c950a2e2c | ||
|
|
577ff78189 | ||
|
|
3f3cde05f7 | ||
|
|
b48f2c3276 | ||
|
|
8e818846f0 | ||
|
|
1554e37f8c | ||
|
|
affaabd011 | ||
|
|
f34a8f991c | ||
|
|
c2b14d610b | ||
|
|
02693d0cbb | ||
|
|
23155279ab | ||
|
|
2d23f7403d | ||
|
|
ae411c6fd5 | ||
|
|
ab8d6075a9 | ||
|
|
bc5ae29a67 | ||
|
|
1a8aabc30c | ||
|
|
8c51d7f713 | ||
|
|
8a1485e7ce | ||
|
|
614145431a | ||
|
|
db25f1999f | ||
|
|
4ed231b9bb | ||
|
|
25a5342c11 | ||
|
|
c7202e3ca9 | ||
|
|
89c2e94cea | ||
|
|
3dc46771b5 | ||
|
|
0eac4d4d7f | ||
|
|
a0ceb788db | ||
|
|
98fb36d03a | ||
|
|
a670006517 | ||
|
|
9cdbe67cd4 | ||
|
|
bbe8af452c | ||
|
|
f5edf28ce1 | ||
|
|
8fc30f590b | ||
|
|
e578f23ebe | ||
|
|
99c488fc9e | ||
|
|
f6a710660d | ||
|
|
a61744bb65 | ||
|
|
17c3078c24 | ||
|
|
5fa7731b56 | ||
|
|
c8e2be4d8c | ||
|
|
e3db613a07 | ||
|
|
0f7839027f | ||
|
|
31b322a108 | ||
|
|
b3c0494618 | ||
|
|
78ddb0533d | ||
|
|
da2158e7f2 | ||
|
|
d2a1efb6e7 | ||
|
|
98a880db2d | ||
|
|
cb82ef8703 | ||
|
|
d56246767b | ||
|
|
bb477984aa | ||
|
|
5a3e650e02 | ||
|
|
3c96dd2fac | ||
|
|
da051e3ff3 | ||
|
|
d15b6323c2 | ||
|
|
ec5f8fe4a4 | ||
|
|
71d84d76f8 | ||
|
|
d33ed52ec2 | ||
|
|
3a970544bb | ||
|
|
792ac3a2e8 | ||
|
|
60bbc27401 | ||
|
|
cf7cbcb6e6 | ||
|
|
c126a8eba9 | ||
|
|
66a60d0357 | ||
|
|
1acdadd027 | ||
|
|
200be9dadd | ||
|
|
a73e2872a4 | ||
|
|
ce6f7729c5 | ||
|
|
c285411371 | ||
|
|
46394c600e | ||
|
|
bea9cb3248 | ||
|
|
1087dcd714 | ||
|
|
0b63029b7e | ||
|
|
9679d24414 | ||
|
|
7947fd53e5 | ||
|
|
8dedf75565 | ||
|
|
b5b7c12b49 | ||
|
|
51f4e3cc3a | ||
|
|
24b0315d2e | ||
|
|
be446220eb | ||
|
|
a3ef2d332e | ||
|
|
ba6fe576e3 | ||
|
|
abcef38102 | ||
|
|
d5780b2f30 | ||
|
|
f7e498a0a2 | ||
|
|
51ac7ca2de | ||
|
|
c94535f6b5 | ||
|
|
07457ae368 | ||
|
|
4767fff08c | ||
|
|
f0c3498ecc | ||
|
|
1eca52d0fe | ||
|
|
7fbac9ad2f | ||
|
|
6fb80ed50b | ||
|
|
47f63ac81b | ||
|
|
42cc0b28ba | ||
|
|
993806f781 | ||
|
|
8a5af33aaa | ||
|
|
a974e36e9e | ||
|
|
17bcb2b39e | ||
|
|
cddf02d0c1 | ||
|
|
75ff7ece37 | ||
|
|
ec2b407a20 | ||
|
|
1dc9d78e54 | ||
|
|
5742a75c9d | ||
|
|
b5e9ad6d7e | ||
|
|
6393025219 | ||
|
|
9ab3e289bc | ||
|
|
6454474886 | ||
|
|
c5720a7a03 | ||
|
|
41b15adc6d | ||
|
|
05b962e718 | ||
|
|
1f01ca7b85 | ||
|
|
5d3b4fa5ec | ||
|
|
754d2b2dd3 | ||
|
|
ffad62e3dc | ||
|
|
1c11e16565 | ||
|
|
edc12990b4 | ||
|
|
12ea234d18 | ||
|
|
0461206a61 | ||
|
|
663f9e3962 | ||
|
|
34ee948c8e | ||
|
|
1bb9c2e4fe | ||
|
|
8ab18ce5cc | ||
|
|
71be16826e | ||
|
|
926c09d9df | ||
|
|
66c065ae7f | ||
|
|
083ed7775c | ||
|
|
5185452495 | ||
|
|
fa15f226ab | ||
|
|
fea4da2a33 | ||
|
|
055c933f4b | ||
|
|
9bcb867748 | ||
|
|
26e3c03f5f | ||
|
|
c195c3b2d1 | ||
|
|
f9e0aacfeb | ||
|
|
37fef66647 | ||
|
|
9f99b67563 | ||
|
|
fe902648ad | ||
|
|
13b933cd0b | ||
|
|
1d3b1d1d80 | ||
|
|
67a612af3a | ||
|
|
a891683806 | ||
|
|
440a72fc42 | ||
|
|
696d2e5197 | ||
|
|
2b17d56fc7 | ||
|
|
a410ef5d9f | ||
|
|
fe94769541 | ||
|
|
c63ae9c00c | ||
|
|
d5ece8d007 | ||
|
|
692a971dc0 | ||
|
|
05b8370cc0 | ||
|
|
b6111b35a2 | ||
|
|
4d72687628 | ||
|
|
8f125983ce | ||
|
|
4279825caa | ||
|
|
77ae3a4623 | ||
|
|
4c222dbc54 | ||
|
|
4e0f93ee8a | ||
|
|
e99f3e6627 | ||
|
|
f73877c34a | ||
|
|
abd3f12cae | ||
|
|
00117f5b7b | ||
|
|
d7d728f93e | ||
|
|
dc9217c4ec | ||
|
|
95acb13b93 | ||
|
|
234cc00d9f | ||
|
|
d6ba164799 | ||
|
|
910aa03dc8 | ||
|
|
a3e4a4c873 | ||
|
|
2d7c843447 | ||
|
|
e342b45473 | ||
|
|
f354bccd58 | ||
|
|
98073134db | ||
|
|
360666b00b | ||
|
|
ce4c807870 | ||
|
|
f2783bdac8 | ||
|
|
875ed16c3b | ||
|
|
383ba56d1f | ||
|
|
45eb54e624 | ||
|
|
5aff4e2ed6 | ||
|
|
e73e47dd94 | ||
|
|
1c8ac5efbc | ||
|
|
90fa5e1ecd | ||
|
|
348994917b | ||
|
|
60dbea1027 | ||
|
|
dae19bbccf | ||
|
|
c81f83887e | ||
|
|
04e555dde9 | ||
|
|
be94518e31 | ||
|
|
66bec1e08c | ||
|
|
f61ce10716 | ||
|
|
b1b92b2995 | ||
|
|
bd9f2c4757 | ||
|
|
a3ead2153e | ||
|
|
e12f008b92 | ||
|
|
d064ece0ff | ||
|
|
379fbf68b1 | ||
|
|
83783c1a88 | ||
|
|
c7e46205b3 | ||
|
|
0c61f0ded2 | ||
|
|
49e2ec0498 | ||
|
|
fb0a74c101 | ||
|
|
245a7ddfe9 | ||
|
|
ca8874c2e1 | ||
|
|
dbcd7c8e03 | ||
|
|
9cce63659e | ||
|
|
be0bbab0c8 | ||
|
|
7b6d3698c4 | ||
|
|
56daca8b4f | ||
|
|
ed382d102e | ||
|
|
745de2502e | ||
|
|
503a4b1374 | ||
|
|
0b71b2d659 | ||
|
|
2ad244df94 | ||
|
|
d0da4f03a6 | ||
|
|
ab0cd4152a | ||
|
|
93d04bfe60 | ||
|
|
b1ccb40bd3 | ||
|
|
d177001ea8 | ||
|
|
2f921897c7 | ||
|
|
1a0f7146ce | ||
|
|
dff2386594 | ||
|
|
55cc782cc6 | ||
|
|
6903099873 | ||
|
|
6e2d84be33 | ||
|
|
9e542d0bbe | ||
|
|
ade9af9ecd | ||
|
|
59f11a1b26 | ||
|
|
cc1d6e2b47 | ||
|
|
2655b1b3d1 | ||
|
|
053dd28f8c | ||
|
|
65e4cf83d8 | ||
|
|
419099318c | ||
|
|
972edd3a30 | ||
|
|
cc4125e766 | ||
|
|
da40cc9830 | ||
|
|
29d7e2dcfe | ||
|
|
3d906fd582 | ||
|
|
0cad43c18b | ||
|
|
c66b686a63 | ||
|
|
e2a1e3f327 | ||
|
|
2b5cf75a53 | ||
|
|
1c350ac87b | ||
|
|
ef6539909c | ||
|
|
47d7c6fe65 | ||
|
|
c94c5fbc95 | ||
|
|
63a833d114 | ||
|
|
72162128e2 | ||
|
|
a8f712c335 | ||
|
|
ea8888a685 | ||
|
|
2aecf69b67 | ||
|
|
1a4c24dd86 | ||
|
|
6cf2b45051 | ||
|
|
38dd2bdf6e | ||
|
|
8784f1da70 | ||
|
|
b9208ea94e | ||
|
|
dfb648b480 | ||
|
|
f7af4f06ea | ||
|
|
d98f9eb62c | ||
|
|
fef1335f51 | ||
|
|
59f14cbdd4 | ||
|
|
e8645c543f | ||
|
|
b92f6177e3 | ||
|
|
2888829a4f | ||
|
|
3d392ead19 | ||
|
|
a79b2bfa79 | ||
|
|
15e1e2f02e | ||
|
|
9e652803d0 | ||
|
|
7795fceb72 | ||
|
|
9f01f26ea6 | ||
|
|
abbc9a1d7a | ||
|
|
2c15befb8e | ||
|
|
ba8ea84c4e | ||
|
|
97912046da | ||
|
|
43e99c23e3 | ||
|
|
ffe14d75fc | ||
|
|
872ef66641 | ||
|
|
6363862ec2 | ||
|
|
775a112e83 | ||
|
|
7168904290 | ||
|
|
4213209b08 | ||
|
|
5dce91b7f6 | ||
|
|
6d7fb9f87c | ||
|
|
cc6c9dd8d1 | ||
|
|
21ebec52f3 | ||
|
|
11023ab225 | ||
|
|
fe97b15905 | ||
|
|
de8738aa03 | ||
|
|
568bbf9126 | ||
|
|
a2481652da | ||
|
|
33e3d3272d | ||
|
|
b909651c29 | ||
|
|
16794c5252 | ||
|
|
49ad270d88 | ||
|
|
df9b6cb7e8 | ||
|
|
c6c4551928 | ||
|
|
de84353eb0 | ||
|
|
6f05b80f34 | ||
|
|
797dc706e2 | ||
|
|
bdb615bcf9 | ||
|
|
1b98717b0e | ||
|
|
7bd1aedada | ||
|
|
34bf8f9d1f | ||
|
|
b07f70e9fe | ||
|
|
e635788955 | ||
|
|
bdb1cef3e5 | ||
|
|
e5a32e65c0 | ||
|
|
5fc77922b4 | ||
|
|
ce8add2895 | ||
|
|
88e1e5b770 | ||
|
|
76ecbd3497 | ||
|
|
e33c9b932f | ||
|
|
038f6caa04 | ||
|
|
36f5249d71 | ||
|
|
570702a5bd | ||
|
|
5d03c9c644 | ||
|
|
83906def4a | ||
|
|
33e0f25fb1 | ||
|
|
2080de4139 | ||
|
|
decaca889c | ||
|
|
794a39032f | ||
|
|
cc3cf17060 | ||
|
|
b07da4bfa8 | ||
|
|
4f52a4ee79 | ||
|
|
cf77b999a3 | ||
|
|
c9f062bdd9 | ||
|
|
12669f7ea0 | ||
|
|
03451d2a6e | ||
|
|
b5d7595f87 | ||
|
|
0d3d760a43 | ||
|
|
1cfc86e5ce | ||
|
|
938343323d | ||
|
|
37112715c0 | ||
|
|
bc80c10193 | ||
|
|
d989f48226 | ||
|
|
45bcbb3a3d | ||
|
|
f4a74e0254 | ||
|
|
ace83cf68d | ||
|
|
209b9b9a6f | ||
|
|
0d9dd6a33e | ||
|
|
e4137a031e | ||
|
|
41e9ea7e4f | ||
|
|
1a8b88973d | ||
|
|
3109e251b9 | ||
|
|
452ab280f1 | ||
|
|
34ecfbb846 | ||
|
|
11fb97d275 | ||
|
|
4bb30d075b | ||
|
|
f38af55c05 | ||
|
|
8a007376cc | ||
|
|
940a1dfc78 | ||
|
|
cbd64d1602 | ||
|
|
955c9992d0 | ||
|
|
87606818cd | ||
|
|
ffcd264bac | ||
|
|
04c70cf99d | ||
|
|
0e0b6fca07 | ||
|
|
b24a789352 | ||
|
|
3d4f447037 | ||
|
|
7aa52ca00a | ||
|
|
a94c9050c5 | ||
|
|
9b18e71e12 | ||
|
|
d5ff048bd0 | ||
|
|
76f44f2573 | ||
|
|
9988f1a76f | ||
|
|
1559376bc2 | ||
|
|
4d0f252acd | ||
|
|
d5a84e66ad | ||
|
|
1dfe3453f4 | ||
|
|
a7dc9f31b9 | ||
|
|
d275f18b3c | ||
|
|
d770429401 | ||
|
|
501f0be99f | ||
|
|
a70122f873 | ||
|
|
1e1bd15a06 | ||
|
|
164fb1f4f5 | ||
|
|
0206514bdb | ||
|
|
ecf2b42fa4 | ||
|
|
df683aeea9 | ||
|
|
cdd05958f7 | ||
|
|
4ec5fc05fb | ||
|
|
4019cafac1 | ||
|
|
4333e179d1 | ||
|
|
87d67be428 | ||
|
|
350863adff | ||
|
|
d125fc46a0 | ||
|
|
5f4c079071 | ||
|
|
5ec576a76e | ||
|
|
9edb00ebbb | ||
|
|
59843e5464 | ||
|
|
4fe0ccf807 | ||
|
|
2ee012d3fe | ||
|
|
161b4cb5f5 | ||
|
|
f9bee68d71 | ||
|
|
e0d98aca1b | ||
|
|
d47518c00c | ||
|
|
58f97dc3ab | ||
|
|
c7706cb80c | ||
|
|
49bcc877ef | ||
|
|
4b81dd552e | ||
|
|
ff63aaf3f3 | ||
|
|
39cbd1477b | ||
|
|
3741cc558a | ||
|
|
2a314ca3c1 | ||
|
|
10f9564825 | ||
|
|
1ae87c9b18 | ||
|
|
24301ba462 | ||
|
|
ff6481274f | ||
|
|
866df585a2 | ||
|
|
416bec04a1 | ||
|
|
f747d6725e | ||
|
|
8846918e55 | ||
|
|
e347aefcd9 | ||
|
|
0a0676af3a | ||
|
|
7a69b63b4f | ||
|
|
fab5626741 | ||
|
|
c6832d6478 | ||
|
|
f451597746 | ||
|
|
a92e5b5156 | ||
|
|
786b3b26ea | ||
|
|
2e9fd2fd79 | ||
|
|
3be2b9893b | ||
|
|
7f6bed4f5f | ||
|
|
37322f284a | ||
|
|
4106bb1792 | ||
|
|
229db1242a | ||
|
|
8201b42135 | ||
|
|
24818575dc | ||
|
|
213050e7f2 | ||
|
|
1efaf4e3ea | ||
|
|
5532147992 | ||
|
|
550b43094d | ||
|
|
5e831837c8 | ||
|
|
480e88a088 | ||
|
|
81cbcbe8af | ||
|
|
5649634809 | ||
|
|
69a253f738 | ||
|
|
fbc8cfddb8 | ||
|
|
3e1024804c | ||
|
|
cf0b51be00 | ||
|
|
5b295a2a8f | ||
|
|
ebb3d7a149 | ||
|
|
73ab348d11 | ||
|
|
8ce17086f8 | ||
|
|
45cffc93b1 | ||
|
|
71cf4d5a34 | ||
|
|
b79a4154af | ||
|
|
ff818f8472 | ||
|
|
b540d32ca3 | ||
|
|
895a11d45f | ||
|
|
bdafae6132 | ||
|
|
f45d9ce6da | ||
|
|
7456e2c8f5 | ||
|
|
fc51b50fe6 | ||
|
|
b31aa26975 | ||
|
|
49801e1b14 | ||
|
|
1453464cbd | ||
|
|
b9be8ff13d | ||
|
|
9663c3cadd | ||
|
|
da5490bc0a | ||
|
|
df9b1b9fbb | ||
|
|
6a0a3ded13 | ||
|
|
ac7f4a08c3 | ||
|
|
e5ff5e3364 | ||
|
|
f0ac19fcc1 | ||
|
|
ce23d34923 | ||
|
|
bdbba87f5a | ||
|
|
fa8ef0477b | ||
|
|
3e9ec4cfb6 | ||
|
|
ad1c9d8129 | ||
|
|
7aaac4c13c | ||
|
|
a6c71c3d54 | ||
|
|
05714f38dc | ||
|
|
4ef5e4d8ae | ||
|
|
5fd0e1d6fe | ||
|
|
97faf5bef3 | ||
|
|
bd77128733 | ||
|
|
935debc6bf | ||
|
|
699ed19b6a | ||
|
|
126e5a94c2 | ||
|
|
792bd36ea7 | ||
|
|
04dfee1b43 | ||
|
|
6a08cfeea9 | ||
|
|
29a0f6c9f6 | ||
|
|
e2bdf26d82 | ||
|
|
1643949110 | ||
|
|
577dabd727 | ||
|
|
ed4bd0693f | ||
|
|
0f2982b34d | ||
|
|
0954094800 | ||
|
|
22dcfafc62 | ||
|
|
dd34051426 | ||
|
|
6feaf2cb8a | ||
|
|
603f64ea92 | ||
|
|
568f9ab0d0 | ||
|
|
a2ff1c33e6 | ||
|
|
84452e9fc0 | ||
|
|
5a6ae453cf | ||
|
|
cde3e3361d | ||
|
|
25f01192a4 | ||
|
|
bacb08ec65 | ||
|
|
36c905ee1c | ||
|
|
6b9280534a | ||
|
|
f94c6e850f | ||
|
|
f4cecf6906 | ||
|
|
a81d3766f8 | ||
|
|
7182c2e66d | ||
|
|
503316bc70 | ||
|
|
d5af59f2c7 | ||
|
|
cb3ac1ad3a | ||
|
|
e161080e4c | ||
|
|
ceab7f917b | ||
|
|
41d8066c4c | ||
|
|
e373cbd776 | ||
|
|
05ea6b6b10 | ||
|
|
28eed3ae71 | ||
|
|
535c67ac9b | ||
|
|
68027a6e15 | ||
|
|
12dea6b499 | ||
|
|
d8bd078a02 | ||
|
|
439bc109b0 | ||
|
|
b1ec93ceb5 | ||
|
|
44b9aa0e48 | ||
|
|
0a59063027 | ||
|
|
5db4608abd | ||
|
|
6ece2aa6cb | ||
|
|
95ee45f666 | ||
|
|
556e90b8d8 | ||
|
|
42841e6247 | ||
|
|
324c82248a | ||
|
|
94f5a47918 | ||
|
|
3f70990956 | ||
|
|
d77635e572 | ||
|
|
0f26f1b751 | ||
|
|
456bc22138 | ||
|
|
b6fe91e396 | ||
|
|
0fb4d26949 | ||
|
|
3b3583a416 | ||
|
|
cac1d576c8 | ||
|
|
9cba0d0a48 | ||
|
|
3bf11e9dc0 | ||
|
|
edbb160ac6 | ||
|
|
ee111dc63c | ||
|
|
b06edb756a | ||
|
|
fd745494e0 | ||
|
|
702bf3f479 | ||
|
|
6adc02a91f | ||
|
|
55bae5a130 | ||
|
|
1aae817b17 | ||
|
|
8afc8c23fb | ||
|
|
36e473b139 | ||
|
|
e529723f86 | ||
|
|
6c5112c142 | ||
|
|
77ac68a603 | ||
|
|
e816f40872 | ||
|
|
65313f114b | ||
|
|
ef1f1342f5 | ||
|
|
9205fe6c08 | ||
|
|
75df3e81fe | ||
|
|
47fffbadb5 | ||
|
|
78354473fa | ||
|
|
8d7efb44b5 | ||
|
|
cab00b3d8c | ||
|
|
8efab01336 | ||
|
|
66feb8beb4 | ||
|
|
ca4cccffeb | ||
|
|
51b1760c50 | ||
|
|
42e1bda365 | ||
|
|
194b6b557a | ||
|
|
e7bc439997 | ||
|
|
ef76cce0ac | ||
|
|
63eec6d969 | ||
|
|
719ae74c06 | ||
|
|
37cf424eb8 | ||
|
|
3a87f7ba9d | ||
|
|
275428d825 | ||
|
|
7b9dac86ca | ||
|
|
46496ee2cc | ||
|
|
110cc402cc | ||
|
|
587f006259 | ||
|
|
ec45c0df81 | ||
|
|
dc575aeca4 | ||
|
|
a3f790f000 | ||
|
|
4e9e188d02 | ||
|
|
abb17efae4 | ||
|
|
16be990502 | ||
|
|
d1a496f9a3 | ||
|
|
cd730fcfef | ||
|
|
1b65bf665b | ||
|
|
6f4735790c | ||
|
|
040666f89d | ||
|
|
227cb800c3 | ||
|
|
0052569a14 | ||
|
|
f7561c4888 | ||
|
|
92200f19e7 | ||
|
|
4c01f18a62 | ||
|
|
a6ce3e49fe | ||
|
|
1b3a5d1bf6 | ||
|
|
db7bdc63c8 | ||
|
|
7aca550f02 | ||
|
|
b60980b3fd | ||
|
|
b578c2c584 | ||
|
|
fc45bd624e | ||
|
|
17be3d9d2c | ||
|
|
5b3a38a7bc | ||
|
|
4403835d50 | ||
|
|
b7a3d3eb46 | ||
|
|
29846b22fe | ||
|
|
32d235e8c7 | ||
|
|
cb982b3513 | ||
|
|
d7ed6c26dd | ||
|
|
8ff19f7e68 | ||
|
|
729e062c3a | ||
|
|
7d0340ac07 | ||
|
|
01960e74c1 | ||
|
|
8e40250985 | ||
|
|
8ae2edb61a | ||
|
|
0baa7bcbf1 | ||
|
|
fffee48918 | ||
|
|
515abb6e14 | ||
|
|
6c1c3ff87f | ||
|
|
5b65575c7a | ||
|
|
0f258fc5f8 | ||
|
|
206bc661dc | ||
|
|
d0e35b109e | ||
|
|
61769c4f20 | ||
|
|
95778ee5f4 | ||
|
|
155030fdca | ||
|
|
98237ef76c | ||
|
|
f0a7b38199 | ||
|
|
9fc5e6751b | ||
|
|
4c1630312b | ||
|
|
d397c5c996 | ||
|
|
f6c41b5a60 | ||
|
|
06eb5c01c3 | ||
|
|
7df49f91e8 | ||
|
|
96dcbb0ce7 | ||
|
|
5f828fb986 | ||
|
|
533d663938 | ||
|
|
ae788503a9 | ||
|
|
cf0acd9c73 | ||
|
|
0857f2f1cf | ||
|
|
c05d412bdb | ||
|
|
c8e0ce717d | ||
|
|
cc3485b201 | ||
|
|
81ea7080c2 | ||
|
|
76ff6f5ae0 | ||
|
|
0c0d0b7a6f | ||
|
|
ec8cf1f6b7 | ||
|
|
da44310d1b | ||
|
|
4bd9c84bb0 | ||
|
|
3b1269a770 | ||
|
|
7c986ccee8 | ||
|
|
903bad8f36 | ||
|
|
4b9577437c | ||
|
|
c316011fbc | ||
|
|
3fb1f18c22 | ||
|
|
53935058f5 | ||
|
|
a3860c9581 | ||
|
|
dc20899d26 | ||
|
|
62ac3ddb75 | ||
|
|
b792a61bf9 | ||
|
|
aae9f9e1cb | ||
|
|
d098bf5e6a | ||
|
|
b0e8a3ecd9 | ||
|
|
4efa684022 | ||
|
|
f2c8082990 | ||
|
|
1abba80045 | ||
|
|
68564a2b75 | ||
|
|
385b701b38 | ||
|
|
11c9a1d707 | ||
|
|
c5aef6b561 | ||
|
|
dfcf73cfd0 | ||
|
|
7fc9389700 | ||
|
|
d1af7349bc | ||
|
|
0fd955197d | ||
|
|
cbf33507d1 | ||
|
|
bc60a5d97e | ||
|
|
57596b2991 | ||
|
|
43b3602a52 | ||
|
|
c09ec961b8 | ||
|
|
d140b453b2 | ||
|
|
9eb42636ec | ||
|
|
ee2d663fce | ||
|
|
8e83615a22 | ||
|
|
0ff129c5ca | ||
|
|
e49858439f | ||
|
|
3df07f7f47 | ||
|
|
ff9e179593 | ||
|
|
7539fee04b | ||
|
|
71a339a58f | ||
|
|
9ef2ea016b | ||
|
|
de4936a16d | ||
|
|
574d2b8904 | ||
|
|
1f3f7634e7 | ||
|
|
3c0725baff | ||
|
|
b0e1411012 | ||
|
|
39daf4714d | ||
|
|
4706afa823 | ||
|
|
25977d389d | ||
|
|
f760110569 | ||
|
|
133e78fe97 | ||
|
|
d92e0c8620 | ||
|
|
62fdb69d6b | ||
|
|
def57c9fb2 | ||
|
|
21c9c898c3 | ||
|
|
1f03c922c2 | ||
|
|
3f6ae6bdac | ||
|
|
60615ee1eb | ||
|
|
92b0d1bfa9 | ||
|
|
237988dc1f | ||
|
|
a846ec29ca | ||
|
|
4533e96bff | ||
|
|
0a401c3ac9 | ||
|
|
468abaf077 | ||
|
|
4ccf2f641c | ||
|
|
34eb2785cf | ||
|
|
09dbfe323e | ||
|
|
1f06c5b425 | ||
|
|
b98e089f7a | ||
|
|
a0ad06ed0a | ||
|
|
ec63365429 | ||
|
|
2cb85e4346 | ||
|
|
0d7c479c51 | ||
|
|
5a6c21e662 | ||
|
|
d6cadac98f | ||
|
|
dac2fc2c37 | ||
|
|
0fb45cef0d | ||
|
|
5ebdbd4003 | ||
|
|
b30f1023cb | ||
|
|
e5f65a4d1e | ||
|
|
ab42a65aa4 | ||
|
|
e351456bfe | ||
|
|
452e68b08f | ||
|
|
d65beed7a1 | ||
|
|
f5a5a0e8cb | ||
|
|
98380a0906 | ||
|
|
22fe7508f3 | ||
|
|
c8e241fc76 | ||
|
|
2c943e00d0 | ||
|
|
7ddb83b72d | ||
|
|
50bac01699 | ||
|
|
e0a92dfadd | ||
|
|
b50c951091 | ||
|
|
2f589a95a9 | ||
|
|
53eac86a95 | ||
|
|
9412f8955e | ||
|
|
71c98d82b1 | ||
|
|
42c1a925b4 | ||
|
|
1e84534ffd | ||
|
|
b087733e37 | ||
|
|
675efe29ac | ||
|
|
9833af8225 | ||
|
|
790c624571 | ||
|
|
67a70a8453 | ||
|
|
64bb05e2dd | ||
|
|
c111db6e73 | ||
|
|
24c5915bd3 | ||
|
|
04c3717618 | ||
|
|
78275a0984 | ||
|
|
bd908ed10d | ||
|
|
5473deec95 | ||
|
|
653258afd2 | ||
|
|
00a2210eea | ||
|
|
ec5688a013 | ||
|
|
fc0c7b5708 | ||
|
|
464f7ac486 | ||
|
|
fb2457146c | ||
|
|
d90870838e | ||
|
|
8c45f23291 | ||
|
|
315a3efa52 | ||
|
|
28abb5ae6f | ||
|
|
bb78d89682 | ||
|
|
fd36e19168 | ||
|
|
58db516e44 | ||
|
|
2b875e94dc | ||
|
|
0ba2447f55 | ||
|
|
8e684d0d3a | ||
|
|
ce3c1d4685 | ||
|
|
2cd77d47eb | ||
|
|
ac9366e351 | ||
|
|
af356586f8 | ||
|
|
7650db81a4 | ||
|
|
8925c86afd | ||
|
|
8f1836009e | ||
|
|
5fcf3f9b95 | ||
|
|
1c466d9e40 | ||
|
|
b97c2d9cbc | ||
|
|
3d970e4967 | ||
|
|
765c8f53dd | ||
|
|
4e81caeadf | ||
|
|
08b10e4d58 | ||
|
|
f8594f72e8 | ||
|
|
24ebee07cd | ||
|
|
a06ebb0991 | ||
|
|
cd5d4498e7 | ||
|
|
0bd62780c6 | ||
|
|
896f9327d6 | ||
|
|
cc83a99efe | ||
|
|
b6da20fef7 | ||
|
|
2912678559 | ||
|
|
409e870ef0 | ||
|
|
3d84074a0a | ||
|
|
a717fdfed4 | ||
|
|
be1f68015b | ||
|
|
2c29dcf1f6 | ||
|
|
2b1173177f | ||
|
|
3c0f7dc79c | ||
|
|
39d813bf3a | ||
|
|
5e2bc0d05b | ||
|
|
ec751159ae | ||
|
|
dda8b95f83 | ||
|
|
286012fe2a | ||
|
|
ab27299789 | ||
|
|
dfcdd5aa88 | ||
|
|
a05ea52689 | ||
|
|
de12b5de5b | ||
|
|
57b03eaca4 | ||
|
|
36bd00b760 | ||
|
|
4578a9974a | ||
|
|
d0371f58c6 | ||
|
|
9f80457351 | ||
|
|
56daf6f676 | ||
|
|
7f7b8d423b | ||
|
|
031afc80cb | ||
|
|
b77e28b04d | ||
|
|
2f8c3fdcfe | ||
|
|
f501a87099 | ||
|
|
d0ec5f26dd | ||
|
|
97776e9329 | ||
|
|
a19356c49e | ||
|
|
4f16918cf0 | ||
|
|
1af9761144 | ||
|
|
d74e814c79 | ||
|
|
16d09bca6c | ||
|
|
462c29b769 | ||
|
|
3939276d58 | ||
|
|
ff85f18c4c | ||
|
|
dd14fe4123 | ||
|
|
d804659ee2 | ||
|
|
e3152cf901 | ||
|
|
de236f321f | ||
|
|
66f4611866 | ||
|
|
6a6ef052af | ||
|
|
4e4606dabd | ||
|
|
0ce11103ab | ||
|
|
899d0e0557 | ||
|
|
4aefeff41f | ||
|
|
08d59e50e8 | ||
|
|
e6c06aba8c | ||
|
|
e5c2a04922 | ||
|
|
c134ccf8d9 | ||
|
|
4f959d1ff6 | ||
|
|
1c06d93951 | ||
|
|
b2dff29baa | ||
|
|
72633e9472 | ||
|
|
898a88f7d8 | ||
|
|
c2f09f6666 | ||
|
|
d45bcbc347 | ||
|
|
1f834567f8 | ||
|
|
0b62c04867 | ||
|
|
c71bb2a570 | ||
|
|
0a575b5bf8 | ||
|
|
6f2bf903c5 | ||
|
|
210ad4b8db | ||
|
|
b42cf0e204 | ||
|
|
2045d3aab8 | ||
|
|
e20cb9431d | ||
|
|
7161eaea8b | ||
|
|
b786da52f5 | ||
|
|
ccbfec838d | ||
|
|
f5c1872225 | ||
|
|
3001013bad | ||
|
|
1280803e5e | ||
|
|
f84fc07fe0 | ||
|
|
d83e53f589 | ||
|
|
52ba2c53f1 | ||
|
|
f266e1de4c | ||
|
|
5a0aafd3ce | ||
|
|
c7ce07a43c | ||
|
|
a73644c285 | ||
|
|
e28f1ffc99 | ||
|
|
afc691b2f4 | ||
|
|
00f6eb83c3 | ||
|
|
bbf51ebbec | ||
|
|
7816c8f16e | ||
|
|
fdbcba2412 | ||
|
|
50dc6cb0aa | ||
|
|
e413198d12 | ||
|
|
72592772f9 | ||
|
|
f918206bcd | ||
|
|
6b34d67da8 | ||
|
|
04fb093d4d | ||
|
|
7de0e0cc4a | ||
|
|
5cf7688e38 | ||
|
|
97cd06b52b | ||
|
|
6d522ead1d | ||
|
|
8e0fae62f3 | ||
|
|
af72098d60 | ||
|
|
62da582f4e | ||
|
|
f3efa6eddc | ||
|
|
c85fb7bd0a | ||
|
|
fc7b183461 | ||
|
|
a378810f88 | ||
|
|
2f5b322fad | ||
|
|
efb9b50f85 | ||
|
|
84fdef8eb6 | ||
|
|
76050a261b | ||
|
|
2667361450 | ||
|
|
a2b3313cc0 | ||
|
|
d343446235 | ||
|
|
cbce70f8a4 | ||
|
|
4573d6b6fb | ||
|
|
973305d13c | ||
|
|
4637016372 | ||
|
|
698ba4f7f1 | ||
|
|
502ebabf1f | ||
|
|
13f88626ca | ||
|
|
a39f58f7b5 | ||
|
|
aeb36468ce | ||
|
|
fdd196526d | ||
|
|
ecca892b16 | ||
|
|
03343d0301 | ||
|
|
a61c8b0cb6 | ||
|
|
a92129da00 | ||
|
|
0b783d6390 | ||
|
|
9aa1a2e30e | ||
|
|
15b3c69514 | ||
|
|
f2722e09ec | ||
|
|
b442859be0 | ||
|
|
1496a2efb1 | ||
|
|
a0edb111f0 | ||
|
|
6bcf54fe29 | ||
|
|
3d7e24816a | ||
|
|
f5e02ec63f | ||
|
|
ed1f4ebace | ||
|
|
eb0e496cfd | ||
|
|
8b9fc30a6d | ||
|
|
12c2a6e99c | ||
|
|
714433b62d | ||
|
|
e42933d786 | ||
|
|
e9531e4edd | ||
|
|
0cd9cd294d | ||
|
|
b03fb12fca | ||
|
|
b7b2e8dc4e | ||
|
|
96568abc51 | ||
|
|
a180688858 | ||
|
|
2590067558 | ||
|
|
b5499238f7 | ||
|
|
cc5b96f539 | ||
|
|
32343dc937 | ||
|
|
1e71dd3031 | ||
|
|
ebf6f6a52a | ||
|
|
6bf6d661af | ||
|
|
fe16879f35 | ||
|
|
ead384d1bb | ||
|
|
1ebdc0bacd | ||
|
|
8eca8cdd53 | ||
|
|
24c61b1b37 | ||
|
|
ea18cc7166 | ||
|
|
387c499829 | ||
|
|
339470dd6e | ||
|
|
02d1089dbc | ||
|
|
1bc0932cec | ||
|
|
76cc2739c8 | ||
|
|
b23908aec2 | ||
|
|
6b1b8c0f7b | ||
|
|
06320a7eba | ||
|
|
fc02145d0c | ||
|
|
5360738775 | ||
|
|
0957df752a | ||
|
|
fe56d06b5d | ||
|
|
f4955b16cd | ||
|
|
07692ba73d | ||
|
|
64ac3e8f32 | ||
|
|
8013d3109a | ||
|
|
be6f01dc99 | ||
|
|
30f7779ec6 | ||
|
|
6b5823ca70 | ||
|
|
d4a09ed569 | ||
|
|
84fe409c4b | ||
|
|
51d90891ad | ||
|
|
b1fa06099c | ||
|
|
fa9d8ad6ad | ||
|
|
6703d7b451 | ||
|
|
73afd44d9c | ||
|
|
93cb93bb9b | ||
|
|
82902cff71 | ||
|
|
3657f7e54c | ||
|
|
a57a2f738d | ||
|
|
b93592d703 | ||
|
|
fd288e624b | ||
|
|
095fa3f6ef | ||
|
|
b1f6c578ad | ||
|
|
1bc991bfcb | ||
|
|
02feb478e8 | ||
|
|
c2df79f0c9 | ||
|
|
ef13965747 | ||
|
|
13421601de | ||
|
|
5a9b46c4b5 | ||
|
|
6268642097 | ||
|
|
12eadbc092 | ||
|
|
c9475d1dc2 | ||
|
|
56805defb6 | ||
|
|
477a784201 | ||
|
|
f54bac15c9 | ||
|
|
ae28ebe701 | ||
|
|
f16adf00da | ||
|
|
d17699f6f7 | ||
|
|
8afcf043ee | ||
|
|
dda9d034aa | ||
|
|
652bad51b4 | ||
|
|
4d2ccc0789 | ||
|
|
0e1c21e0f4 | ||
|
|
8d3f58b2cc | ||
|
|
be78905d85 | ||
|
|
b3f232c840 | ||
|
|
075a16c1c3 | ||
|
|
18a13e6266 | ||
|
|
7149bdbc3a | ||
|
|
9a4c4aa9bf | ||
|
|
2b32cab9d1 | ||
|
|
66611db261 | ||
|
|
fdc2095b42 | ||
|
|
2f9cab0da2 | ||
|
|
bd0d474751 | ||
|
|
cca0ab2669 | ||
|
|
cb5ca575d5 | ||
|
|
f4caaad9ee | ||
|
|
b9cfb32a20 | ||
|
|
095e5e5dd6 | ||
|
|
ffc58688d8 | ||
|
|
6a69a7f416 | ||
|
|
b4188b4712 | ||
|
|
4a22c28df4 | ||
|
|
76e9a25b1a | ||
|
|
1928d0823e | ||
|
|
c183d22412 | ||
|
|
b684353721 | ||
|
|
72f0e871c7 | ||
|
|
9a63962903 | ||
|
|
938de28b49 | ||
|
|
20fc094d71 | ||
|
|
40180d5883 | ||
|
|
59e5865318 | ||
|
|
f63d6bdc1d | ||
|
|
fe33c0ae7d | ||
|
|
ca4ad1c1fd | ||
|
|
adf5382804 | ||
|
|
7f5406ac98 | ||
|
|
23b21ea154 | ||
|
|
49d4d0421a | ||
|
|
23859a61bb | ||
|
|
221f81f51e | ||
|
|
6e7c0d5073 | ||
|
|
e8e3d53685 | ||
|
|
e6d9df2b98 | ||
|
|
477a8f2e38 | ||
|
|
5e66697b8b | ||
|
|
16320abb7d | ||
|
|
f122c2832c | ||
|
|
d84c561f44 | ||
|
|
da49c9c045 | ||
|
|
553920e37c | ||
|
|
450d2d113b | ||
|
|
744c80e34d | ||
|
|
c0f8cca7c6 | ||
|
|
b129f220f7 | ||
|
|
7a3df02e38 | ||
|
|
befd29c396 | ||
|
|
b8245621ea | ||
|
|
ecda25a743 | ||
|
|
d97a85b997 | ||
|
|
8c0d7ab9ed | ||
|
|
f3fa73ea34 | ||
|
|
788734ccad | ||
|
|
e088f4a4ad | ||
|
|
86bd018e4e | ||
|
|
283145034d | ||
|
|
163162497e | ||
|
|
56911fb58f | ||
|
|
dae6481aff | ||
|
|
6b2eb5e4f6 | ||
|
|
c563787f73 | ||
|
|
2737755b85 | ||
|
|
fd9486ca77 | ||
|
|
14020ec0b5 | ||
|
|
5a6c466ebd | ||
|
|
76fcd5fe19 | ||
|
|
3732ff1ebc | ||
|
|
22dd09954b | ||
|
|
ef7387f2f3 | ||
|
|
f774298587 | ||
|
|
21c6ea73b2 | ||
|
|
51dc302bb0 | ||
|
|
fb023b81b5 | ||
|
|
9fd342f1e7 | ||
|
|
8988f17765 | ||
|
|
b7dc13d863 | ||
|
|
6e00fa2d01 | ||
|
|
2573434763 | ||
|
|
f153c26fef | ||
|
|
125f461cbe | ||
|
|
b705b4b712 | ||
|
|
c67b0bb858 | ||
|
|
c4d3c8cbfb | ||
|
|
4e16ba5f56 | ||
|
|
7137a2fadb | ||
|
|
9d90d0eaba | ||
|
|
0aa0b3e993 | ||
|
|
f6985c8944 | ||
|
|
4388d56c52 | ||
|
|
a70fe24c97 | ||
|
|
8e0392753c | ||
|
|
b0ec4942bc | ||
|
|
2cbc9675f6 | ||
|
|
94bdb0e3da | ||
|
|
65360c2a1e | ||
|
|
70d30bdbe6 | ||
|
|
66f7e6d1b1 | ||
|
|
5e5baa4892 | ||
|
|
9d1257ed9d | ||
|
|
9d7546053d | ||
|
|
9558fcaf21 |
7
.github/ISSUE_TEMPLATE/bug_report.md
vendored
7
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -20,9 +20,6 @@ Steps to reproduce the behavior:
|
|||||||
**Expected behavior**
|
**Expected behavior**
|
||||||
A clear and concise description of what you expected to happen.
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
** Keepass Database **
|
** Keepass Database **
|
||||||
- Created with: [e.g Windows KeePass 2.42]
|
- Created with: [e.g Windows KeePass 2.42]
|
||||||
- Version: [e.g. 2]
|
- Version: [e.g. 2]
|
||||||
@@ -30,7 +27,7 @@ If applicable, add screenshots to help explain your problem.
|
|||||||
- Size: [e.g. 150Mo]
|
- Size: [e.g. 150Mo]
|
||||||
- Contains attachment: [e.g. Yes]
|
- Contains attachment: [e.g. Yes]
|
||||||
|
|
||||||
**KeePass DX (please complete the following information):**
|
**KeePassDX (please complete the following information):**
|
||||||
- Version: [e.g. 2.5.0.0beta23]
|
- Version: [e.g. 2.5.0.0beta23]
|
||||||
- Build: [e.g. Free]
|
- Build: [e.g. Free]
|
||||||
- Language: [e.g. French]
|
- Language: [e.g. French]
|
||||||
@@ -38,7 +35,7 @@ If applicable, add screenshots to help explain your problem.
|
|||||||
**Android (please complete the following information):**
|
**Android (please complete the following information):**
|
||||||
- Device: [e.g. GalaxyS8]
|
- Device: [e.g. GalaxyS8]
|
||||||
- Version: [e.g. 8.1]
|
- Version: [e.g. 8.1]
|
||||||
- Browser: [e.g. Chrome]
|
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
- Browser for Autofill: [e.g. Chrome version X]
|
||||||
|
|||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -38,6 +38,13 @@ proguard/
|
|||||||
# Android Studio captures folder
|
# Android Studio captures folder
|
||||||
captures/
|
captures/
|
||||||
|
|
||||||
|
# Eclipse/VS Code
|
||||||
|
.project
|
||||||
|
.settings/*
|
||||||
|
*/.project
|
||||||
|
*/.classpath
|
||||||
|
*/.settings/*
|
||||||
|
|
||||||
# Intellij
|
# Intellij
|
||||||
*.iml
|
*.iml
|
||||||
.idea/workspace.xml
|
.idea/workspace.xml
|
||||||
|
|||||||
196
CHANGELOG
196
CHANGELOG
@@ -1,14 +1,148 @@
|
|||||||
KeepassDX (2.5.0.0beta24)
|
KeePassDX(2.8.6)
|
||||||
|
* Fix Autofill recognition #712
|
||||||
|
* Keep value after renaming custom field #709
|
||||||
|
* Prevent random binary bug #713
|
||||||
|
* Fix dialog background #717
|
||||||
|
* Better domain recognition for autofill #702
|
||||||
|
* Write custom data #651
|
||||||
|
* Fix autolink #720
|
||||||
|
|
||||||
|
KeePassDX(2.8.5)
|
||||||
|
* Fix Base 64 #708
|
||||||
|
|
||||||
|
KeePassDX(2.8.4)
|
||||||
|
* Fix incomplete attachment deletion #684
|
||||||
|
* Fix opening database v1 without backup folder #692
|
||||||
|
* Fix ANR during first entry education #685
|
||||||
|
* Entry edition as fragment and manual views to fix focus #686
|
||||||
|
* Fix opening database with corrupted attachment #691
|
||||||
|
* Manage empty keyfile #679
|
||||||
|
|
||||||
|
KeePassDX(2.8.3)
|
||||||
|
* Upload attachments
|
||||||
|
* Visibility button for each hidden field
|
||||||
|
* Fix read header file
|
||||||
|
* Fix deletion in KDB database
|
||||||
|
* Fix minor issues
|
||||||
|
|
||||||
|
KeePassDX(2.8.2)
|
||||||
|
* Fix themes / new UI
|
||||||
|
* Fix multiples notifications
|
||||||
|
* Fix entry in Magikeyboard memory
|
||||||
|
* Fix biometric view visibility
|
||||||
|
* Fix fields order
|
||||||
|
* Upgrade code with ViewModel and LiveData
|
||||||
|
|
||||||
|
KeePassDX(2.8.1)
|
||||||
|
* Capture exceptions in coroutines
|
||||||
|
|
||||||
|
KeePassDX(2.8)
|
||||||
|
* Fix TOTP period (> 60s)
|
||||||
|
* Fix searching in recycle bin
|
||||||
|
* Settings to back to the previous keyboard during database credentials and after form filling
|
||||||
|
* Improve action tasks
|
||||||
|
* Improve recognition to reset app timeout
|
||||||
|
* Fix minor issues
|
||||||
|
|
||||||
|
KeePassDX(2.7)
|
||||||
|
* Add blocklists for autofill
|
||||||
|
* Add autofill compatibility mode (usefull for Browser not compatible)
|
||||||
|
* Upgrade autofill recognition algorithm
|
||||||
|
* Setting to search through web subdomains
|
||||||
|
* Refactoring selection mode
|
||||||
|
|
||||||
|
KeePassDX(2.6)
|
||||||
|
* Share a web domain to automatically search for an entry
|
||||||
|
* Default group icon for a new entry
|
||||||
|
* Better autofill recognition
|
||||||
|
* Fix entry not visually deleted in search
|
||||||
|
* Fix hanged loading dialog
|
||||||
|
* Fix auto open biometric prompt if comes from background
|
||||||
|
* Minor fixes
|
||||||
|
|
||||||
|
KeePassDX(2.5)
|
||||||
|
* First stable version of KeePassDX
|
||||||
|
* Fork completely rewritten from the KeePassDroid project
|
||||||
|
* Fix small issues from the last Release Candidate
|
||||||
|
|
||||||
|
KeePassDX(2.5RC2)
|
||||||
|
* Replacement of Spongy Castle by Bouncy Castle
|
||||||
|
* Update Autofill compatibility
|
||||||
|
* Fix Magikeyboard "Go" action
|
||||||
|
* Fix KeeWeb database opening
|
||||||
|
* Fix default username
|
||||||
|
* Fix themes
|
||||||
|
* Fix small issues
|
||||||
|
|
||||||
|
KeePassDX(2.5RC1)
|
||||||
|
* Add write permission to keep compatibility with old file managers
|
||||||
|
* Fix autofill for apps
|
||||||
|
* Auto search for autofill
|
||||||
|
* New keyfile input
|
||||||
|
* Icon to hide keyfile input
|
||||||
|
* New lock button
|
||||||
|
* Setting to hide lock button in user interface
|
||||||
|
* Clickable links in notes
|
||||||
|
* Fix autofill for key-value pairs
|
||||||
|
|
||||||
|
KeePassDX(2.5beta30)
|
||||||
|
* Fix Lock after screen off (wait 1.5 seconds)
|
||||||
|
* Upgrade autofill algorithm
|
||||||
|
* Fix ANR during file verifications
|
||||||
|
|
||||||
|
KeePassDX(2.5beta29)
|
||||||
|
* Upgrade autofill algorithm
|
||||||
|
* Delete registered KeyFile after save new credentials
|
||||||
|
* Fix title and username entry view refresh after an update
|
||||||
|
* Fix database lock request (open notification always active)
|
||||||
|
* Allow empty title in entries
|
||||||
|
* Add expiration datetime
|
||||||
|
|
||||||
|
KeePassDX(2.5beta28)
|
||||||
|
* Fix read only database
|
||||||
|
* Upgrade to Android SDK 29
|
||||||
|
|
||||||
|
KeePassDX (2.5beta27)
|
||||||
|
* New setting to hide broken links
|
||||||
|
* Show URL when title is empty
|
||||||
|
* Setting to open search field at database opening
|
||||||
|
* Fix settings for database locations
|
||||||
|
* Fix error message when database file not writable
|
||||||
|
* Fix appearance refresh settings
|
||||||
|
* Sort optimization
|
||||||
|
|
||||||
|
KeePassDX (2.5.0.0beta26)
|
||||||
|
* Download attachments
|
||||||
|
* Change file size string format
|
||||||
|
* Prevent screenshot for all screen
|
||||||
|
* Auto performed "Go" key in Magikeyboard
|
||||||
|
* Restore and delete entry history
|
||||||
|
* Setting to hide expired entries
|
||||||
|
* New Black theme
|
||||||
|
* Fix crash when clearing clipboard
|
||||||
|
* Fix attachments compressions
|
||||||
|
* Fix dates
|
||||||
|
* Fix UUID message for Database v1
|
||||||
|
|
||||||
|
KeePassDX (2.5.0.0beta25)
|
||||||
|
* Setting for Recycle Bin
|
||||||
|
* Fix Recycle bin issues
|
||||||
|
* Fix TOTP
|
||||||
|
* Fix infinite save
|
||||||
|
* Fix update group
|
||||||
|
* Fix OOM
|
||||||
|
|
||||||
|
KeePassDX (2.5.0.0beta24)
|
||||||
|
* Add OTP (HOTP / TOTP)
|
||||||
* Add settings (Color, Security, Master Key)
|
* Add settings (Color, Security, Master Key)
|
||||||
* Show history of each entry
|
* Show history of each entry
|
||||||
* Auto repair database for nodes with same UUID
|
* Auto repair database for nodes with same UUID
|
||||||
* Management of expired nodes
|
* Management of expired nodes
|
||||||
* Multi-selection for actions (Cut - Copy - Delete)
|
* Multi-selection for actions (Cut - Copy - Delete)
|
||||||
* Fix settings
|
* Open/Save database as service / Add persistent notification
|
||||||
* Fix edit group
|
* Fix settings / edit group / small bugs
|
||||||
* Fix small bugs
|
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta23)
|
KeePassDX (2.5.0.0beta23)
|
||||||
* New, more secure database creation workflow
|
* New, more secure database creation workflow
|
||||||
* Recognize more database files
|
* Recognize more database files
|
||||||
* Add alias for history files (WARNING: history is erased)
|
* Add alias for history files (WARNING: history is erased)
|
||||||
@@ -17,14 +151,14 @@ KeepassDX (2.5.0.0beta23)
|
|||||||
* Fix OOM with KeyFile
|
* Fix OOM with KeyFile
|
||||||
* Fix small issues
|
* Fix small issues
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta22)
|
KeePassDX (2.5.0.0beta22)
|
||||||
* Rebuild code for actions
|
* Rebuild code for actions
|
||||||
* Add UUID as entry view
|
* Add UUID as entry view
|
||||||
* Fix bug with natural order
|
* Fix bug with natural order
|
||||||
* Fix number of entries in databaseV1
|
* Fix number of entries in databaseV1
|
||||||
* New entry views
|
* New entry views
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta21)
|
KeePassDX (2.5.0.0beta21)
|
||||||
* Fix nested groups no longer visible in V1 databases
|
* Fix nested groups no longer visible in V1 databases
|
||||||
* Improved data import algorithm for V1 databases
|
* Improved data import algorithm for V1 databases
|
||||||
* Add natural database sort
|
* Add natural database sort
|
||||||
@@ -32,10 +166,10 @@ KeepassDX (2.5.0.0beta21)
|
|||||||
* Fix button disabled with only KeyFile
|
* Fix button disabled with only KeyFile
|
||||||
* Show the number of entries in a group
|
* Show the number of entries in a group
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta20)
|
KeePassDX (2.5.0.0beta20)
|
||||||
* Fix a major bug that displays an entry history
|
* Fix a major bug that displays an entry history
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta19)
|
KeePassDX (2.5.0.0beta19)
|
||||||
* Add lock button always visible
|
* Add lock button always visible
|
||||||
* New connection workflow
|
* New connection workflow
|
||||||
* Code refactored in Kotlin
|
* Code refactored in Kotlin
|
||||||
@@ -46,7 +180,7 @@ KeepassDX (2.5.0.0beta19)
|
|||||||
* Fix memory when load database
|
* Fix memory when load database
|
||||||
* Fix small bugs
|
* Fix small bugs
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta18)
|
KeePassDX (2.5.0.0beta18)
|
||||||
* New recent databases views
|
* New recent databases views
|
||||||
* New information dialog
|
* New information dialog
|
||||||
* Custom fields for the Magikeyboard
|
* Custom fields for the Magikeyboard
|
||||||
@@ -55,10 +189,10 @@ KeepassDX (2.5.0.0beta18)
|
|||||||
* Fix memory when opening the database
|
* Fix memory when opening the database
|
||||||
* Memory management for attachments
|
* Memory management for attachments
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta17)
|
KeePassDX (2.5.0.0beta17)
|
||||||
* Fix font and search
|
* Fix font and search
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta16)
|
KeePassDX (2.5.0.0beta16)
|
||||||
* New search in a single fragment
|
* New search in a single fragment
|
||||||
* Search suggestions
|
* Search suggestions
|
||||||
* Added the display of usernames
|
* Added the display of usernames
|
||||||
@@ -66,20 +200,20 @@ KeepassDX (2.5.0.0beta16)
|
|||||||
* Fix read-only mode
|
* Fix read-only mode
|
||||||
* Fix parcelable / toolbar / back
|
* Fix parcelable / toolbar / back
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta15)
|
KeePassDX (2.5.0.0beta15)
|
||||||
* Read only mode
|
* Read only mode
|
||||||
* Best group recovery for the navigation fragment
|
* Best group recovery for the navigation fragment
|
||||||
* Fix copies in notifications
|
* Fix copies in notifications
|
||||||
* Fix orientation
|
* Fix orientation
|
||||||
* Added translations
|
* Added translations
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta14)
|
KeePassDX (2.5.0.0beta14)
|
||||||
* Optimize all the memory with parcelables / fix search
|
* Optimize all the memory with parcelables / fix search
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta13)
|
KeePassDX (2.5.0.0beta13)
|
||||||
* Fix memory issue with parcelable (crash in beta12 version)
|
* Fix memory issue with parcelable (crash in beta12 version)
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta12)
|
KeePassDX (2.5.0.0beta12)
|
||||||
* Added the Magikeyboard to fill the forms (settings still in development)
|
* Added the Magikeyboard to fill the forms (settings still in development)
|
||||||
* Added move and copy for groups and entries
|
* Added move and copy for groups and entries
|
||||||
* New navigation in a single screen / new animations between activities
|
* New navigation in a single screen / new animations between activities
|
||||||
@@ -92,10 +226,10 @@ KeepassDX (2.5.0.0beta12)
|
|||||||
* Fix the fingerprint recognition (WARNING : The keystore is reinit, you must delete the old keys)
|
* Fix the fingerprint recognition (WARNING : The keystore is reinit, you must delete the old keys)
|
||||||
* Fix small bugs
|
* Fix small bugs
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta11)
|
KeePassDX (2.5.0.0beta11)
|
||||||
* Fix crash in beta10 version
|
* Fix crash in beta10 version
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta10)
|
KeePassDX (2.5.0.0beta10)
|
||||||
* Dynamically change Algorithm and Key Derivation Function in settings
|
* Dynamically change Algorithm and Key Derivation Function in settings
|
||||||
* Upgrade translations
|
* Upgrade translations
|
||||||
* New red volcano theme, fix classic dark theme
|
* New red volcano theme, fix classic dark theme
|
||||||
@@ -103,7 +237,7 @@ KeepassDX (2.5.0.0beta10)
|
|||||||
* Update fingerprint state with checkbox
|
* Update fingerprint state with checkbox
|
||||||
* Fix bugs
|
* Fix bugs
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta9)
|
KeePassDX (2.5.0.0beta9)
|
||||||
* Education Screens to learn how to use the app
|
* Education Screens to learn how to use the app
|
||||||
* New designs
|
* New designs
|
||||||
* New custom font for character visibility
|
* New custom font for character visibility
|
||||||
@@ -112,9 +246,9 @@ KeepassDX (2.5.0.0beta9)
|
|||||||
* Change setting organisation
|
* Change setting organisation
|
||||||
* Pro version
|
* Pro version
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta8)
|
KeePassDX (2.5.0.0beta8)
|
||||||
* Hide custom entries protected
|
* Hide custom entries protected
|
||||||
* Best management of field references (https://keepass.info/help/base/fieldrefs.html)
|
* Best management of field references (https://KeePass.info/help/base/fieldrefs.html)
|
||||||
* Change database / default settings
|
* Change database / default settings
|
||||||
* Add Autofill for search
|
* Add Autofill for search
|
||||||
* Add sorting by last access and by creation time
|
* Add sorting by last access and by creation time
|
||||||
@@ -122,7 +256,7 @@ KeepassDX (2.5.0.0beta8)
|
|||||||
* Refactor old code
|
* Refactor old code
|
||||||
* Fix bugs
|
* Fix bugs
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta7)
|
KeePassDX (2.5.0.0beta7)
|
||||||
* Rebuild Notifications
|
* Rebuild Notifications
|
||||||
* Change links to https
|
* Change links to https
|
||||||
* Add extended Ascii (ñæËÌÂÝÜ...)
|
* Add extended Ascii (ñæËÌÂÝÜ...)
|
||||||
@@ -131,10 +265,10 @@ KeepassDX (2.5.0.0beta7)
|
|||||||
* Add setting to prevent the password copy
|
* Add setting to prevent the password copy
|
||||||
* Fix bugs
|
* Fix bugs
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta6)
|
KeePassDX (2.5.0.0beta6)
|
||||||
* Fix crash
|
* Fix crash
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta5)
|
KeePassDX (2.5.0.0beta5)
|
||||||
* Autofill (Android O)
|
* Autofill (Android O)
|
||||||
* Deletion for group
|
* Deletion for group
|
||||||
* New sorts with (Asc/Dsc, Groups before or after)
|
* New sorts with (Asc/Dsc, Groups before or after)
|
||||||
@@ -155,7 +289,7 @@ KeepassDX (2.5.0.0beta5)
|
|||||||
* Fix many small bugs
|
* Fix many small bugs
|
||||||
* Add recycle bin setting (not yet accessible)
|
* Add recycle bin setting (not yet accessible)
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta4)
|
KeePassDX (2.5.0.0beta4)
|
||||||
* Show only file name
|
* Show only file name
|
||||||
* Setting for full path
|
* Setting for full path
|
||||||
* Add information for each database file
|
* Add information for each database file
|
||||||
@@ -164,7 +298,7 @@ KeepassDX (2.5.0.0beta4)
|
|||||||
* Delete view assignment for fingerprint opening
|
* Delete view assignment for fingerprint opening
|
||||||
* Merge KeePassDroid 2.2.1
|
* Merge KeePassDroid 2.2.1
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta3)
|
KeePassDX (2.5.0.0beta3)
|
||||||
* New database workflow with new screens and folder selection
|
* New database workflow with new screens and folder selection
|
||||||
* Settings for default password generation
|
* Settings for default password generation
|
||||||
* Fingerprint dialog for explanations
|
* Fingerprint dialog for explanations
|
||||||
@@ -175,17 +309,17 @@ KeepassDX (2.5.0.0beta3)
|
|||||||
* Merge KeePassDroid 2.2.0.9
|
* Merge KeePassDroid 2.2.0.9
|
||||||
* Add corruption fix mode
|
* Add corruption fix mode
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta2)
|
KeePassDX (2.5.0.0beta2)
|
||||||
* Remove libs for F-Droid
|
* Remove libs for F-Droid
|
||||||
|
|
||||||
KeepassDX (2.5.0.0beta1)
|
KeePassDX (2.5.0.0beta1)
|
||||||
* Fork KeepassDroid
|
* Fork KeePassDroid
|
||||||
* Add Material Design
|
* Add Material Design
|
||||||
* Add Light and Night theme
|
* Add Light and Night theme
|
||||||
* Min API is 14
|
* Min API is 14
|
||||||
* Solve bug for fingerprint
|
* Solve bug for fingerprint
|
||||||
* Update French translation
|
* Update French translation
|
||||||
* Change donation (see KeepassDroid to contribute on both projects)
|
* Change donation (see KeePassDroid to contribute on both projects)
|
||||||
|
|
||||||
KeePassDroid (2.2.1)
|
KeePassDroid (2.2.1)
|
||||||
* Fix kdbx4 date corruption
|
* Fix kdbx4 date corruption
|
||||||
@@ -446,7 +580,7 @@ KeePassDroid (1.9.10)
|
|||||||
|
|
||||||
KeePassDroid (1.9.9)
|
KeePassDroid (1.9.9)
|
||||||
* Go back to explicitly storing blank fields in the database
|
* Go back to explicitly storing blank fields in the database
|
||||||
(works around bug in keepassx)
|
(works around bug in KeePassx)
|
||||||
* Add support for native code on MIPS architectures
|
* Add support for native code on MIPS architectures
|
||||||
* Adding Vibrate permission. On some devices notifications fail
|
* Adding Vibrate permission. On some devices notifications fail
|
||||||
without the vibrate permission.
|
without the vibrate permission.
|
||||||
|
|||||||
4
LICENSE
4
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
KeePass DX is free software: you can redistribute it and/or modify
|
KeePassDX is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
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.
|
||||||
@@ -13,7 +13,7 @@ KeePass DX is free software: you can redistribute it and/or modify
|
|||||||
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 this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
The KeePass DX icon was created by Jeremy JAMET and is licensed under the terms of GPLv3.
|
The KeePassDX icon was created by Jeremy JAMET and is licensed under the terms of GPLv3.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
177
LICENSES/LICENSE_ANDROID_BACKUP_SERVICE
Normal file
177
LICENSES/LICENSE_ANDROID_BACKUP_SERVICE
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
Terms of Service
|
||||||
|
|
||||||
|
This is the terms of service for the Android Backup Service.
|
||||||
|
|
||||||
|
1. Your relationship with Google
|
||||||
|
|
||||||
|
1.1 Your use of the Android Backup Service (referred to as the "Service" in this document) is subject to the terms of a legal agreement between you and Google. "Google" means Google LLC, whose principal place of business is at 1600 Amphitheatre Parkway, Mountain View, CA 94043, United States. This document explains how the agreement is made up, and sets out some of the terms of that agreement.
|
||||||
|
|
||||||
|
1.2 Unless otherwise agreed in writing with Google, your agreement with Google will always include, at a minimum, the terms and conditions set out in this document. These are referred to below as the "Terms".
|
||||||
|
|
||||||
|
1.3 The Terms form a legally binding agreement between you and Google in relation to your use of the Service. It is important that you take the time to read them carefully.
|
||||||
|
|
||||||
|
2. Accepting the Terms
|
||||||
|
|
||||||
|
2.1 In order to use the Service, you must first agree to the Terms. You may not use the Service if you do not accept the Terms.
|
||||||
|
|
||||||
|
2.2 You can accept the Terms by clicking to accept or agree to the Terms, where this option is made available to you by Google.
|
||||||
|
|
||||||
|
2.3 You may not use the Service and may not accept the Terms if you are not of legal age to form a binding contract with Google.
|
||||||
|
|
||||||
|
2.4 You represent that you have full power, capacity and authority to accept these Terms. If you are accepting on behalf of your employer or another entity, you represent that you have full legal authority to bind your employer or such entity to these Terms. If you don't have the legal authority to bind, please ensure that an authorized person from your entity consents to and accepts these Terms.
|
||||||
|
|
||||||
|
3. Provision of the Service by Google
|
||||||
|
|
||||||
|
3.1 Google has subsidiaries and affiliated legal entities around the world ("Subsidiaries and Affiliates"). Sometimes, these companies will be providing the Service to you on behalf of Google itself. You acknowledge and agree that Subsidiaries and Affiliates will be entitled to provide the Service to you.
|
||||||
|
|
||||||
|
3.2 Google is constantly innovating in order to provide the best possible experience for its users. You acknowledge and agree that the form and nature of the Service which Google provides may change from time to time without prior notice to you.
|
||||||
|
|
||||||
|
3.3 As part of this continuing innovation, you acknowledge and agree that Google may stop (permanently or temporarily) providing the Service (or any features within the Service) to you or to users generally at Google's sole discretion, without prior notice to you. You may stop using the Service at any time. You do not need to specifically inform Google when you stop using the Service.
|
||||||
|
|
||||||
|
3.4 You acknowledge and agree that if Google disables your Backup Service Key, you and the Android application(s) you developed ("Application(s)") may be prevented from accessing the Service and any content that is stored with the Service.
|
||||||
|
|
||||||
|
3.5 You acknowledge and agree that Google may set a fixed upper limit on the number of backup transmissions you may send or receive through the Service or on the amount of storage space used for the provision of the Service at any time, at Google's discretion. You agree to abide by any such fixed upper limits.
|
||||||
|
|
||||||
|
4. Use of the Service by you
|
||||||
|
|
||||||
|
4.1 In order to access the Service, you must have a unique application identifier ("Package Name") for your Application as described in the documentation for the Service.
|
||||||
|
|
||||||
|
4.2 After supplying Google with the Package Name and accepting the Terms, you will be issued an alphanumeric key ("Backup Service Key") assigned to you by Google that is uniquely associated with your Application. Your Application must include this Backup Service Key as described in the documentation for the Service.
|
||||||
|
|
||||||
|
4.3 There is currently no limit to the number of Backup Service Keys you may obtain in this manner provided that you use a different Package Name for each Backup Service Key you obtain. You agree that each Backup Service Key is only valid for Applications with the corresponding Package Name. You agree that Google may, in its sole discretion, impose a limit on the number of Backup Service Keys that may be obtained in the future. You agree that your continued use of any of the Backup Service Keys assigned by Google, or distribution of any Applications using such Backup Service Keys, constitutes your continued agreement to these Terms.
|
||||||
|
|
||||||
|
4.4 You agree to use the Service only for purposes that are permitted by (a) the Terms and (b) any applicable law, regulation, third-party terms of service, or generally accepted practices or guidelines in the relevant jurisdictions (including any laws regarding the export of data or software to and from the United States or other relevant countries).
|
||||||
|
|
||||||
|
4.5 You agree not to access (or attempt to access) any of the Service by any means other than through the interfaces, methods, and APIs that are provided by Google, unless you have been specifically allowed to do so in a separate agreement with Google.
|
||||||
|
|
||||||
|
4.6 You agree that you will not engage in any activity that interferes with or disrupts the Service (or the servers and networks which are connected to the Service), or the servers or networks of any third-party.
|
||||||
|
|
||||||
|
4.7 You agree that your use of the Service will be in compliance with any documentation guidelines provided by Google and that failure to comply with the documentation guidelines may result in the disabling of the Backup Service Key(s) for your Application(s).
|
||||||
|
|
||||||
|
4.8 Unless you have been specifically permitted to do so in a separate agreement with Google, you agree that you will not reproduce, duplicate, copy, sell, trade or resell (a) use of the Service, or (b) access to the Service.
|
||||||
|
|
||||||
|
4.9 You agree that you are solely responsible for (and that Google has no responsibility to you or to any third party for) your and your Application's use of the Service, any breach of your obligations under the Terms, and for the consequences (including any loss or damage which Google may suffer) of any such breach.
|
||||||
|
|
||||||
|
4.10 You agree that in your use of the Service, you and your Applications will protect the privacy and legal rights of users. You must provide legally adequate privacy notice and protection for users whose data your Applications back up to the Service. Further, your Application may only use that information for the limited purpose of backing up the data to the Service unless the user has given you permission for further use. If the user has not given you permission to back up information to the Service, you may not transmit such information to the Service.
|
||||||
|
|
||||||
|
4.11 You agree that you and your Applications will not transmit or store sensitive user information, such as user names, passwords, or credit card numbers, through the Service.
|
||||||
|
|
||||||
|
5. Security
|
||||||
|
|
||||||
|
5.1 You agree and understand that you are responsible for maintaining the security associated with any information you provide to access the Service as well as of the Backup Service Key(s) assigned to you by Google. You agree that only you are authorized to use the Backup Service Key(s) assigned to you.
|
||||||
|
|
||||||
|
5.2 Accordingly, you agree that you will be solely responsible to Google for all activities that occur in connection with your access to the Service, as well as the Backup Service Key.
|
||||||
|
|
||||||
|
5.3 If you become aware of any unauthorized use of your Backup Service Key(s) you agree to notify Google immediately.
|
||||||
|
|
||||||
|
6. Privacy and your personal information
|
||||||
|
|
||||||
|
6.1 For information about Google's data protection practices, please read Google's privacy policy at http://www.google.com/privacy.html. This policy explains how Google treats your personal information when you use the Service.
|
||||||
|
|
||||||
|
6.2 You agree to the use of your data in accordance with Google's privacy policies.
|
||||||
|
|
||||||
|
7. Content in the Service
|
||||||
|
|
||||||
|
7.1 You agree that you are solely responsible for (and that Google has no responsibility to you or to any third party for) any Content that you or your Applications transmit or store through the Service and for the consequences of your actions (including any loss or damage which Google may suffer) by doing so. You agree that you are solely responsible for (A) any Content that is transmitted through the Service by your Applications, and (B) any Content that Devices retrieve from the Service by virtue of your Applications. For purposes of the Terms, "Content" means information such as data, messages, settings information, written text, computer software, music, audio files or other sounds, photographs, videos or other images. "Device(s)" means device(s) powered by the Android operating system.
|
||||||
|
|
||||||
|
7.2 You agree that you will not transmit any Content through the Service that is copyrighted, protected by trade secret or otherwise subject to third party proprietary rights, including patent, privacy and publicity rights, unless you are the owner of such rights or have permission from their rightful owner to transmit the Content through the Service.
|
||||||
|
|
||||||
|
8. Proprietary rights
|
||||||
|
|
||||||
|
8.1 You acknowledge and agree that Google (or Google's licensors) own all legal right, title and interest in and to the Service, including any intellectual property rights which subsist in the Service (whether those rights happen to be registered or not, and wherever in the world those rights may exist).
|
||||||
|
|
||||||
|
8.2 Unless you have agreed otherwise in writing with Google, nothing in the Terms gives you a right to use any of Google's trade names, trademarks, service marks, logos, domain names, and other distinctive brand features.
|
||||||
|
|
||||||
|
8.3 If you have been given an explicit right to use any of these brand features in a separate written agreement with Google, then you agree that your use of such features shall be in compliance with that agreement, any applicable provisions of the Terms, and Google's brand feature use guidelines as updated from time to time. These guidelines can be viewed online at http://www.google.com/permissions/ guidelines.html (or such other URL as Google may provide for this purpose from time to time).
|
||||||
|
|
||||||
|
8.4 You agree that you shall not remove, obscure, or alter any proprietary rights notices (including copyright, trade mark notices) which may be affixed to or contained within the Service.
|
||||||
|
|
||||||
|
9. License from Google
|
||||||
|
|
||||||
|
9.1 Subject to terms and conditions of these Terms, Google gives you a personal, worldwide, royalty-free, non-assignable and non-exclusive license to use the Service as provided to you by Google. This license is for the sole purpose of enabling you to use and enjoy the benefit of the Service as provided by Google, in the manner permitted by the Terms.
|
||||||
|
|
||||||
|
9.2 You may not (and you may not permit anyone else to) copy, modify, create a derivative work of, reverse engineer, decompile or otherwise attempt to extract the source code from the Service or any part thereof, unless this is expressly permitted or required by law, or unless you have been specifically told that you may do so by Google, in writing.
|
||||||
|
|
||||||
|
9.3 Unless Google has given you specific written permission to do so, you may not assign (or grant a sub-license of) your rights to use the Service, grant a security interest in or over your rights to use the Service, or otherwise transfer any part of your rights to use the Service.
|
||||||
|
|
||||||
|
10. Your code
|
||||||
|
|
||||||
|
10.1 Google claims no ownership or control over any source code written by you to be used with the Service. You retain copyright and any other rights you already hold in this code, and you are responsible for protecting those rights, as appropriate.
|
||||||
|
|
||||||
|
11. Ending your relationship with Google
|
||||||
|
|
||||||
|
11.1 The Terms will continue to apply until terminated by either you or Google as set out below.
|
||||||
|
|
||||||
|
11.2 You may terminate your legal agreement with Google by discontinuing your use of the Service at any time.
|
||||||
|
|
||||||
|
11.3 Google may, at any time, terminate its legal agreement with you if:
|
||||||
|
|
||||||
|
(A) you have breached any provision of the Terms (or have acted in manner which clearly shows that you do not intend to, or are unable to comply with the provisions of the Terms); or
|
||||||
|
|
||||||
|
(B) Google is required to do so by law (for example, where the provision of the Service to you is, or becomes, unlawful); or
|
||||||
|
|
||||||
|
(C) Google is transitioning to no longer providing the Service; or
|
||||||
|
|
||||||
|
(D) your Application fails to meet the documentation guidelines provided by Google.
|
||||||
|
|
||||||
|
11.4 Nothing in this Section shall affect Google's rights regarding provision of the Service under Section 3 of the Terms.
|
||||||
|
|
||||||
|
11.5 When these Terms come to an end, all of the legal rights, obligations and liabilities that you and Google have benefited from, been subject to (or which have accrued over time whilst the Terms have been in force) or which are expressed to continue indefinitely, shall be unaffected by this cessation, and the provisions of Sections 12, 13 and Paragraph 16 shall continue to apply to such rights, obligations and liabilities indefinitely.
|
||||||
|
|
||||||
|
12. EXCLUSION OF WARRANTIES
|
||||||
|
|
||||||
|
12.1 NOTHING IN THESE TERMS, INCLUDING SECTIONS 12 AND 13, SHALL EXCLUDE OR LIMIT GOOGLE'S WARRANTY OR LIABILITY FOR LOSSES WHICH MAY NOT BE LAWFULLY EXCLUDED OR LIMITED BY APPLICABLE LAW. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR CONDITIONS OR THE LIMITATION OR EXCLUSION OF LIABILITY FOR LOSS OR DAMAGE CAUSED BY NEGLIGENCE, BREACH OF CONTRACT OR BREACH OF IMPLIED TERMS, OR INCIDENTAL OR CONSEQUENTIAL DAMAGES. ACCORDINGLY, ONLY THE LIMITATIONS WHICH ARE LAWFUL IN YOUR JURISDICTION WILL APPLY TO YOU AND OUR LIABILITY WILL BE LIMITED TO THE MAXIMUM EXTENT PERMITTED BY LAW.
|
||||||
|
|
||||||
|
12.2 YOU EXPRESSLY UNDERSTAND AND AGREE THAT YOUR USE OF THE SERVICE IS AT YOUR SOLE RISK AND THAT THE SERVICE AND CONTENT ARE PROVIDED "AS IS" AND "AS AVAILABLE".
|
||||||
|
|
||||||
|
12.3 IN PARTICULAR, GOOGLE, ITS SUBSIDIARIES AND AFFILIATES, AND ITS LICENSORS DO NOT REPRESENT OR WARRANT TO YOU THAT:
|
||||||
|
|
||||||
|
(A) YOUR USE OF THE SERVICE WILL MEET YOUR REQUIREMENTS,
|
||||||
|
|
||||||
|
(B) YOUR USE OF THE SERVICE WILL BE UNINTERRUPTED, TIMELY, SECURE OR FREE FROM ERROR, AND
|
||||||
|
|
||||||
|
(C) THAT DEFECTS IN THE OPERATION OR FUNCTIONALITY OF ANY SOFTWARE PROVIDED TO YOU AS PART OF THE SERVICE WILL BE CORRECTED.
|
||||||
|
|
||||||
|
12.4 NO ADVICE OR INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED BY YOU FROM GOOGLE OR THROUGH OR FROM THE SERVICE SHALL CREATE ANY WARRANTY NOT EXPRESSLY STATED IN THE TERMS.
|
||||||
|
|
||||||
|
12.5 GOOGLE FURTHER EXPRESSLY DISCLAIMS ALL WARRANTIES AND CONDITIONS OF ANY KIND, WHETHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
|
||||||
|
|
||||||
|
13. LIMITATION OF LIABILITY
|
||||||
|
|
||||||
|
13.1 SUBJECT TO OVERALL PROVISION IN PARAGRAPH 12.1 ABOVE, YOU EXPRESSLY UNDERSTAND AND AGREE THAT GOOGLE, ITS SUBSIDIARIES AND AFFILIATES, AND ITS LICENSORS SHALL NOT BE LIABLE TO YOU FOR:
|
||||||
|
|
||||||
|
(A) ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL CONSEQUENTIAL OR EXEMPLARY DAMAGES WHICH MAY BE INCURRED BY YOU, HOWEVER CAUSED AND UNDER ANY THEORY OF LIABILITY. THIS SHALL INCLUDE, BUT NOT BE LIMITED TO, ANY LOSS OF PROFIT (WHETHER INCURRED DIRECTLY OR INDIRECTLY), ANY LOSS OF GOODWILL OR BUSINESS REPUTATION, ANY LOSS OF DATA SUFFERED, COST OF PROCUREMENT OF SUBSTITUTE GOODS OR SERVICE, OR OTHER INTANGIBLE LOSS;
|
||||||
|
|
||||||
|
(B) ANY LOSS OR DAMAGE WHICH MAY BE INCURRED BY YOU, INCLUDING BUT NOT LIMITED TO LOSS OR DAMAGE AS A RESULT OF:
|
||||||
|
|
||||||
|
(I) ANY CHANGES WHICH GOOGLE MAY MAKE TO THE SERVICE, OR FOR ANY PERMANENT OR TEMPORARY CESSATION IN THE PROVISION OF THE SERVICE (OR ANY FEATURES WITHIN THE SERVICE);
|
||||||
|
|
||||||
|
(II) THE DELETION OF, CORRUPTION OF, OR FAILURE TO STORE, ANY CONTENT AND OTHER COMMUNICATIONS DATA MAINTAINED OR TRANSMITTED BY OR THROUGH YOUR USE OF THE SERVICE;
|
||||||
|
|
||||||
|
(III) YOUR FAILURE TO PROVIDE GOOGLE WITH ACCURATE ACCOUNT INFORMATION; OR
|
||||||
|
|
||||||
|
(IV) YOUR FAILURE TO KEEP YOUR PASSWORD OR ACCOUNT DETAILS SECURE AND CONFIDENTIAL.
|
||||||
|
|
||||||
|
13.2 THE LIMITATIONS ON GOOGLE'S LIABILITY TO YOU IN PARAGRAPH 13.1 ABOVE SHALL APPLY WHETHER OR NOT GOOGLE HAS BEEN ADVISED OF OR SHOULD HAVE BEEN AWARE OF THE POSSIBILITY OF ANY SUCH LOSSES ARISING.
|
||||||
|
|
||||||
|
14. Indemnification
|
||||||
|
|
||||||
|
14.1 You agree to hold harmless and indemnify Google, and its subsidiaries, affiliates, officers, agents, employees, or licensors from and against any third party claim arising from or in any way related to (a) your breach of the Terms, (b) your use of the Service, or (c) your violation of applicable laws, rules or regulations in connection with the Service, including any liability or expense arising from all claims, losses, damages (actual and consequential), suits, judgments, litigation costs and attorneys' fees, of every kind and nature. In such a case, Google will provide you with written notice of such claim, suit or action.
|
||||||
|
|
||||||
|
15. Changes to the Terms
|
||||||
|
|
||||||
|
15.1 Due to things like changes to the law or changes to functionality offered through the Service, Google may need to change these Terms from time to time. You should look at the Terms regularly. We'll post notice of the modified Terms within, or through, the Service. Once the modified Terms are posted, the changes will become effective immediately, and you are deemed to have accepted the modified Terms if you continue to use the Service. If you do not agree to the modified Terms for the Service, please stop using the Service.
|
||||||
|
|
||||||
|
16. General legal terms
|
||||||
|
|
||||||
|
16.1 The Terms constitute the whole legal agreement between you and Google and govern your use of the Service (but excluding any service which Google may provide to you under a separate written agreement), and completely replace any prior agreements between you and Google in relation to the Service.
|
||||||
|
|
||||||
|
16.2 You agree that Google may provide you with notices, including those regarding changes to the Terms, by email, regular mail, or postings on the Service.
|
||||||
|
|
||||||
|
16.3 You agree that if Google does not exercise or enforce any legal right or remedy which is contained in the Terms (or which Google has the benefit of under any applicable law), this will not be taken to be a formal waiver of Google's rights and that those rights or remedies will still be available to Google.
|
||||||
|
|
||||||
|
16.4 If any court of law, having the jurisdiction to decide on this matter, rules that any provision of these Terms is invalid, then that provision will be removed from the Terms without affecting the rest of the Terms. The remaining provisions of the Terms will continue to be valid and enforceable.
|
||||||
|
|
||||||
|
16.5 You acknowledge and agree that each member of the group of companies of which Google is the parent shall be third party beneficiaries to the Terms and that such other companies shall be entitled to directly enforce, and rely upon, any provision of the Terms which confers a benefit on (or rights in favor of) them. Other than this, no other person or company shall be third party beneficiaries to the Terms.
|
||||||
|
|
||||||
|
16.6 The Terms, and your relationship with Google under the Terms, shall be governed by the laws of the State of California without regard to its conflict of laws provisions. You and Google agree to submit to the exclusive jurisdiction of the courts located within the county of Santa Clara, California to resolve any legal matter arising from the Terms. Notwithstanding this, you agree that Google shall still be allowed to apply for injunctive remedies (or an equivalent type of urgent legal relief) in any jurisdiction.
|
||||||
91
README.md
Normal file
91
README.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# Android KeepassDX
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> KeepassDX is a **multi-format KeePass manager for Android devices**. The app allows creating keys and passwords in a secure way by integrating with the Android design standards.
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Create database files / entries and groups.
|
||||||
|
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
|
||||||
|
- **Compatible** with the majority of alternative programs (KeePass, KeePassX, KeePassXC, …).
|
||||||
|
- Allows opening and **copying URI / URL fields quickly**.
|
||||||
|
- **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*.
|
||||||
|
- **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA).
|
||||||
|
- Material design with **themes**.
|
||||||
|
- **Auto-Fill** and Integration.
|
||||||
|
- Field filling **keyboard**.
|
||||||
|
- **History** of each entry.
|
||||||
|
- Precise management of **settings**.
|
||||||
|
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
|
||||||
|
|
||||||
|
KeepassDX is **open source** and **ad-free**.
|
||||||
|
|
||||||
|
## What is KeePassDX?
|
||||||
|
|
||||||
|
An alternative to remembering an endless list of passwords manually. This is made more difficult by **using different passwords for each account**. If you use one password everywhere and security fails only one of those places, it grants access to your e-mail account, website, etc, and you may not know about it or notice, before bad things happen.
|
||||||
|
|
||||||
|
KeePassDX is a **password manager for Android**, which helps you **manage your passwords in a secure way**. You can put all your passwords in one database, locked with a **master key** and/or a **keyfile**. You **only have to remember one single master password and/or select the keyfile** to unlock the whole database. The databases are encrypted using the best and **most secure encryption algorithms** currently known.
|
||||||
|
|
||||||
|
## Small print?
|
||||||
|
|
||||||
|
KeePassDX is under **open source GPL3 license**, meaning you can use, study, change and share it at will. Copyleft ensures it stays that way.
|
||||||
|
From the full source, anyone can build, fork, and check whether for example the encryption algorithms are implemented correctly.
|
||||||
|
There is **no advertising**.
|
||||||
|
|
||||||
|
Do not worry, **the main features remain completely free**.
|
||||||
|
|
||||||
|
Optional visual styles are accessible after a contribution (and a congratulatory message (Ո‿Ո) ) or the purchase of an extended version to encourage contribution to the work of open source projects!
|
||||||
|
*If you contribute to the project and do not have access to the styles, do not hesitate to contact the author at [contact@kunzisoft.com](contact@kunzisoft.com).*
|
||||||
|
|
||||||
|
## Contributions
|
||||||
|
|
||||||
|
* Add features by making a **[pull request](https://help.github.com/articles/about-pull-requests/)**.
|
||||||
|
* Help to **[translate](https://hosted.weblate.org/projects/keepass-dx/strings/)** KeePassDX to your language (on [Weblate](https://hosted.weblate.org/projects/keepass-dx/) or by sending a [pull request](https://help.github.com/articles/about-pull-requests/)).
|
||||||
|
* **[Donate](https://www.kunzisoft.com/donation)** 人◕ ‿‿ ◕人Y for a better service and a quick development of your features.
|
||||||
|
* Buy the **[Pro version](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro)** of KeePassDX.
|
||||||
|
|
||||||
|
## Download
|
||||||
|
|
||||||
|
*[F-Droid](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/) is the recommended way of installing, a libre software project that verifies that all the libraries and app code is libre software.*
|
||||||
|
|
||||||
|
[<img src="https://f-droid.org/badge/get-it-on.png"
|
||||||
|
alt="Get it on F-Droid"
|
||||||
|
height="80">](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/)
|
||||||
|
|
||||||
|
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
|
||||||
|
alt="Get it on Google Play"
|
||||||
|
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
|
||||||
|
|
||||||
|
## Frequently Asked Questions
|
||||||
|
|
||||||
|
Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/wiki/FAQ)
|
||||||
|
|
||||||
|
## Other devices
|
||||||
|
|
||||||
|
- [KeePass](https://keepass.info/) (https://keepass.info/) is the original and official project for the desktop, with technical documentation for standardized database files. It is updated regularly with active maintenance (written in C#).
|
||||||
|
|
||||||
|
- [KeePassXC](https://keepassxc.org/) (https://keepassxc.org/) is an alternative integration of KeePass written in C++.
|
||||||
|
|
||||||
|
- [KeeWeb](https://keeweb.info/) (https://keeweb.info/) is a web version that is also compatible with KeePass files.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Copyright © 2020 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||||
|
|
||||||
|
This file is part of KeePassDX.
|
||||||
|
|
||||||
|
[KeePassDX](https://www.keepassdx.com) is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
*This project is a fork of [KeepassDroid](https://github.com/bpellin/keepassdroid) by bpellin.*
|
||||||
84
ReadMe.md
84
ReadMe.md
@@ -1,84 +0,0 @@
|
|||||||
# Android Keepass DX
|
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> Keepass DX is a **multi-format KeePass manager for Android devices**. The application allows to create keys and passwords in a secure way by integrating with the Android design standards.
|
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Create database files / entries and groups
|
|
||||||
* Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm
|
|
||||||
* **Compatible** with the majority of alternative programs (KeePass, KeePassX, KeePass XC...)
|
|
||||||
* Allows **fast copy** of fields and opening of URI / URL
|
|
||||||
* **Fingerprint** for fast unlocking
|
|
||||||
* Material design with **themes**
|
|
||||||
* **AutoFill** and Integration
|
|
||||||
* Field filling **keyboard**
|
|
||||||
* Precise management of **settings**
|
|
||||||
* Code written in **native language** *(Kotlin / Java / JNI / C)*
|
|
||||||
|
|
||||||
Keepass DX is **open source** and **ad-free**.
|
|
||||||
|
|
||||||
## What is KeePass DX?
|
|
||||||
|
|
||||||
Today you need to remember many passwords. You need a password for your e-mail account, your website's FTP password, online passwords (like website member account), etc. etc. etc. The list is endless. Also, you **should use different passwords for each account**. Because if you use only one password everywhere and someone gets this password you have a problem... A serious problem. The thief would have access to your e-mail account, website, etc. Unimaginable.
|
|
||||||
|
|
||||||
KeePass DX is a **free open source password manager for Android**, which helps you to **manage your passwords in a secure way**. You can put all your passwords in one database, which is locked with one **master key** or a **key file**. So you **only have to remember one single master password or select the key file** to unlock the whole database. The databases are encrypted using the best and **most secure encryption algorithms** currently known.
|
|
||||||
|
|
||||||
## Is it really free?
|
|
||||||
|
|
||||||
Yes, KeePass DX is under **free license (OSI certified)** and **without advertising**. You can have a look at its full source and check whether the encryption algorithms are implemented correctly.
|
|
||||||
|
|
||||||
*Note : If you access the application from a store, visual features may not be available to incentivize the contribution to the work of open source projects. These optional visuals are accessible after a donation (and a small congratulation message :) or the purchase of an extended version, but do not worry, the main features remain completely free. If you contribute to the project and you do not have access to the themes, do not hesitate to contact me at [contact@kunzisoft.com](contact@kunzisoft.com), I will give you the procedure.*
|
|
||||||
|
|
||||||
## Contributions
|
|
||||||
|
|
||||||
You can contribute in different ways to help us on our work.
|
|
||||||
|
|
||||||
* Add features by a **[pull request](https://help.github.com/articles/about-pull-requests/)**.
|
|
||||||
* Help to **[translate](https://hosted.weblate.org/projects/keepass-dx/strings/)** into your language (By using [Weblate](https://hosted.weblate.org/projects/keepass-dx/) or with a manual [pull request](https://help.github.com/articles/about-pull-requests/))
|
|
||||||
* **[Donate](https://www.kunzisoft.com/donation)** 人◕ ‿‿ ◕人Y for a better service and a quick development of your features.
|
|
||||||
* Buy the **[Pro version](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro)** of KeePass DX
|
|
||||||
|
|
||||||
## Download
|
|
||||||
|
|
||||||
*We recommend the installation from [F-Droid](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/) which verifies that all libraries and application code are open source.*
|
|
||||||
|
|
||||||
[<img src="https://f-droid.org/badge/get-it-on.png"
|
|
||||||
alt="Get it on F-Droid"
|
|
||||||
height="80">](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/)
|
|
||||||
|
|
||||||
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
|
|
||||||
alt="Get it on Google Play"
|
|
||||||
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
|
|
||||||
|
|
||||||
## F.A.Q.
|
|
||||||
|
|
||||||
Other questions? You can read the [F.A.Q.](https://www.keepassdx.com/FAQ)
|
|
||||||
|
|
||||||
## Other devices
|
|
||||||
|
|
||||||
- [KeePass XC](https://keepassxc.org/) (https://keepassxc.org/) works with **GNU/Linux**, **Mac** and **Windows**, is updated regularly and under the terms of the GNU General Public License. This is the recommended version for computers.
|
|
||||||
|
|
||||||
- [KeePass](https://keepass.info/) (https://keepass.info/) is the historical project, with good technical documentation for standardized database files but only running on **Windows**.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Copyright (c) 2019 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
|
||||||
|
|
||||||
This file is part of KeePass DX.
|
|
||||||
|
|
||||||
[KeePass DX](https://www.keepassdx.com) is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
KeePass DX is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
*This project is a fork of [KeepassDroid](https://github.com/bpellin/keepassdroid) by bpellin.*
|
|
||||||
1
_config.yml
Normal file
1
_config.yml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
theme: jekyll-theme-cayman
|
||||||
@@ -4,22 +4,29 @@ apply plugin: 'kotlin-android-extensions'
|
|||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 28
|
compileSdkVersion 30
|
||||||
buildToolsVersion '28.0.3'
|
buildToolsVersion '30.0.2'
|
||||||
ndkVersion "20.0.5594570"
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 14
|
minSdkVersion 14
|
||||||
targetSdkVersion 28
|
targetSdkVersion 30
|
||||||
versionCode = 24
|
versionCode = 42
|
||||||
versionName = "2.5.0.0beta24"
|
versionName = "2.8.6"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
testInstrumentationRunner = "android.test.InstrumentationTestRunner"
|
testInstrumentationRunner = "android.test.InstrumentationTestRunner"
|
||||||
|
|
||||||
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
|
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
|
||||||
|
manifestPlaceholders = [ googleAndroidBackupAPIKey:"" ]
|
||||||
|
|
||||||
|
kapt {
|
||||||
|
arguments {
|
||||||
|
arg("room.incremental", "true")
|
||||||
|
arg("room.schemaLocation", "$projectDir/schemas".toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
@@ -28,7 +35,6 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled = false
|
minifyEnabled = false
|
||||||
@@ -36,35 +42,36 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dexOptions {
|
flavorDimensions "version"
|
||||||
}
|
|
||||||
|
|
||||||
flavorDimensions "tier"
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
libre {
|
libre {
|
||||||
|
dimension "version"
|
||||||
applicationIdSuffix = ".libre"
|
applicationIdSuffix = ".libre"
|
||||||
buildConfigField "String", "BUILD_VERSION", "\"libre\""
|
buildConfigField "String", "BUILD_VERSION", "\"libre\""
|
||||||
buildConfigField "boolean", "FULL_VERSION", "true"
|
buildConfigField "boolean", "FULL_VERSION", "true"
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "false"
|
buildConfigField "boolean", "CLOSED_STORE", "false"
|
||||||
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Dark\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||||
|
|
||||||
}
|
}
|
||||||
pro {
|
pro {
|
||||||
|
dimension "version"
|
||||||
applicationIdSuffix = ".pro"
|
applicationIdSuffix = ".pro"
|
||||||
buildConfigField "String", "BUILD_VERSION", "\"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", "{}"
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||||
|
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIZiXvrQCzSV9LNI6-p7cjTKENZLHIrz_zaqZuQQ" ]
|
||||||
}
|
}
|
||||||
free {
|
free {
|
||||||
|
dimension "version"
|
||||||
applicationIdSuffix = ".free"
|
applicationIdSuffix = ".free"
|
||||||
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
||||||
buildConfigField "boolean", "FULL_VERSION", "false"
|
buildConfigField "boolean", "FULL_VERSION", "false"
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "true"
|
buildConfigField "boolean", "CLOSED_STORE", "true"
|
||||||
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Dark\",\"KeepassDXStyle_Blue\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
buildConfigField "String[]", "STYLES_DISABLED", "{\"KeepassDXStyle_Blue\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||||
|
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,38 +85,47 @@ android {
|
|||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def spongycastleVersion = "1.58.0.0"
|
def room_version = "2.2.5"
|
||||||
def room_version = "2.2.0"
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||||
implementation 'androidx.preference:preference:1.1.0'
|
implementation 'androidx.preference:preference:1.1.1'
|
||||||
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
|
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
|
// WARNING: Bug with extra field
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
implementation 'androidx.biometric:biometric:1.0.0-rc01'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
|
implementation 'androidx.biometric:biometric:1.0.1'
|
||||||
|
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||||
|
implementation "androidx.core:core-ktx:1.3.1"
|
||||||
|
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||||
|
// WARNING: To upgrade with style, bug in edit text
|
||||||
implementation 'com.google.android.material:material:1.0.0'
|
implementation 'com.google.android.material:material:1.0.0'
|
||||||
|
// Database
|
||||||
implementation "androidx.room:room-runtime:$room_version"
|
implementation "androidx.room:room-runtime:$room_version"
|
||||||
kapt "androidx.room:room-compiler:$room_version"
|
kapt "androidx.room:room-compiler:$room_version"
|
||||||
|
// Crypto
|
||||||
implementation "com.madgag.spongycastle:core:$spongycastleVersion"
|
implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01'
|
||||||
implementation "com.madgag.spongycastle:prov:$spongycastleVersion"
|
|
||||||
// Time
|
// Time
|
||||||
implementation 'joda-time:joda-time:2.9.9'
|
implementation 'joda-time:joda-time:2.10.6'
|
||||||
// Color
|
// Color
|
||||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.3'
|
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.3'
|
||||||
// Education
|
// Education
|
||||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.12.0'
|
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
|
||||||
// Apache Commons Collections
|
// Apache Commons Collections
|
||||||
implementation 'commons-collections:commons-collections:3.2.1'
|
implementation 'commons-collections:commons-collections:3.2.2'
|
||||||
implementation 'org.apache.commons:commons-io:1.3.2'
|
// Apache Commons Codec
|
||||||
// Base64
|
implementation 'commons-codec:commons-codec:1.14'
|
||||||
implementation 'biz.source_code:base64coder:2010-12-19'
|
|
||||||
// Icon pack
|
// Icon pack
|
||||||
implementation project(path: ':icon-pack-classic')
|
implementation project(path: ':icon-pack-classic')
|
||||||
implementation project(path: ':icon-pack-material')
|
implementation project(path: ':icon-pack-material')
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
androidTestImplementation 'junit:junit:4.13'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1,
|
||||||
|
"identityHash": "56438e5f7372ef3e36e33b782aed245d",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "file_database_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `database_alias` TEXT NOT NULL, `keyfile_uri` TEXT, `updated` INTEGER NOT NULL, PRIMARY KEY(`database_uri`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "databaseUri",
|
||||||
|
"columnName": "database_uri",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "databaseAlias",
|
||||||
|
"columnName": "database_alias",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "keyFileUri",
|
||||||
|
"columnName": "keyfile_uri",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "updated",
|
||||||
|
"columnName": "updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"database_uri"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "cipher_database",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `encrypted_value` TEXT NOT NULL, `specs_parameters` TEXT NOT NULL, PRIMARY KEY(`database_uri`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "databaseUri",
|
||||||
|
"columnName": "database_uri",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "encryptedValue",
|
||||||
|
"columnName": "encrypted_value",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "specParameters",
|
||||||
|
"columnName": "specs_parameters",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"database_uri"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '56438e5f7372ef3e36e33b782aed245d')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePass DX.
|
|
||||||
*
|
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePass DX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.tests
|
|
||||||
|
|
||||||
import junit.framework.TestCase
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.PwDate
|
|
||||||
import org.junit.Assert
|
|
||||||
|
|
||||||
class PwDateTest : TestCase() {
|
|
||||||
|
|
||||||
fun testDate() {
|
|
||||||
val jDate = PwDate(System.currentTimeMillis())
|
|
||||||
val intermediate = PwDate(jDate)
|
|
||||||
val cDate = PwDate(intermediate.byteArrayDate!!, 0)
|
|
||||||
|
|
||||||
Assert.assertTrue("jDate and intermediate not equal", jDate == intermediate)
|
|
||||||
Assert.assertTrue("jDate and cDate not equal", cDate == jDate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* 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 KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests.crypto
|
package com.kunzisoft.keepass.tests.crypto
|
||||||
@@ -26,11 +26,11 @@ import java.util.Random
|
|||||||
|
|
||||||
import junit.framework.TestCase
|
import junit.framework.TestCase
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.finalkey.AndroidFinalKey
|
import com.kunzisoft.keepass.crypto.finalkey.AndroidAESKeyTransformer
|
||||||
import com.kunzisoft.keepass.crypto.finalkey.NativeFinalKey
|
import com.kunzisoft.keepass.crypto.finalkey.NativeAESKeyTransformer
|
||||||
|
|
||||||
class FinalKeyTest : TestCase() {
|
class AESKeyTest : TestCase() {
|
||||||
private var mRand: Random? = null
|
private lateinit var mRand: Random
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun setUp() {
|
override fun setUp() {
|
||||||
@@ -40,29 +40,28 @@ class FinalKeyTest : TestCase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun testNativeAndroid() {
|
fun testAES() {
|
||||||
// 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)
|
testAESFinalKey(5)
|
||||||
testNativeFinalKey(6)
|
testAESFinalKey(6)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun testNativeFinalKey(rounds: Int) {
|
private fun testAESFinalKey(rounds: Long) {
|
||||||
val seed = ByteArray(32)
|
val seed = ByteArray(32)
|
||||||
val key = ByteArray(32)
|
val key = ByteArray(32)
|
||||||
val nativeKey: ByteArray
|
val nativeKey: ByteArray?
|
||||||
val androidKey: ByteArray
|
val androidKey: ByteArray?
|
||||||
|
|
||||||
mRand!!.nextBytes(seed)
|
mRand.nextBytes(seed)
|
||||||
mRand!!.nextBytes(key)
|
mRand.nextBytes(key)
|
||||||
|
|
||||||
val aKey = AndroidFinalKey()
|
val androidAESKey = AndroidAESKeyTransformer()
|
||||||
androidKey = aKey.transformMasterKey(seed, key, rounds.toLong())
|
androidKey = androidAESKey.transformMasterKey(seed, key, rounds)
|
||||||
|
|
||||||
val nKey = NativeFinalKey()
|
val nativeAESKey = NativeAESKeyTransformer()
|
||||||
nativeKey = nKey.transformMasterKey(seed, key, rounds.toLong())
|
nativeKey = nativeAESKey.transformMasterKey(seed, key, rounds)
|
||||||
|
|
||||||
assertArrayEquals("Does not match", androidKey, nativeKey)
|
assertArrayEquals("Does not match", androidKey, nativeKey)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* 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 KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests.crypto
|
package com.kunzisoft.keepass.tests.crypto
|
||||||
@@ -80,6 +80,4 @@ class AESTest : TestCase() {
|
|||||||
|
|
||||||
assertArrayEquals("Arrays differ on size: $dataSize", outAndroid, outNative)
|
assertArrayEquals("Arrays differ on size: $dataSize", outAndroid, outNative)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* 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 KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests.crypto
|
package com.kunzisoft.keepass.tests.crypto
|
||||||
@@ -39,9 +39,8 @@ import junit.framework.TestCase
|
|||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||||
import com.kunzisoft.keepass.crypto.engine.AesEngine
|
import com.kunzisoft.keepass.crypto.engine.AesEngine
|
||||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
|
||||||
import com.kunzisoft.keepass.stream.BetterCipherInputStream
|
import com.kunzisoft.keepass.stream.BetterCipherInputStream
|
||||||
import com.kunzisoft.keepass.stream.LEDataInputStream
|
import com.kunzisoft.keepass.stream.LittleEndianDataInputStream
|
||||||
|
|
||||||
class CipherTest : TestCase() {
|
class CipherTest : TestCase() {
|
||||||
private val rand = Random()
|
private val rand = Random()
|
||||||
@@ -93,7 +92,7 @@ class CipherTest : TestCase() {
|
|||||||
|
|
||||||
val bis = ByteArrayInputStream(secrettext)
|
val bis = ByteArrayInputStream(secrettext)
|
||||||
val cis = BetterCipherInputStream(bis, decrypt)
|
val cis = BetterCipherInputStream(bis, decrypt)
|
||||||
val lis = LEDataInputStream(cis)
|
val lis = LittleEndianDataInputStream(cis)
|
||||||
|
|
||||||
val decrypttext = lis.readBytes(MESSAGE_LENGTH)
|
val decrypttext = lis.readBytes(MESSAGE_LENGTH)
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* 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 KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests.stream
|
package com.kunzisoft.keepass.tests.stream
|
||||||
@@ -33,7 +33,7 @@ import junit.framework.TestCase
|
|||||||
import com.kunzisoft.keepass.stream.HashedBlockInputStream
|
import com.kunzisoft.keepass.stream.HashedBlockInputStream
|
||||||
import com.kunzisoft.keepass.stream.HashedBlockOutputStream
|
import com.kunzisoft.keepass.stream.HashedBlockOutputStream
|
||||||
|
|
||||||
class HashedBlock : TestCase() {
|
class HashedBlockTest : TestCase() {
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun testBlockAligned() {
|
fun testBlockAligned() {
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePass DX.
|
|
||||||
*
|
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePass DX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.tests.utils
|
|
||||||
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.utils.StringUtil
|
|
||||||
|
|
||||||
import junit.framework.TestCase
|
|
||||||
|
|
||||||
class StringUtilTest : TestCase() {
|
|
||||||
private val text = "AbCdEfGhIj"
|
|
||||||
private val search = "BcDe"
|
|
||||||
private val badSearch = "Ed"
|
|
||||||
|
|
||||||
private val repText = "AbCtestingaBc"
|
|
||||||
private val repSearch = "ABc"
|
|
||||||
private val repSearchBad = "CCCCCC"
|
|
||||||
private val repNew = "12345"
|
|
||||||
private val repResult = "12345testing12345"
|
|
||||||
|
|
||||||
fun testIndexOfIgnoreCase1() {
|
|
||||||
assertEquals(1, StringUtil.indexOfIgnoreCase(text, search, Locale.ENGLISH))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun testIndexOfIgnoreCase2() {
|
|
||||||
assertEquals(-1f, StringUtil.indexOfIgnoreCase(text, search, Locale.ENGLISH).toFloat(), 2f)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun testIndexOfIgnoreCase3() {
|
|
||||||
assertEquals(-1, StringUtil.indexOfIgnoreCase(text, badSearch, Locale.ENGLISH))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun testReplaceAllIgnoresCase1() {
|
|
||||||
assertEquals(repResult, StringUtil.replaceAllIgnoresCase(repText, repSearch, repNew, Locale.ENGLISH))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun testReplaceAllIgnoresCase2() {
|
|
||||||
assertEquals(repText, StringUtil.replaceAllIgnoresCase(repText, repSearchBad, repNew, Locale.ENGLISH))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.kunzisoft.keepass.tests.utils
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
|
import junit.framework.TestCase
|
||||||
|
|
||||||
|
class UnsignedIntTest: TestCase() {
|
||||||
|
|
||||||
|
fun testUInt() {
|
||||||
|
val standardInt = UnsignedInt(15).toKotlinInt()
|
||||||
|
assertEquals(15, standardInt)
|
||||||
|
val unsignedInt = UnsignedInt(-1).toKotlinLong()
|
||||||
|
assertEquals(4294967295L, unsignedInt)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testMaxValue() {
|
||||||
|
val maxValue = UnsignedInt.MAX_VALUE.toKotlinLong()
|
||||||
|
assertEquals(4294967295L, maxValue)
|
||||||
|
val longValue = UnsignedInt.fromKotlinLong(4294967295L).toKotlinLong()
|
||||||
|
assertEquals(longValue, maxValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testLong() {
|
||||||
|
val longValue = UnsignedInt.fromKotlinLong(50L).toKotlinInt()
|
||||||
|
assertEquals(50, longValue)
|
||||||
|
val uIntLongValue = UnsignedInt.fromKotlinLong(4294967290).toKotlinLong()
|
||||||
|
assertEquals(4294967290, uIntLongValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,49 +1,45 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests
|
package com.kunzisoft.keepass.tests.utils
|
||||||
|
|
||||||
import org.junit.Assert.assertArrayEquals
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.util.Calendar
|
|
||||||
import java.util.Random
|
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
|
import com.kunzisoft.keepass.stream.*
|
||||||
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
|
import com.kunzisoft.keepass.utils.UnsignedLong
|
||||||
import junit.framework.TestCase
|
import junit.framework.TestCase
|
||||||
|
import org.junit.Assert.assertArrayEquals
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.PwDate
|
class ValuesTest : TestCase() {
|
||||||
import com.kunzisoft.keepass.stream.LEDataInputStream
|
|
||||||
import com.kunzisoft.keepass.stream.LEDataOutputStream
|
|
||||||
import com.kunzisoft.keepass.utils.Types
|
|
||||||
|
|
||||||
class TypesTest : TestCase() {
|
|
||||||
|
|
||||||
fun testReadWriteLongZero() {
|
fun testReadWriteLongZero() {
|
||||||
testReadWriteLong(0.toByte())
|
testReadWriteLong(0.toByte())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteLongMax() {
|
fun testReadWriteLongMax() {
|
||||||
testReadWriteLong(java.lang.Byte.MAX_VALUE)
|
testReadWriteLong(Byte.MAX_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteLongMin() {
|
fun testReadWriteLongMin() {
|
||||||
testReadWriteLong(java.lang.Byte.MIN_VALUE)
|
testReadWriteLong(Byte.MIN_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteLongRnd() {
|
fun testReadWriteLongRnd() {
|
||||||
@@ -56,15 +52,9 @@ class TypesTest : TestCase() {
|
|||||||
|
|
||||||
private fun testReadWriteLong(value: Byte) {
|
private fun testReadWriteLong(value: Byte) {
|
||||||
val orig = ByteArray(8)
|
val orig = ByteArray(8)
|
||||||
val dest = ByteArray(8)
|
setArray(orig, value, 8)
|
||||||
|
|
||||||
setArray(orig, value, 0, 8)
|
|
||||||
|
|
||||||
val one = LEDataInputStream.readLong(orig, 0)
|
|
||||||
LEDataOutputStream.writeLong(one, dest, 0)
|
|
||||||
|
|
||||||
assertArrayEquals(orig, dest)
|
|
||||||
|
|
||||||
|
assertArrayEquals(orig, longTo8Bytes(bytes64ToLong(orig)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteIntZero() {
|
fun testReadWriteIntZero() {
|
||||||
@@ -72,33 +62,30 @@ class TypesTest : TestCase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteIntMin() {
|
fun testReadWriteIntMin() {
|
||||||
testReadWriteInt(java.lang.Byte.MIN_VALUE)
|
testReadWriteInt(Byte.MIN_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteIntMax() {
|
fun testReadWriteIntMax() {
|
||||||
testReadWriteInt(java.lang.Byte.MAX_VALUE)
|
testReadWriteInt(Byte.MAX_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun testReadWriteInt(value: Byte) {
|
private fun testReadWriteInt(value: Byte) {
|
||||||
val orig = ByteArray(4)
|
val orig = ByteArray(4)
|
||||||
val dest = ByteArray(4)
|
|
||||||
|
|
||||||
for (i in 0..3) {
|
for (i in 0..3) {
|
||||||
orig[i] = 0
|
orig[i] = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
setArray(orig, value, 0, 4)
|
setArray(orig, value, 4)
|
||||||
|
|
||||||
val one = LEDataInputStream.readInt(orig, 0)
|
val one = bytes4ToUInt(orig)
|
||||||
|
val dest = uIntTo4Bytes(one)
|
||||||
LEDataOutputStream.writeInt(one, dest, 0)
|
|
||||||
|
|
||||||
assertArrayEquals(orig, dest)
|
assertArrayEquals(orig, dest)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setArray(buf: ByteArray, value: Byte, offset: Int, size: Int) {
|
private fun setArray(buf: ByteArray, value: Byte, size: Int) {
|
||||||
for (i in offset until offset + size) {
|
for (i in 0 until size) {
|
||||||
buf[i] = value
|
buf[i] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,32 +96,28 @@ class TypesTest : TestCase() {
|
|||||||
orig[0] = 0
|
orig[0] = 0
|
||||||
orig[1] = 1
|
orig[1] = 1
|
||||||
|
|
||||||
val one = LEDataInputStream.readUShort(orig, 0)
|
val one = bytes2ToUShort(orig)
|
||||||
val dest = LEDataOutputStream.writeUShortBuf(one)
|
val dest = uShortTo2Bytes(one)
|
||||||
|
|
||||||
assertArrayEquals(orig, dest)
|
assertArrayEquals(orig, dest)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteShortMin() {
|
fun testReadWriteShortMin() {
|
||||||
testReadWriteShort(java.lang.Byte.MIN_VALUE)
|
testReadWriteShort(Byte.MIN_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteShortMax() {
|
fun testReadWriteShortMax() {
|
||||||
testReadWriteShort(java.lang.Byte.MAX_VALUE)
|
testReadWriteShort(Byte.MAX_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun testReadWriteShort(value: Byte) {
|
private fun testReadWriteShort(value: Byte) {
|
||||||
val orig = ByteArray(2)
|
val orig = ByteArray(2)
|
||||||
val dest = ByteArray(2)
|
setArray(orig, value, 2)
|
||||||
|
|
||||||
setArray(orig, value, 0, 2)
|
val one = bytes2ToUShort(orig)
|
||||||
|
val dest = uShortTo2Bytes(one)
|
||||||
val one = LEDataInputStream.readUShort(orig, 0)
|
|
||||||
LEDataOutputStream.writeUShort(one, dest, 0)
|
|
||||||
|
|
||||||
assertArrayEquals(orig, dest)
|
assertArrayEquals(orig, dest)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteByteZero() {
|
fun testReadWriteByteZero() {
|
||||||
@@ -142,24 +125,16 @@ class TypesTest : TestCase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteByteMin() {
|
fun testReadWriteByteMin() {
|
||||||
testReadWriteByte(java.lang.Byte.MIN_VALUE)
|
testReadWriteByte(Byte.MIN_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteByteMax() {
|
fun testReadWriteByteMax() {
|
||||||
testReadWriteShort(java.lang.Byte.MAX_VALUE)
|
testReadWriteShort(Byte.MAX_VALUE)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun testReadWriteByte(value: Byte) {
|
private fun testReadWriteByte(value: Byte) {
|
||||||
val orig = ByteArray(1)
|
val dest: Byte = UnsignedInt(UnsignedInt.fromKotlinByte(value)).toKotlinByte()
|
||||||
val dest = ByteArray(1)
|
assert(value == dest)
|
||||||
|
|
||||||
setArray(orig, value, 0, 1)
|
|
||||||
|
|
||||||
val one = Types.readUByte(orig, 0)
|
|
||||||
Types.writeUByte(one, dest, 0)
|
|
||||||
|
|
||||||
assertArrayEquals(orig, dest)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testDate() {
|
fun testDate() {
|
||||||
@@ -168,27 +143,37 @@ class TypesTest : TestCase() {
|
|||||||
val expected = Calendar.getInstance()
|
val expected = Calendar.getInstance()
|
||||||
expected.set(2008, 1, 2, 3, 4, 5)
|
expected.set(2008, 1, 2, 3, 4, 5)
|
||||||
|
|
||||||
val buf = PwDate.writeTime(expected.time, cal)
|
|
||||||
val actual = Calendar.getInstance()
|
val actual = Calendar.getInstance()
|
||||||
actual.time = PwDate.readTime(buf, 0, cal)
|
dateTo5Bytes(expected.time, cal)?.let { buf ->
|
||||||
|
actual.time = bytes5ToDate(buf, cal).date
|
||||||
|
}
|
||||||
|
|
||||||
|
val jDate = DateInstant(System.currentTimeMillis())
|
||||||
|
val intermediate = DateInstant(jDate)
|
||||||
|
val cDate = bytes5ToDate(dateTo5Bytes(intermediate.date)!!)
|
||||||
|
|
||||||
assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR))
|
assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR))
|
||||||
assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH))
|
assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH))
|
||||||
assertEquals("Day mismatch: ", 1, actual.get(Calendar.DAY_OF_MONTH))
|
assertEquals("Day mismatch: ", 2, actual.get(Calendar.DAY_OF_MONTH))
|
||||||
assertEquals("Hour mismatch: ", 3, actual.get(Calendar.HOUR_OF_DAY))
|
assertEquals("Hour mismatch: ", 3, actual.get(Calendar.HOUR_OF_DAY))
|
||||||
assertEquals("Minute mismatch: ", 4, actual.get(Calendar.MINUTE))
|
assertEquals("Minute mismatch: ", 4, actual.get(Calendar.MINUTE))
|
||||||
assertEquals("Second mismatch: ", 5, actual.get(Calendar.SECOND))
|
assertEquals("Second mismatch: ", 5, actual.get(Calendar.SECOND))
|
||||||
|
assertTrue("jDate and intermediate not equal", jDate == intermediate)
|
||||||
|
assertTrue("jDate $jDate and cDate $cDate not equal", cDate == jDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testUUID() {
|
fun testUUID() {
|
||||||
val rnd = Random()
|
|
||||||
val bUUID = ByteArray(16)
|
val bUUID = ByteArray(16)
|
||||||
rnd.nextBytes(bUUID)
|
Random().nextBytes(bUUID)
|
||||||
|
|
||||||
val uuid = Types.bytestoUUID(bUUID)
|
val uuid = bytes16ToUuid(bUUID)
|
||||||
val eUUID = Types.UUIDtoBytes(uuid)
|
val eUUID = uuidTo16Bytes(uuid)
|
||||||
|
|
||||||
|
val lUUID = bytes16ToUuid(bUUID)
|
||||||
|
val leUUID = uuidTo16Bytes(lUUID)
|
||||||
|
|
||||||
assertArrayEquals("UUID match failed", bUUID, eUUID)
|
assertArrayEquals("UUID match failed", bUUID, eUUID)
|
||||||
|
assertArrayEquals("UUID match failed", bUUID, leUUID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
@@ -199,8 +184,8 @@ class TypesTest : TestCase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val bos = ByteArrayOutputStream()
|
val bos = ByteArrayOutputStream()
|
||||||
val leos = LEDataOutputStream(bos)
|
val leos = LittleEndianDataOutputStream(bos)
|
||||||
leos.writeLong(Types.ULONG_MAX_VALUE)
|
leos.writeLong(UnsignedLong.MAX_VALUE)
|
||||||
leos.close()
|
leos.close()
|
||||||
|
|
||||||
val uLongMax = bos.toByteArray()
|
val uLongMax = bos.toByteArray()
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.kunzisoft.keepass"
|
package="com.kunzisoft.keepass"
|
||||||
android:installLocation="auto">
|
android:installLocation="auto">
|
||||||
<supports-screens
|
<supports-screens
|
||||||
@@ -7,10 +8,14 @@
|
|||||||
android:normalScreens="true"
|
android:normalScreens="true"
|
||||||
android:largeScreens="true"
|
android:largeScreens="true"
|
||||||
android:anyDensity="true" />
|
android:anyDensity="true" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission
|
||||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
android:name="android.permission.USE_BIOMETRIC" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.VIBRATE"/>
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
@@ -18,14 +23,15 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:name="com.kunzisoft.keepass.app.App"
|
android:name="com.kunzisoft.keepass.app.App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:fullBackupContent="@xml/backup"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
|
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
|
||||||
android:theme="@style/KeepassDXStyle.Night">
|
android:largeHeap="true"
|
||||||
<!-- TODO backup API Key -->
|
android:resizeableActivity="true"
|
||||||
|
android:theme="@style/KeepassDXStyle.Night"
|
||||||
|
tools:targetApi="n">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.backup.api_key"
|
android:name="com.google.android.backup.api_key"
|
||||||
android:value="" />
|
android:value="${googleAndroidBackupAPIKey}" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.FileDatabaseSelectActivity"
|
android:name="com.kunzisoft.keepass.activities.FileDatabaseSelectActivity"
|
||||||
android:theme="@style/KeepassDXStyle.SplashScreen"
|
android:theme="@style/KeepassDXStyle.SplashScreen"
|
||||||
@@ -118,34 +124,50 @@
|
|||||||
android:configChanges="keyboardHidden" />
|
android:configChanges="keyboardHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
|
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
|
||||||
android:configChanges="keyboardHidden"
|
android:windowSoftInputMode="adjustPan|stateAlwaysHidden" />
|
||||||
android:windowSoftInputMode="adjustResize" />
|
|
||||||
<!-- About and Settings -->
|
<!-- About and Settings -->
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:label="@string/menu_about" />
|
android:label="@string/about" />
|
||||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
<activity
|
||||||
<activity android:name="com.kunzisoft.keepass.autofill.AutofillLauncherActivity"
|
android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
|
||||||
|
android:theme="@style/Theme.Transparent"
|
||||||
android:configChanges="keyboardHidden" />
|
android:configChanges="keyboardHidden" />
|
||||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
|
<activity
|
||||||
<activity android:name="com.kunzisoft.keepass.settings.SettingsAutofillActivity" />
|
android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
|
||||||
<activity android:name="com.kunzisoft.keepass.magikeyboard.KeyboardLauncherActivity"
|
<activity
|
||||||
android:label="@string/keyboard_name"
|
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
||||||
android:exported="true">
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
|
||||||
|
android:theme="@style/Theme.Transparent">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name="com.kunzisoft.keepass.settings.MagikIMESettings"
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.MagikeyboardLauncherActivity"
|
||||||
|
android:theme="@style/Theme.Transparent" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
||||||
android:label="@string/keyboard_setting_label">
|
android:label="@string/keyboard_setting_label">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService"
|
android:name="com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
<service
|
||||||
|
android:name=".notifications.AttachmentFileNotificationService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService"
|
android:name="com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
|
|||||||
Binary file not shown.
@@ -1,35 +1,35 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.content.pm.PackageManager.NameNotFoundException
|
import android.content.pm.PackageManager.NameNotFoundException
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.widget.Toolbar
|
import android.text.method.LinkMovementMethod
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.core.text.HtmlCompat
|
||||||
import com.kunzisoft.keepass.BuildConfig
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
|
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
class AboutActivity : StylishActivity() {
|
class AboutActivity : StylishActivity() {
|
||||||
@@ -40,7 +40,7 @@ class AboutActivity : StylishActivity() {
|
|||||||
setContentView(R.layout.activity_about)
|
setContentView(R.layout.activity_about)
|
||||||
|
|
||||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||||
toolbar.title = getString(R.string.menu_about)
|
toolbar.title = getString(R.string.about)
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
@@ -64,9 +64,17 @@ class AboutActivity : StylishActivity() {
|
|||||||
val buildTextView = findViewById<TextView>(R.id.activity_about_build)
|
val buildTextView = findViewById<TextView>(R.id.activity_about_build)
|
||||||
buildTextView.text = build
|
buildTextView.text = build
|
||||||
|
|
||||||
|
findViewById<TextView>(R.id.activity_about_licence_text).apply {
|
||||||
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
text = HtmlCompat.fromHtml(getString(R.string.html_about_licence, DateTime().year),
|
||||||
|
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||||
|
}
|
||||||
|
|
||||||
val disclaimerText = findViewById<TextView>(R.id.disclaimer)
|
findViewById<TextView>(R.id.activity_about_contribution_text).apply {
|
||||||
disclaimerText.text = getString(R.string.disclaimer_formal, DateTime().year)
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
text = HtmlCompat.fromHtml(getString(R.string.html_about_contribution),
|
||||||
|
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentSender
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.autofill.KeeAutofillService
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
|
class AutofillLauncherActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
||||||
|
// Build search param
|
||||||
|
val searchInfo = SearchInfo().apply {
|
||||||
|
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
|
||||||
|
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
||||||
|
val assistStructure = AutofillHelper.retrieveAssistStructure(intent)
|
||||||
|
|
||||||
|
if (assistStructure == null) {
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
} else if (!KeeAutofillService.searchAllowedFor(searchInfo.applicationId,
|
||||||
|
PreferencesUtil.applicationIdBlocklist(this))
|
||||||
|
|| !KeeAutofillService.searchAllowedFor(searchInfo.webDomain,
|
||||||
|
PreferencesUtil.webDomainBlocklist(this))) {
|
||||||
|
// If item not allowed, show a toast
|
||||||
|
Toast.makeText(this.applicationContext, R.string.autofill_block_restart, Toast.LENGTH_LONG).show()
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
// If database is open
|
||||||
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
|
Database.getInstance(),
|
||||||
|
searchInfo,
|
||||||
|
{ items ->
|
||||||
|
// Items found
|
||||||
|
AutofillHelper.buildResponse(this, items)
|
||||||
|
finish()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Show the database UI to select the entry
|
||||||
|
GroupActivity.launchForAutofillResult(this,
|
||||||
|
assistStructure,
|
||||||
|
false,
|
||||||
|
searchInfo)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// If database not open
|
||||||
|
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
||||||
|
assistStructure,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
|
||||||
|
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
|
||||||
|
|
||||||
|
fun getAuthIntentSenderForResponse(context: Context,
|
||||||
|
searchInfo: SearchInfo? = null): IntentSender {
|
||||||
|
return PendingIntent.getActivity(context, 0,
|
||||||
|
// Doesn't work with Parcelable (don't know why?)
|
||||||
|
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||||
|
searchInfo?.let {
|
||||||
|
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
|
||||||
|
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PendingIntent.FLAG_CANCEL_CURRENT).intentSender
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
@@ -22,53 +22,76 @@ import android.app.Activity
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
import android.os.Looper
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import android.widget.ProgressBar
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.EntryVersioned
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
import com.kunzisoft.keepass.database.element.PwNodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||||
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
|
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.settings.SettingsAutofillActivity
|
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.utils.createDocument
|
||||||
|
import com.kunzisoft.keepass.utils.onCreateDocumentResult
|
||||||
import com.kunzisoft.keepass.view.EntryContentsView
|
import com.kunzisoft.keepass.view.EntryContentsView
|
||||||
|
import com.kunzisoft.keepass.view.showActionError
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.collections.HashMap
|
||||||
|
|
||||||
class EntryActivity : LockingHideActivity() {
|
class EntryActivity : LockingActivity() {
|
||||||
|
|
||||||
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
||||||
private var titleIconView: ImageView? = null
|
private var titleIconView: ImageView? = null
|
||||||
private var historyView: View? = null
|
private var historyView: View? = null
|
||||||
private var entryContentsView: EntryContentsView? = null
|
private var entryContentsView: EntryContentsView? = null
|
||||||
|
private var entryProgress: ProgressBar? = null
|
||||||
|
private var lockView: View? = null
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
|
|
||||||
private var mDatabase: Database? = null
|
private var mDatabase: Database? = null
|
||||||
|
|
||||||
private var mEntry: EntryVersioned? = null
|
private var mEntry: Entry? = null
|
||||||
|
|
||||||
private var mIsHistory: Boolean = false
|
private var mIsHistory: Boolean = false
|
||||||
|
private var mEntryLastVersion: Entry? = null
|
||||||
|
private var mEntryHistoryPosition: Int = -1
|
||||||
|
|
||||||
private var mShowPassword: Boolean = false
|
private var mShowPassword: Boolean = false
|
||||||
|
|
||||||
|
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||||
|
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
|
||||||
|
|
||||||
private var clipboardHelper: ClipboardHelper? = null
|
private var clipboardHelper: ClipboardHelper? = null
|
||||||
private var firstLaunchOfActivity: Boolean = false
|
private var mFirstLaunchOfActivity: Boolean = false
|
||||||
|
|
||||||
private var iconColor: Int = 0
|
private var iconColor: Int = 0
|
||||||
|
|
||||||
@@ -96,29 +119,65 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
|
|
||||||
// Get views
|
// Get views
|
||||||
|
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
||||||
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
||||||
titleIconView = findViewById(R.id.entry_icon)
|
titleIconView = findViewById(R.id.entry_icon)
|
||||||
historyView = findViewById(R.id.history_container)
|
historyView = findViewById(R.id.history_container)
|
||||||
entryContentsView = findViewById(R.id.entry_contents)
|
entryContentsView = findViewById(R.id.entry_contents)
|
||||||
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
||||||
|
entryProgress = findViewById(R.id.entry_progress)
|
||||||
|
lockView = findViewById(R.id.lock_button)
|
||||||
|
|
||||||
|
lockView?.setOnClickListener {
|
||||||
|
lockAndExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus view to reinitialize timeout
|
||||||
|
resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout)
|
||||||
|
|
||||||
// Init the clipboard helper
|
// Init the clipboard helper
|
||||||
clipboardHelper = ClipboardHelper(this)
|
clipboardHelper = ClipboardHelper(this)
|
||||||
firstLaunchOfActivity = true
|
mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true
|
||||||
|
|
||||||
|
// Init attachment service binder manager
|
||||||
|
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||||
|
|
||||||
|
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
|
||||||
|
when (actionTask) {
|
||||||
|
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
|
||||||
|
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
|
||||||
|
// Close the current activity after an history action
|
||||||
|
if (result.isSuccess)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
coordinatorLayout?.showActionError(result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
|
// Show the lock button
|
||||||
|
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
// Get Entry from UUID
|
// Get Entry from UUID
|
||||||
try {
|
try {
|
||||||
val keyEntry: PwNodeId<UUID> = intent.getParcelableExtra(KEY_ENTRY)
|
val keyEntry: NodeId<UUID>? = intent.getParcelableExtra(KEY_ENTRY)
|
||||||
mEntry = mDatabase?.getEntryById(keyEntry)
|
if (keyEntry != null) {
|
||||||
|
mEntry = mDatabase?.getEntryById(keyEntry)
|
||||||
|
mEntryLastVersion = mEntry
|
||||||
|
}
|
||||||
} catch (e: ClassCastException) {
|
} catch (e: ClassCastException) {
|
||||||
Log.e(TAG, "Unable to retrieve the entry key")
|
Log.e(TAG, "Unable to retrieve the entry key")
|
||||||
}
|
}
|
||||||
|
|
||||||
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
|
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, mEntryHistoryPosition)
|
||||||
|
mEntryHistoryPosition = historyPosition
|
||||||
if (historyPosition >= 0) {
|
if (historyPosition >= 0) {
|
||||||
mIsHistory = true
|
mIsHistory = true
|
||||||
mEntry = mEntry?.getHistory()?.get(historyPosition)
|
mEntry = mEntry?.getHistory()?.get(historyPosition)
|
||||||
@@ -142,7 +201,7 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
val entryInfo = entry.getEntryInfo(Database.getInstance())
|
val entryInfo = entry.getEntryInfo(Database.getInstance())
|
||||||
|
|
||||||
// Manage entry copy to start notification if allowed
|
// Manage entry copy to start notification if allowed
|
||||||
if (firstLaunchOfActivity) {
|
if (mFirstLaunchOfActivity) {
|
||||||
// Manage entry to launch copying notification if allowed
|
// Manage entry to launch copying notification if allowed
|
||||||
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
||||||
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
||||||
@@ -152,10 +211,25 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
firstLaunchOfActivity = false
|
mAttachmentFileBinderManager?.apply {
|
||||||
|
registerProgressTask()
|
||||||
|
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
||||||
|
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
||||||
|
entryContentsView?.putAttachment(entryAttachmentState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mFirstLaunchOfActivity = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fillEntryDataInContentsView(entry: EntryVersioned) {
|
override fun onPause() {
|
||||||
|
mAttachmentFileBinderManager?.unregisterProgressTask()
|
||||||
|
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fillEntryDataInContentsView(entry: Entry) {
|
||||||
|
|
||||||
val database = Database.getInstance()
|
val database = Database.getInstance()
|
||||||
database.startManageEntry(entry)
|
database.startManageEntry(entry)
|
||||||
@@ -168,14 +242,13 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
toolbar?.title = entryTitle
|
toolbar?.title = entryTitle
|
||||||
|
|
||||||
// Assign basic fields
|
// Assign basic fields
|
||||||
entryContentsView?.assignUserName(entry.username)
|
entryContentsView?.assignUserName(entry.username) {
|
||||||
entryContentsView?.assignUserNameCopyListener(View.OnClickListener {
|
|
||||||
database.startManageEntry(entry)
|
database.startManageEntry(entry)
|
||||||
clipboardHelper?.timeoutCopyToClipboard(entry.username,
|
clipboardHelper?.timeoutCopyToClipboard(entry.username,
|
||||||
getString(R.string.copy_field,
|
getString(R.string.copy_field,
|
||||||
getString(R.string.entry_user_name)))
|
getString(R.string.entry_user_name)))
|
||||||
database.stopManageEntry(entry)
|
database.stopManageEntry(entry)
|
||||||
})
|
}
|
||||||
|
|
||||||
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
||||||
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
|
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
|
||||||
@@ -188,7 +261,7 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
"\n\n" +
|
"\n\n" +
|
||||||
getString(R.string.clipboard_warning))
|
getString(R.string.clipboard_warning))
|
||||||
.create().apply {
|
.create().apply {
|
||||||
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) {dialog, _ ->
|
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) { dialog, _ ->
|
||||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true)
|
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
fillEntryDataInContentsView(entry)
|
fillEntryDataInContentsView(entry)
|
||||||
@@ -202,43 +275,54 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entryContentsView?.assignPassword(entry.password, allowCopyPasswordAndProtectedFields)
|
val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) {
|
||||||
if (allowCopyPasswordAndProtectedFields) {
|
View.OnClickListener {
|
||||||
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
|
|
||||||
database.startManageEntry(entry)
|
database.startManageEntry(entry)
|
||||||
clipboardHelper?.timeoutCopyToClipboard(entry.password,
|
clipboardHelper?.timeoutCopyToClipboard(entry.password,
|
||||||
getString(R.string.copy_field,
|
getString(R.string.copy_field,
|
||||||
getString(R.string.entry_password)))
|
getString(R.string.entry_password)))
|
||||||
database.stopManageEntry(entry)
|
database.stopManageEntry(entry)
|
||||||
})
|
}
|
||||||
} else {
|
} else {
|
||||||
// If dialog not already shown
|
// If dialog not already shown
|
||||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||||
entryContentsView?.assignPasswordCopyListener(showWarningClipboardDialogOnClickListener)
|
showWarningClipboardDialogOnClickListener
|
||||||
} else {
|
} else {
|
||||||
entryContentsView?.assignPasswordCopyListener(null)
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
entryContentsView?.assignPassword(entry.password,
|
||||||
|
allowCopyPasswordAndProtectedFields,
|
||||||
|
onPasswordCopyClickListener)
|
||||||
|
|
||||||
|
//Assign OTP field
|
||||||
|
entryContentsView?.assignOtp(entry.getOtpElement(), entryProgress,
|
||||||
|
View.OnClickListener {
|
||||||
|
entry.getOtpElement()?.let { otpElement ->
|
||||||
|
clipboardHelper?.timeoutCopyToClipboard(
|
||||||
|
otpElement.token,
|
||||||
|
getString(R.string.copy_field, getString(R.string.entry_otp))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
entryContentsView?.assignURL(entry.url)
|
entryContentsView?.assignURL(entry.url)
|
||||||
entryContentsView?.assignComment(entry.notes)
|
entryContentsView?.assignNotes(entry.notes)
|
||||||
|
|
||||||
// Assign custom fields
|
// Assign custom fields
|
||||||
if (entry.allowCustomFields()) {
|
if (mDatabase?.allowEntryCustomFields() == true) {
|
||||||
entryContentsView?.clearExtraFields()
|
entryContentsView?.clearExtraFields()
|
||||||
|
entry.getExtraFields().forEach { field ->
|
||||||
for (element in entry.customFields.entries) {
|
val label = field.name
|
||||||
val label = element.key
|
val value = field.protectedValue
|
||||||
val value = element.value
|
|
||||||
|
|
||||||
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
||||||
if (allowCopyProtectedField) {
|
if (allowCopyProtectedField) {
|
||||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, View.OnClickListener {
|
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
|
||||||
clipboardHelper?.timeoutCopyToClipboard(
|
clipboardHelper?.timeoutCopyToClipboard(
|
||||||
value.toString(),
|
value.toString(),
|
||||||
getString(R.string.copy_field, label)
|
getString(R.string.copy_field, label)
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
} else {
|
} else {
|
||||||
// If dialog not already shown
|
// If dialog not already shown
|
||||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||||
@@ -249,7 +333,16 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
|
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
|
||||||
|
|
||||||
|
// Manage attachments
|
||||||
|
mDatabase?.binaryPool?.let { binaryPool ->
|
||||||
|
entryContentsView?.assignAttachments(entry.getAttachments(binaryPool).toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
|
||||||
|
createDocument(this, attachmentItem.name)?.let { requestCode ->
|
||||||
|
mAttachmentsToDownload[requestCode] = attachmentItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Assign dates
|
// Assign dates
|
||||||
entryContentsView?.assignCreationDate(entry.creationTime)
|
entryContentsView?.assignCreationDate(entry.creationTime)
|
||||||
@@ -262,9 +355,6 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
entryContentsView?.assignExpiresDate(getString(R.string.never))
|
entryContentsView?.assignExpiresDate(getString(R.string.never))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign special data
|
|
||||||
entryContentsView?.assignUUID(entry.nodeId.id)
|
|
||||||
|
|
||||||
// Manage history
|
// Manage history
|
||||||
historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE
|
historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE
|
||||||
if (mIsHistory) {
|
if (mIsHistory) {
|
||||||
@@ -272,22 +362,19 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
||||||
taColorAccent.recycle()
|
taColorAccent.recycle()
|
||||||
}
|
}
|
||||||
val entryHistory = entry.getHistory()
|
entryContentsView?.assignHistory(entry.getHistory()) { historyItem, position ->
|
||||||
// isMainEntry = not an history
|
launch(this, historyItem, mReadOnly, position)
|
||||||
val showHistoryView = entryHistory.isNotEmpty()
|
|
||||||
entryContentsView?.showHistory(showHistoryView)
|
|
||||||
if (showHistoryView) {
|
|
||||||
entryContentsView?.assignHistory(entryHistory)
|
|
||||||
entryContentsView?.onHistoryClick { historyItem, position ->
|
|
||||||
launch(this, historyItem, true, position)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assign special data
|
||||||
|
entryContentsView?.assignUUID(entry.nodeId.id)
|
||||||
|
|
||||||
database.stopManageEntry(entry)
|
database.stopManageEntry(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
when (requestCode) {
|
when (requestCode) {
|
||||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE ->
|
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE ->
|
||||||
// Not directly get the entry from intent data but from database
|
// Not directly get the entry from intent data but from database
|
||||||
@@ -295,15 +382,14 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
fillEntryDataInContentsView(it)
|
fillEntryDataInContentsView(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun changeShowPasswordIcon(togglePassword: MenuItem?) {
|
onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
||||||
if (mShowPassword) {
|
if (createdFileUri != null) {
|
||||||
togglePassword?.setTitle(R.string.menu_hide_password)
|
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
|
||||||
togglePassword?.setIcon(R.drawable.ic_visibility_off_white_24dp)
|
mAttachmentFileBinderManager
|
||||||
} else {
|
?.startDownloadAttachment(createdFileUri, attachmentToDownload)
|
||||||
togglePassword?.setTitle(R.string.menu_showpass)
|
}
|
||||||
togglePassword?.setIcon(R.drawable.ic_visibility_white_24dp)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,19 +399,13 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
val inflater = menuInflater
|
val inflater = menuInflater
|
||||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||||
inflater.inflate(R.menu.entry, menu)
|
inflater.inflate(R.menu.entry, menu)
|
||||||
inflater.inflate(R.menu.database_lock, menu)
|
inflater.inflate(R.menu.database, menu)
|
||||||
|
if (mIsHistory && !mReadOnly) {
|
||||||
if (mReadOnly) {
|
inflater.inflate(R.menu.entry_history, menu)
|
||||||
menu.findItem(R.id.menu_edit)?.isVisible = false
|
|
||||||
}
|
}
|
||||||
|
if (mIsHistory || mReadOnly) {
|
||||||
val togglePassword = menu.findItem(R.id.menu_toggle_pass)
|
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||||
entryContentsView?.let {
|
menu.findItem(R.id.menu_edit)?.isVisible = false
|
||||||
if (it.isPasswordPresent || it.atLeastOneFieldProtectedPresent()) {
|
|
||||||
changeShowPasswordIcon(togglePassword)
|
|
||||||
} else {
|
|
||||||
togglePassword?.isVisible = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val gotoUrl = menu.findItem(R.id.menu_goto_url)
|
val gotoUrl = menu.findItem(R.id.menu_goto_url)
|
||||||
@@ -343,38 +423,38 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show education views
|
// Show education views
|
||||||
Handler().post { performedNextEducation(EntryActivityEducation(this), menu) }
|
Handler(Looper.getMainLooper()).post { performedNextEducation(EntryActivityEducation(this), menu) }
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
||||||
menu: Menu) {
|
menu: Menu) {
|
||||||
val entryCopyEducationPerformed = entryContentsView?.isUserNamePresent == true
|
val entryFieldCopyView = entryContentsView?.firstEntryFieldCopyView()
|
||||||
|
val entryCopyEducationPerformed = entryFieldCopyView != null
|
||||||
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
||||||
findViewById(R.id.entry_user_name_action_image),
|
entryFieldCopyView,
|
||||||
{
|
{
|
||||||
clipboardHelper?.timeoutCopyToClipboard(mEntry!!.username,
|
val appNameString = getString(R.string.app_name)
|
||||||
getString(R.string.copy_field,
|
clipboardHelper?.timeoutCopyToClipboard(appNameString,
|
||||||
getString(R.string.entry_user_name)))
|
getString(R.string.copy_field, appNameString))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Launch autofill settings
|
performedNextEducation(entryActivityEducation, menu)
|
||||||
startActivity(Intent(this@EntryActivity, SettingsAutofillActivity::class.java))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!entryCopyEducationPerformed) {
|
if (!entryCopyEducationPerformed) {
|
||||||
|
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
|
||||||
// entryEditEducationPerformed
|
// entryEditEducationPerformed
|
||||||
toolbar?.findViewById<View>(R.id.menu_edit) != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
|
menuEditView != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
|
||||||
toolbar!!.findViewById(R.id.menu_edit),
|
menuEditView,
|
||||||
{
|
{
|
||||||
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Open Keepass doc to create field references
|
performedNextEducation(entryActivityEducation, menu)
|
||||||
startActivity(Intent(Intent.ACTION_VIEW,
|
}
|
||||||
UriUtil.parse(getString(R.string.field_references_url))))
|
)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,21 +464,12 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
MenuUtil.onContributionItemSelected(this)
|
MenuUtil.onContributionItemSelected(this)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.menu_toggle_pass -> {
|
|
||||||
mShowPassword = !mShowPassword
|
|
||||||
changeShowPasswordIcon(item)
|
|
||||||
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.menu_edit -> {
|
R.id.menu_edit -> {
|
||||||
mEntry?.let {
|
mEntry?.let {
|
||||||
EntryEditActivity.launch(this@EntryActivity, it)
|
EntryEditActivity.launch(this@EntryActivity, it)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.menu_goto_url -> {
|
R.id.menu_goto_url -> {
|
||||||
var url: String = mEntry?.url ?: ""
|
var url: String = mEntry?.url ?: ""
|
||||||
|
|
||||||
@@ -408,40 +479,56 @@ class EntryActivity : LockingHideActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
UriUtil.gotoUrl(this, url)
|
UriUtil.gotoUrl(this, url)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
R.id.menu_restore_entry_history -> {
|
||||||
R.id.menu_lock -> {
|
mEntryLastVersion?.let { mainEntry ->
|
||||||
lockAndExit()
|
mProgressDatabaseTaskProvider?.startDatabaseRestoreEntryHistory(
|
||||||
return true
|
mainEntry,
|
||||||
|
mEntryHistoryPosition,
|
||||||
|
!mReadOnly && mAutoSaveEnable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
R.id.menu_delete_entry_history -> {
|
||||||
|
mEntryLastVersion?.let { mainEntry ->
|
||||||
|
mProgressDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(
|
||||||
|
mainEntry,
|
||||||
|
mEntryHistoryPosition,
|
||||||
|
!mReadOnly && mAutoSaveEnable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
R.id.menu_save_database -> {
|
||||||
|
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
|
||||||
}
|
}
|
||||||
|
|
||||||
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
|
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
|
||||||
|
outState.putBoolean(KEY_FIRST_LAUNCH_ACTIVITY, mFirstLaunchOfActivity)
|
||||||
|
}
|
||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
// Transit data in previous Activity after an update
|
// Transit data in previous Activity after an update
|
||||||
/*
|
Intent().apply {
|
||||||
TODO Slowdown when add entry as result
|
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry)
|
||||||
Intent intent = new Intent();
|
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, this)
|
||||||
intent.putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry);
|
}
|
||||||
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, intent);
|
|
||||||
*/
|
|
||||||
super.finish()
|
super.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = EntryActivity::class.java.name
|
private val TAG = EntryActivity::class.java.name
|
||||||
|
|
||||||
|
private const val KEY_FIRST_LAUNCH_ACTIVITY = "KEY_FIRST_LAUNCH_ACTIVITY"
|
||||||
|
|
||||||
const val KEY_ENTRY = "KEY_ENTRY"
|
const val KEY_ENTRY = "KEY_ENTRY"
|
||||||
const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION"
|
const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION"
|
||||||
|
|
||||||
fun launch(activity: Activity, entry: EntryVersioned, readOnly: Boolean, historyPosition: Int? = null) {
|
fun launch(activity: Activity, entry: Entry, readOnly: Boolean, historyPosition: Int? = null) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
val intent = Intent(activity, EntryActivity::class.java)
|
val intent = Intent(activity, EntryActivity::class.java)
|
||||||
intent.putExtra(KEY_ENTRY, entry.nodeId)
|
intent.putExtra(KEY_ENTRY, entry.nodeId)
|
||||||
|
|||||||
@@ -1,68 +1,107 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.app.DatePickerDialog
|
||||||
|
import android.app.TimePickerDialog
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ScrollView
|
import android.widget.DatePicker
|
||||||
|
import android.widget.TimePicker
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.widget.NestedScrollView
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.*
|
||||||
import com.kunzisoft.keepass.activities.dialogs.IconPickerDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingHideActivity
|
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.database.element.*
|
import com.kunzisoft.keepass.database.element.*
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||||
|
import com.kunzisoft.keepass.model.*
|
||||||
|
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
|
||||||
|
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
|
import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
|
||||||
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.view.EntryEditContentsView
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.utils.closeDatabase
|
||||||
|
import com.kunzisoft.keepass.view.asError
|
||||||
|
import com.kunzisoft.keepass.view.showActionError
|
||||||
|
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
||||||
|
import org.joda.time.DateTime
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
class EntryEditActivity : LockingHideActivity(),
|
class EntryEditActivity : LockingActivity(),
|
||||||
IconPickerDialogFragment.IconPickerListener,
|
IconPickerDialogFragment.IconPickerListener,
|
||||||
GeneratePasswordDialogFragment.GeneratePasswordListener {
|
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
|
||||||
|
GeneratePasswordDialogFragment.GeneratePasswordListener,
|
||||||
|
SetOTPDialogFragment.CreateOtpListener,
|
||||||
|
DatePickerDialog.OnDateSetListener,
|
||||||
|
TimePickerDialog.OnTimeSetListener,
|
||||||
|
FileTooBigDialogFragment.ActionChooseListener,
|
||||||
|
ReplaceFileDialogFragment.ActionChooseListener {
|
||||||
|
|
||||||
private var mDatabase: Database? = null
|
private var mDatabase: Database? = null
|
||||||
|
|
||||||
// Refs of an entry and group in database, are not modifiable
|
// Refs of an entry and group in database, are not modifiable
|
||||||
private var mEntry: EntryVersioned? = null
|
private var mEntry: Entry? = null
|
||||||
private var mParent: GroupVersioned? = null
|
private var mParent: Group? = null
|
||||||
// New or copy of mEntry in the database to be modifiable
|
|
||||||
private var mNewEntry: EntryVersioned? = null
|
|
||||||
private var mIsNew: Boolean = false
|
private var mIsNew: Boolean = false
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var scrollView: ScrollView? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var entryEditContentsView: EntryEditContentsView? = null
|
private var scrollView: NestedScrollView? = null
|
||||||
private var saveView: View? = null
|
private var entryEditFragment: EntryEditFragment? = null
|
||||||
|
private var entryEditAddToolBar: Toolbar? = null
|
||||||
|
private var validateButton: View? = null
|
||||||
|
private var lockView: View? = null
|
||||||
|
|
||||||
// Dialog thread
|
// To manage attachments
|
||||||
private var progressDialogThread: ProgressDialogThread? = null
|
private var mSelectFileHelper: SelectFileHelper? = null
|
||||||
|
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||||
|
private var mAllowMultipleAttachments: Boolean = false
|
||||||
|
private var mTempAttachments = ArrayList<Attachment>()
|
||||||
|
|
||||||
// Education
|
// Education
|
||||||
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
||||||
@@ -77,19 +116,29 @@ class EntryEditActivity : LockingHideActivity(),
|
|||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
|
coordinatorLayout = findViewById(R.id.entry_edit_coordinator_layout)
|
||||||
|
|
||||||
scrollView = findViewById(R.id.entry_edit_scroll)
|
scrollView = findViewById(R.id.entry_edit_scroll)
|
||||||
scrollView?.scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
scrollView?.scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
||||||
|
|
||||||
entryEditContentsView = findViewById(R.id.entry_edit_contents)
|
lockView = findViewById(R.id.lock_button)
|
||||||
entryEditContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
lockView?.setOnClickListener {
|
||||||
|
lockAndExit()
|
||||||
|
}
|
||||||
|
|
||||||
// Focus view to reinitialize timeout
|
// Focus view to reinitialize timeout
|
||||||
resetAppTimeoutWhenViewFocusedOrChanged(entryEditContentsView)
|
resetAppTimeoutWhenViewFocusedOrChanged(coordinatorLayout)
|
||||||
|
|
||||||
|
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
||||||
|
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
||||||
|
|
||||||
// Likely the app has been killed exit the activity
|
// Likely the app has been killed exit the activity
|
||||||
mDatabase = Database.getInstance()
|
mDatabase = Database.getInstance()
|
||||||
|
|
||||||
|
var tempEntryInfo: EntryInfo? = null
|
||||||
|
|
||||||
// Entry is retrieve, it's an entry to update
|
// Entry is retrieve, it's an entry to update
|
||||||
intent.getParcelableExtra<PwNodeId<UUID>>(KEY_ENTRY)?.let {
|
intent.getParcelableExtra<NodeId<UUID>>(KEY_ENTRY)?.let {
|
||||||
mIsNew = false
|
mIsNew = false
|
||||||
// Create an Entry copy to modify from the database entry
|
// Create an Entry copy to modify from the database entry
|
||||||
mEntry = mDatabase?.getEntryById(it)
|
mEntry = mDatabase?.getEntryById(it)
|
||||||
@@ -103,128 +152,210 @@ class EntryEditActivity : LockingHideActivity(),
|
|||||||
entry.parent = mParent
|
entry.parent = mParent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
tempEntryInfo = mEntry?.getEntryInfo(mDatabase, true)
|
||||||
// Create the new entry from the current one
|
|
||||||
if (savedInstanceState == null
|
|
||||||
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
|
||||||
mEntry?.let { entry ->
|
|
||||||
// Create a copy to modify
|
|
||||||
mNewEntry = EntryVersioned(entry).also { newEntry ->
|
|
||||||
// WARNING Remove the parent to keep memory with parcelable
|
|
||||||
newEntry.removeParent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parent is retrieve, it's a new entry to create
|
// Parent is retrieve, it's a new entry to create
|
||||||
intent.getParcelableExtra<PwNodeId<*>>(KEY_PARENT)?.let {
|
intent.getParcelableExtra<NodeId<*>>(KEY_PARENT)?.let {
|
||||||
mIsNew = true
|
mIsNew = true
|
||||||
// Create an empty new entry
|
|
||||||
if (savedInstanceState == null
|
|
||||||
|| !savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
|
||||||
mNewEntry = mDatabase?.createEntry()
|
|
||||||
}
|
|
||||||
mParent = mDatabase?.getGroupById(it)
|
mParent = mDatabase?.getGroupById(it)
|
||||||
// Add the default icon
|
// Add the default icon from parent if not a folder
|
||||||
mDatabase?.drawFactory?.let { iconFactory ->
|
val parentIcon = mParent?.icon
|
||||||
entryEditContentsView?.setDefaultIcon(iconFactory)
|
tempEntryInfo = mDatabase?.createEntry()?.getEntryInfo(mDatabase, true)
|
||||||
|
// Set default icon
|
||||||
|
if (parentIcon != null
|
||||||
|
&& parentIcon.iconId != IconImage.UNKNOWN_ID
|
||||||
|
&& parentIcon.iconId != IconImageStandard.FOLDER) {
|
||||||
|
tempEntryInfo?.icon = parentIcon
|
||||||
|
}
|
||||||
|
// Set default username
|
||||||
|
tempEntryInfo?.username = mDatabase?.defaultUsername ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build fragment to manage entry modification
|
||||||
|
entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment?
|
||||||
|
if (entryEditFragment == null) {
|
||||||
|
entryEditFragment = EntryEditFragment.getInstance(tempEntryInfo)
|
||||||
|
}
|
||||||
|
supportFragmentManager.beginTransaction()
|
||||||
|
.replace(R.id.entry_edit_contents, entryEditFragment!!, ENTRY_EDIT_FRAGMENT_TAG)
|
||||||
|
.commit()
|
||||||
|
entryEditFragment?.apply {
|
||||||
|
drawFactory = mDatabase?.drawFactory
|
||||||
|
setOnDateClickListener = View.OnClickListener {
|
||||||
|
expiryTime.date.let { expiresDate ->
|
||||||
|
val dateTime = DateTime(expiresDate)
|
||||||
|
val defaultYear = dateTime.year
|
||||||
|
val defaultMonth = dateTime.monthOfYear-1
|
||||||
|
val defaultDay = dateTime.dayOfMonth
|
||||||
|
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
|
||||||
|
.show(supportFragmentManager, "DatePickerFragment")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setOnPasswordGeneratorClickListener = View.OnClickListener {
|
||||||
|
openPasswordGenerator()
|
||||||
|
}
|
||||||
|
// Add listener to the icon
|
||||||
|
setOnIconViewClickListener = View.OnClickListener {
|
||||||
|
IconPickerDialogFragment.launch(this@EntryEditActivity)
|
||||||
|
}
|
||||||
|
setOnRemoveAttachment = { attachment ->
|
||||||
|
mAttachmentFileBinderManager?.removeBinaryAttachment(attachment)
|
||||||
|
removeAttachment(EntryAttachmentState(attachment, StreamDirection.DOWNLOAD))
|
||||||
|
}
|
||||||
|
setOnEditCustomField = { field ->
|
||||||
|
editCustomField(field)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the new entry after an orientation change
|
// Retrieve temp attachments in case of deletion
|
||||||
if (savedInstanceState != null
|
if (savedInstanceState?.containsKey(TEMP_ATTACHMENTS) == true) {
|
||||||
&& savedInstanceState.containsKey(KEY_NEW_ENTRY)) {
|
mTempAttachments = savedInstanceState.getParcelableArrayList(TEMP_ATTACHMENTS) ?: mTempAttachments
|
||||||
mNewEntry = savedInstanceState.getParcelable(KEY_NEW_ENTRY)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the activity if entry or parent can't be retrieve
|
|
||||||
if (mNewEntry == null || mParent == null) {
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
populateViewsWithEntry(mNewEntry!!)
|
|
||||||
|
|
||||||
// Assign title
|
// Assign title
|
||||||
title = if (mIsNew) getString(R.string.add_entry) else getString(R.string.edit_entry)
|
title = if (mIsNew) getString(R.string.add_entry) else getString(R.string.edit_entry)
|
||||||
|
|
||||||
// Add listener to the icon
|
// Bottom Bar
|
||||||
entryEditContentsView?.setOnIconViewClickListener { IconPickerDialogFragment.launch(this@EntryEditActivity) }
|
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
|
||||||
|
entryEditAddToolBar?.apply {
|
||||||
|
menuInflater.inflate(R.menu.entry_edit, menu)
|
||||||
|
|
||||||
// Generate password button
|
menu.findItem(R.id.menu_add_field).apply {
|
||||||
entryEditContentsView?.setOnPasswordGeneratorClickListener { openPasswordGenerator() }
|
val allowCustomField = mDatabase?.allowEntryCustomFields() == true
|
||||||
|
isEnabled = allowCustomField
|
||||||
|
isVisible = allowCustomField
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attachment not compatible below KitKat
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||||
|
menu.findItem(R.id.menu_add_attachment).isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.findItem(R.id.menu_add_otp).apply {
|
||||||
|
val allowOTP = mDatabase?.allowOTP == true
|
||||||
|
isEnabled = allowOTP
|
||||||
|
// OTP not compatible below KitKat
|
||||||
|
isVisible = allowOTP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnMenuItemClickListener { item ->
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.menu_add_field -> {
|
||||||
|
addNewCustomField()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.menu_add_attachment -> {
|
||||||
|
addNewAttachment(item)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.menu_add_otp -> {
|
||||||
|
setupOTP()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// To retrieve attachment
|
||||||
|
mSelectFileHelper = SelectFileHelper(this)
|
||||||
|
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||||
|
|
||||||
// Save button
|
// Save button
|
||||||
saveView = findViewById(R.id.entry_edit_save)
|
validateButton = findViewById(R.id.entry_edit_validate)
|
||||||
saveView?.setOnClickListener { saveEntry() }
|
validateButton?.setOnClickListener { saveEntry() }
|
||||||
|
|
||||||
entryEditContentsView?.allowCustomField(mNewEntry?.allowCustomFields() == true) { addNewCustomField() }
|
|
||||||
|
|
||||||
// Verify the education views
|
// Verify the education views
|
||||||
entryEditActivityEducation = EntryEditActivityEducation(this)
|
entryEditActivityEducation = EntryEditActivityEducation(this)
|
||||||
|
|
||||||
// Create progress dialog
|
// Create progress dialog
|
||||||
progressDialogThread = ProgressDialogThread(this) { actionTask, result ->
|
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_CREATE_ENTRY_TASK,
|
ACTION_DATABASE_CREATE_ENTRY_TASK,
|
||||||
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||||
if (result.isSuccess)
|
try {
|
||||||
finish()
|
if (result.isSuccess) {
|
||||||
|
var newNodes: List<Node> = ArrayList()
|
||||||
|
result.data?.getBundle(DatabaseTaskNotificationService.NEW_NODES_KEY)?.let { newNodesBundle ->
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
newNodes = DatabaseTaskNotificationService.getListNodesFromBundle(database, newNodesBundle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newNodes.size == 1) {
|
||||||
|
mEntry = newNodes[0] as Entry?
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to retrieve entry after database action", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
coordinatorLayout?.showActionError(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Padding if lock button visible
|
||||||
|
entryEditAddToolBar?.updateLockPaddingLeft()
|
||||||
|
|
||||||
|
mAllowMultipleAttachments = mDatabase?.allowMultipleAttachments == true
|
||||||
|
mAttachmentFileBinderManager?.apply {
|
||||||
|
registerProgressTask()
|
||||||
|
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
||||||
|
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
||||||
|
when (entryAttachmentState.downloadState) {
|
||||||
|
AttachmentState.START -> {
|
||||||
|
entryEditFragment?.apply {
|
||||||
|
// When only one attachment is allowed
|
||||||
|
if (!mAllowMultipleAttachments) {
|
||||||
|
clearAttachments()
|
||||||
|
}
|
||||||
|
putAttachment(entryAttachmentState)
|
||||||
|
// Scroll to the attachment position
|
||||||
|
getAttachmentViewPosition(entryAttachmentState) {
|
||||||
|
scrollView?.smoothScrollTo(0, it.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AttachmentState.IN_PROGRESS -> {
|
||||||
|
entryEditFragment?.putAttachment(entryAttachmentState)
|
||||||
|
}
|
||||||
|
AttachmentState.COMPLETE -> {
|
||||||
|
entryEditFragment?.apply {
|
||||||
|
putAttachment(entryAttachmentState)
|
||||||
|
// Scroll to the attachment position
|
||||||
|
getAttachmentViewPosition(entryAttachmentState) {
|
||||||
|
scrollView?.smoothScrollTo(0, it.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AttachmentState.ERROR -> {
|
||||||
|
entryEditFragment?.removeAttachment(entryAttachmentState)
|
||||||
|
coordinatorLayout?.let {
|
||||||
|
Snackbar.make(it, R.string.error_file_not_create, Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun populateViewsWithEntry(newEntry: EntryVersioned) {
|
override fun onPause() {
|
||||||
// Don't start the field reference manager, we want to see the raw ref
|
mAttachmentFileBinderManager?.unregisterProgressTask()
|
||||||
mDatabase?.stopManageEntry(newEntry)
|
|
||||||
|
|
||||||
// Set info in temp parameters
|
super.onPause()
|
||||||
temporarilySaveAndShowSelectedIcon(newEntry.icon)
|
|
||||||
|
|
||||||
// Set info in view
|
|
||||||
entryEditContentsView?.apply {
|
|
||||||
title = newEntry.title
|
|
||||||
username = if (newEntry.username.isEmpty()) mDatabase?.defaultUsername ?:"" else newEntry.username
|
|
||||||
url = newEntry.url
|
|
||||||
password = newEntry.password
|
|
||||||
notes = newEntry.notes
|
|
||||||
for (entry in newEntry.customFields.entries) {
|
|
||||||
post {
|
|
||||||
addNewCustomField(entry.key, entry.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun populateEntryWithViews(newEntry: EntryVersioned) {
|
|
||||||
|
|
||||||
mDatabase?.startManageEntry(newEntry)
|
|
||||||
|
|
||||||
newEntry.apply {
|
|
||||||
// Build info from view
|
|
||||||
entryEditContentsView?.let { entryView ->
|
|
||||||
removeAllFields()
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -235,45 +366,145 @@ class EntryEditActivity : LockingHideActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new customized field view and scroll to bottom
|
* Add a new customized field
|
||||||
*/
|
*/
|
||||||
private fun addNewCustomField() {
|
private fun addNewCustomField() {
|
||||||
entryEditContentsView?.addNewCustomField()
|
EntryCustomFieldDialogFragment.getInstance().show(supportFragmentManager, "customFieldDialog")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun editCustomField(field: Field) {
|
||||||
|
EntryCustomFieldDialogFragment.getInstance(field).show(supportFragmentManager, "customFieldDialog")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewCustomFieldApproved(newField: Field) {
|
||||||
|
entryEditFragment?.apply {
|
||||||
|
putExtraField(newField)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEditCustomFieldApproved(oldField: Field, newField: Field) {
|
||||||
|
entryEditFragment?.replaceExtraField(oldField, newField)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDeleteCustomFieldApproved(oldField: Field) {
|
||||||
|
entryEditFragment?.removeExtraField(oldField)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new attachment
|
||||||
|
*/
|
||||||
|
private fun addNewAttachment(item: MenuItem) {
|
||||||
|
mSelectFileHelper?.selectFileOnClickViewListener?.onMenuItemClick(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?) {
|
||||||
|
if (attachmentToUploadUri != null && fileName != null) {
|
||||||
|
buildNewAttachment(attachmentToUploadUri, fileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onValidateReplaceFile(attachmentToUploadUri: Uri?, attachment: Attachment?) {
|
||||||
|
startUploadAttachment(attachmentToUploadUri, attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startUploadAttachment(attachmentToUploadUri: Uri?, attachment: Attachment?) {
|
||||||
|
if (attachmentToUploadUri != null && attachment != null) {
|
||||||
|
// Start uploading in service
|
||||||
|
mAttachmentFileBinderManager?.startUploadAttachment(attachmentToUploadUri, attachment)
|
||||||
|
// Add in temp list
|
||||||
|
mTempAttachments.add(attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildNewAttachment(attachmentToUploadUri: Uri, fileName: String) {
|
||||||
|
val compression = mDatabase?.compressionForNewEntry() ?: false
|
||||||
|
mDatabase?.buildNewBinary(UriUtil.getBinaryDir(this), compression)?.let { binaryAttachment ->
|
||||||
|
val entryAttachment = Attachment(fileName, binaryAttachment)
|
||||||
|
// Ask to replace the current attachment
|
||||||
|
if ((mDatabase?.allowMultipleAttachments != true && entryEditFragment?.containsAttachment() == true) ||
|
||||||
|
entryEditFragment?.containsAttachment(EntryAttachmentState(entryAttachment, StreamDirection.UPLOAD)) == true) {
|
||||||
|
ReplaceFileDialogFragment.build(attachmentToUploadUri, entryAttachment)
|
||||||
|
.show(supportFragmentManager, "replacementFileFragment")
|
||||||
|
} else {
|
||||||
|
startUploadAttachment(attachmentToUploadUri, entryAttachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
|
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||||
|
uri?.let { attachmentToUploadUri ->
|
||||||
|
// TODO Async to get the name
|
||||||
|
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
|
||||||
|
documentFile.name?.let { fileName ->
|
||||||
|
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
|
||||||
|
FileTooBigDialogFragment.build(attachmentToUploadUri, fileName)
|
||||||
|
.show(supportFragmentManager, "fileTooBigFragment")
|
||||||
|
} else {
|
||||||
|
buildNewAttachment(attachmentToUploadUri, fileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up OTP (HOTP or TOTP) and add it as extra field
|
||||||
|
*/
|
||||||
|
private fun setupOTP() {
|
||||||
|
// Retrieve the current otpElement if exists
|
||||||
|
// and open the dialog to set up the OTP
|
||||||
|
SetOTPDialogFragment.build(entryEditFragment?.getEntryInfo()?.otpModel)
|
||||||
|
.show(supportFragmentManager, "addOTPDialog")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the new entry or update an existing entry in the database
|
* Saves the new entry or update an existing entry in the database
|
||||||
*/
|
*/
|
||||||
private fun saveEntry() {
|
private fun saveEntry() {
|
||||||
|
// Get the temp entry
|
||||||
|
entryEditFragment?.getEntryInfo()?.let { newEntryInfo ->
|
||||||
|
|
||||||
// Launch a validation and show the error if present
|
if (mIsNew) {
|
||||||
if (entryEditContentsView?.isValid() == true) {
|
// Create new one
|
||||||
// Clone the entry
|
mDatabase?.createEntry()
|
||||||
mNewEntry?.let { newEntry ->
|
} else {
|
||||||
|
// Create a clone
|
||||||
|
Entry(mEntry!!)
|
||||||
|
}?.let { newEntry ->
|
||||||
|
|
||||||
// WARNING Add the parent previously deleted
|
newEntry.setEntryInfo(mDatabase, newEntryInfo)
|
||||||
newEntry.parent = mEntry?.parent
|
|
||||||
// Build info
|
// Build info
|
||||||
newEntry.lastAccessTime = PwDate()
|
newEntry.lastAccessTime = DateInstant()
|
||||||
newEntry.lastModificationTime = PwDate()
|
newEntry.lastModificationTime = DateInstant()
|
||||||
|
|
||||||
populateEntryWithViews(newEntry)
|
// Delete temp attachment if not used
|
||||||
|
mTempAttachments.forEach {
|
||||||
|
mDatabase?.binaryPool?.let { binaryPool ->
|
||||||
|
if (!newEntry.getAttachments(binaryPool).contains(it)) {
|
||||||
|
mDatabase?.removeAttachmentIfNotUsed(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Open a progress dialog and save entry
|
// Open a progress dialog and save entry
|
||||||
if (mIsNew) {
|
if (mIsNew) {
|
||||||
mParent?.let { parent ->
|
mParent?.let { parent ->
|
||||||
progressDialogThread?.startDatabaseCreateEntry(
|
mProgressDatabaseTaskProvider?.startDatabaseCreateEntry(
|
||||||
newEntry,
|
newEntry,
|
||||||
parent,
|
parent,
|
||||||
!mReadOnly
|
!mReadOnly && mAutoSaveEnable
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mEntry?.let { oldEntry ->
|
mEntry?.let { oldEntry ->
|
||||||
progressDialogThread?.startDatabaseUpdateEntry(
|
mProgressDatabaseTaskProvider?.startDatabaseUpdateEntry(
|
||||||
oldEntry,
|
oldEntry,
|
||||||
newEntry,
|
newEntry,
|
||||||
!mReadOnly
|
!mReadOnly && mAutoSaveEnable
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,98 +512,159 @@ class EntryEditActivity : LockingHideActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
progressDialogThread?.registerProgressTask()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
progressDialogThread?.unregisterProgressTask()
|
|
||||||
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
|
|
||||||
val inflater = menuInflater
|
val inflater = menuInflater
|
||||||
inflater.inflate(R.menu.database_lock, menu)
|
inflater.inflate(R.menu.database, menu)
|
||||||
|
// Save database not needed here
|
||||||
|
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||||
|
|
||||||
entryEditActivityEducation?.let {
|
|
||||||
Handler().post { performedNextEducation(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
|
||||||
val passwordView = entryEditContentsView?.generatePasswordView
|
|
||||||
val addNewFieldView = entryEditContentsView?.addNewFieldView
|
|
||||||
|
|
||||||
val generatePasswordEducationPerformed = passwordView != null
|
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||||
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
entryEditActivityEducation?.let {
|
||||||
passwordView,
|
Handler(Looper.getMainLooper()).post { performedNextEducation(it) }
|
||||||
{
|
}
|
||||||
openPasswordGenerator()
|
return super.onPrepareOptionsMenu(menu)
|
||||||
},
|
}
|
||||||
{
|
|
||||||
performedNextEducation(entryEditActivityEducation)
|
fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
||||||
}
|
if (entryEditFragment?.generatePasswordEducationPerformed(entryEditActivityEducation) != true) {
|
||||||
)
|
val addNewFieldView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_field)
|
||||||
if (!generatePasswordEducationPerformed) {
|
val addNewFieldEducationPerformed = mDatabase?.allowEntryCustomFields() == true
|
||||||
// entryNewFieldEducationPerformed
|
&& addNewFieldView != null
|
||||||
mNewEntry != null && mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty()
|
&& addNewFieldView.isVisible
|
||||||
&& addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE
|
|
||||||
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
|
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
|
||||||
addNewFieldView,
|
addNewFieldView,
|
||||||
{
|
{
|
||||||
addNewCustomField()
|
addNewCustomField()
|
||||||
})
|
},
|
||||||
|
{
|
||||||
|
performedNextEducation(entryEditActivityEducation)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (!addNewFieldEducationPerformed) {
|
||||||
|
val attachmentView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_attachment)
|
||||||
|
val addAttachmentEducationPerformed = attachmentView != null
|
||||||
|
&& attachmentView.isVisible
|
||||||
|
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation(
|
||||||
|
attachmentView,
|
||||||
|
{
|
||||||
|
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(attachmentView)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
performedNextEducation(entryEditActivityEducation)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (!addAttachmentEducationPerformed) {
|
||||||
|
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
|
||||||
|
setupOtpView != null
|
||||||
|
&& setupOtpView.isVisible
|
||||||
|
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
|
||||||
|
setupOtpView,
|
||||||
|
{
|
||||||
|
setupOTP()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.menu_lock -> {
|
R.id.menu_save_database -> {
|
||||||
lockAndExit()
|
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.menu_contribute -> {
|
R.id.menu_contribute -> {
|
||||||
MenuUtil.onContributionItemSelected(this)
|
MenuUtil.onContributionItemSelected(this)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
android.R.id.home -> {
|
||||||
android.R.id.home -> finish()
|
onBackPressed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onOtpCreated(otpElement: OtpElement) {
|
||||||
|
var titleOTP: String? = null
|
||||||
|
var usernameOTP: String? = null
|
||||||
|
// Build a temp entry to get title and username (by ref)
|
||||||
|
entryEditFragment?.getEntryInfo()?.let { entryInfo ->
|
||||||
|
val entryTemp = mDatabase?.createEntry()
|
||||||
|
entryTemp?.setEntryInfo(mDatabase, entryInfo)
|
||||||
|
mDatabase?.startManageEntry(entryTemp)
|
||||||
|
titleOTP = entryTemp?.title
|
||||||
|
usernameOTP = entryTemp?.username
|
||||||
|
mDatabase?.stopManageEntry(mEntry)
|
||||||
|
}
|
||||||
|
// Update the otp field with otpauth:// url
|
||||||
|
val otpField = OtpEntryFields.buildOtpField(otpElement, titleOTP, usernameOTP)
|
||||||
|
mEntry?.putExtraField(Field(otpField.name, otpField.protectedValue))
|
||||||
|
entryEditFragment?.apply {
|
||||||
|
putExtraField(otpField)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun iconPicked(bundle: Bundle) {
|
override fun iconPicked(bundle: Bundle) {
|
||||||
IconPickerDialogFragment.getIconStandardFromBundle(bundle)?.let { icon ->
|
IconPickerDialogFragment.getIconStandardFromBundle(bundle)?.let { icon ->
|
||||||
temporarilySaveAndShowSelectedIcon(icon)
|
entryEditFragment?.icon = icon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) {
|
||||||
|
// To fix android 4.4 issue
|
||||||
|
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
|
||||||
|
if (datePicker?.isShown == true) {
|
||||||
|
entryEditFragment?.expiryTime?.date?.let { expiresDate ->
|
||||||
|
// Save the date
|
||||||
|
entryEditFragment?.expiryTime =
|
||||||
|
DateInstant(DateTime(expiresDate)
|
||||||
|
.withYear(year)
|
||||||
|
.withMonthOfYear(month + 1)
|
||||||
|
.withDayOfMonth(day)
|
||||||
|
.toDate())
|
||||||
|
// Launch the time picker
|
||||||
|
val dateTime = DateTime(expiresDate)
|
||||||
|
val defaultHour = dateTime.hourOfDay
|
||||||
|
val defaultMinute = dateTime.minuteOfHour
|
||||||
|
TimePickerFragment.getInstance(defaultHour, defaultMinute)
|
||||||
|
.show(supportFragmentManager, "TimePickerFragment")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTimeSet(timePicker: TimePicker?, hours: Int, minutes: Int) {
|
||||||
|
entryEditFragment?.expiryTime?.date?.let { expiresDate ->
|
||||||
|
// Save the date
|
||||||
|
entryEditFragment?.expiryTime =
|
||||||
|
DateInstant(DateTime(expiresDate)
|
||||||
|
.withHourOfDay(hours)
|
||||||
|
.withMinuteOfHour(minutes)
|
||||||
|
.toDate())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
mNewEntry?.let {
|
|
||||||
populateEntryWithViews(it)
|
outState.putParcelableArrayList(TEMP_ATTACHMENTS, mTempAttachments)
|
||||||
outState.putParcelable(KEY_NEW_ENTRY, it)
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun acceptPassword(bundle: Bundle) {
|
override fun acceptPassword(bundle: Bundle) {
|
||||||
bundle.getString(GeneratePasswordDialogFragment.KEY_PASSWORD_ID)?.let {
|
bundle.getString(GeneratePasswordDialogFragment.KEY_PASSWORD_ID)?.let {
|
||||||
entryEditContentsView?.password = it
|
entryEditFragment?.password = it
|
||||||
}
|
}
|
||||||
|
|
||||||
entryEditActivityEducation?.let {
|
entryEditActivityEducation?.let {
|
||||||
Handler().post { performedNextEducation(it) }
|
Handler(Looper.getMainLooper()).post { performedNextEducation(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,13 +672,22 @@ class EntryEditActivity : LockingHideActivity(),
|
|||||||
// Do nothing here
|
// Do nothing here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setMessage(R.string.discard_changes)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setPositiveButton(R.string.discard) { _, _ ->
|
||||||
|
super@EntryEditActivity.onBackPressed()
|
||||||
|
}.create().show()
|
||||||
|
}
|
||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
// Assign entry callback as a result in all case
|
// Assign entry callback as a result in all case
|
||||||
try {
|
try {
|
||||||
mNewEntry?.let {
|
mEntry?.let { entry ->
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
val intentEntry = Intent()
|
val intentEntry = Intent()
|
||||||
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, mNewEntry)
|
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry)
|
||||||
intentEntry.putExtras(bundle)
|
intentEntry.putExtras(bundle)
|
||||||
if (mIsNew) {
|
if (mIsNew) {
|
||||||
setResult(ADD_ENTRY_RESULT_CODE, intentEntry)
|
setResult(ADD_ENTRY_RESULT_CODE, intentEntry)
|
||||||
@@ -410,7 +711,7 @@ class EntryEditActivity : LockingHideActivity(),
|
|||||||
const val KEY_PARENT = "parent"
|
const val KEY_PARENT = "parent"
|
||||||
|
|
||||||
// SaveInstanceState
|
// SaveInstanceState
|
||||||
const val KEY_NEW_ENTRY = "new_entry"
|
const val TEMP_ATTACHMENTS = "TEMP_ATTACHMENTS"
|
||||||
|
|
||||||
// Keys for callback
|
// Keys for callback
|
||||||
const val ADD_ENTRY_RESULT_CODE = 31
|
const val ADD_ENTRY_RESULT_CODE = 31
|
||||||
@@ -418,13 +719,15 @@ class EntryEditActivity : LockingHideActivity(),
|
|||||||
const val ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129
|
const val ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129
|
||||||
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
|
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
|
||||||
|
|
||||||
|
const val ENTRY_EDIT_FRAGMENT_TAG = "ENTRY_EDIT_FRAGMENT_TAG"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch EntryEditActivity to update an existing entry
|
* Launch EntryEditActivity to update an existing entry
|
||||||
*
|
*
|
||||||
* @param activity from activity
|
* @param activity from activity
|
||||||
* @param pwEntry Entry to update
|
* @param pwEntry Entry to update
|
||||||
*/
|
*/
|
||||||
fun launch(activity: Activity, pwEntry: EntryVersioned) {
|
fun launch(activity: Activity, pwEntry: Entry) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||||
intent.putExtra(KEY_ENTRY, pwEntry.nodeId)
|
intent.putExtra(KEY_ENTRY, pwEntry.nodeId)
|
||||||
@@ -438,7 +741,7 @@ class EntryEditActivity : LockingHideActivity(),
|
|||||||
* @param activity from activity
|
* @param activity from activity
|
||||||
* @param pwGroup Group who will contains new entry
|
* @param pwGroup Group who will contains new entry
|
||||||
*/
|
*/
|
||||||
fun launch(activity: Activity, pwGroup: GroupVersioned) {
|
fun launch(activity: Activity, pwGroup: Group) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||||
intent.putExtra(KEY_PARENT, pwGroup.nodeId)
|
intent.putExtra(KEY_PARENT, pwGroup.nodeId)
|
||||||
|
|||||||
@@ -0,0 +1,546 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||||
|
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
|
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||||
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
|
import com.kunzisoft.keepass.model.*
|
||||||
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||||
|
import com.kunzisoft.keepass.view.collapse
|
||||||
|
import com.kunzisoft.keepass.view.expand
|
||||||
|
|
||||||
|
class EntryEditFragment: StylishFragment() {
|
||||||
|
|
||||||
|
private lateinit var entryTitleLayoutView: TextInputLayout
|
||||||
|
private lateinit var entryTitleView: EditText
|
||||||
|
private lateinit var entryIconView: ImageView
|
||||||
|
private lateinit var entryUserNameView: EditText
|
||||||
|
private lateinit var entryUrlView: EditText
|
||||||
|
private lateinit var entryPasswordLayoutView: TextInputLayout
|
||||||
|
private lateinit var entryPasswordView: EditText
|
||||||
|
private lateinit var entryPasswordGeneratorView: View
|
||||||
|
private lateinit var entryExpiresCheckBox: CompoundButton
|
||||||
|
private lateinit var entryExpiresTextView: TextView
|
||||||
|
private lateinit var entryNotesView: EditText
|
||||||
|
private lateinit var extraFieldsContainerView: View
|
||||||
|
private lateinit var extraFieldsListView: ViewGroup
|
||||||
|
private lateinit var attachmentsContainerView: View
|
||||||
|
private lateinit var attachmentsListView: RecyclerView
|
||||||
|
|
||||||
|
private lateinit var attachmentsAdapter: EntryAttachmentsItemsAdapter
|
||||||
|
|
||||||
|
private var fontInVisibility: Boolean = false
|
||||||
|
private var iconColor: Int = 0
|
||||||
|
private var expiresInstant: DateInstant = DateInstant.IN_ONE_MONTH
|
||||||
|
|
||||||
|
var drawFactory: IconDrawableFactory? = null
|
||||||
|
var setOnDateClickListener: View.OnClickListener? = null
|
||||||
|
var setOnPasswordGeneratorClickListener: View.OnClickListener? = null
|
||||||
|
var setOnIconViewClickListener: View.OnClickListener? = null
|
||||||
|
var setOnEditCustomField: ((Field) -> Unit)? = null
|
||||||
|
var setOnRemoveAttachment: ((Attachment) -> Unit)? = null
|
||||||
|
|
||||||
|
// Elements to modify the current entry
|
||||||
|
private var mEntryInfo = EntryInfo()
|
||||||
|
private var mLastFocusedEditField: FocusedEditField? = null
|
||||||
|
private var mExtraViewToRequestFocus: EditText? = null
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
|
val rootView = inflater.cloneInContext(contextThemed)
|
||||||
|
.inflate(R.layout.fragment_entry_edit_contents, container, false)
|
||||||
|
|
||||||
|
fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(requireContext())
|
||||||
|
|
||||||
|
entryTitleLayoutView = rootView.findViewById(R.id.entry_edit_container_title)
|
||||||
|
entryTitleView = rootView.findViewById(R.id.entry_edit_title)
|
||||||
|
entryIconView = rootView.findViewById(R.id.entry_edit_icon_button)
|
||||||
|
entryIconView.setOnClickListener {
|
||||||
|
setOnIconViewClickListener?.onClick(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
entryUserNameView = rootView.findViewById(R.id.entry_edit_user_name)
|
||||||
|
entryUrlView = rootView.findViewById(R.id.entry_edit_url)
|
||||||
|
entryPasswordLayoutView = rootView.findViewById(R.id.entry_edit_container_password)
|
||||||
|
entryPasswordView = rootView.findViewById(R.id.entry_edit_password)
|
||||||
|
entryPasswordGeneratorView = rootView.findViewById(R.id.entry_edit_password_generator_button)
|
||||||
|
entryPasswordGeneratorView.setOnClickListener {
|
||||||
|
setOnPasswordGeneratorClickListener?.onClick(it)
|
||||||
|
}
|
||||||
|
entryExpiresCheckBox = rootView.findViewById(R.id.entry_edit_expires_checkbox)
|
||||||
|
entryExpiresTextView = rootView.findViewById(R.id.entry_edit_expires_text)
|
||||||
|
entryExpiresTextView.setOnClickListener {
|
||||||
|
if (entryExpiresCheckBox.isChecked)
|
||||||
|
setOnDateClickListener?.onClick(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
entryNotesView = rootView.findViewById(R.id.entry_edit_notes)
|
||||||
|
|
||||||
|
extraFieldsContainerView = rootView.findViewById(R.id.extra_fields_container)
|
||||||
|
extraFieldsListView = rootView.findViewById(R.id.extra_fields_list)
|
||||||
|
|
||||||
|
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
|
||||||
|
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
|
||||||
|
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
|
||||||
|
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
|
||||||
|
if (previousSize > 0 && newSize == 0) {
|
||||||
|
attachmentsContainerView.collapse(true)
|
||||||
|
} else if (previousSize == 0 && newSize == 1) {
|
||||||
|
attachmentsContainerView.expand(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attachmentsListView.apply {
|
||||||
|
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||||
|
adapter = attachmentsAdapter
|
||||||
|
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
|
}
|
||||||
|
|
||||||
|
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
|
||||||
|
assignExpiresDateText()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the textColor to tint the icon
|
||||||
|
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
|
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
|
||||||
|
taIconColor?.recycle()
|
||||||
|
|
||||||
|
// Retrieve the new entry after an orientation change
|
||||||
|
if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true)
|
||||||
|
mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
|
||||||
|
else if (savedInstanceState?.containsKey(KEY_TEMP_ENTRY_INFO) == true) {
|
||||||
|
mEntryInfo = savedInstanceState.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedInstanceState?.containsKey(KEY_LAST_FOCUSED_FIELD) == true) {
|
||||||
|
mLastFocusedEditField = savedInstanceState.getParcelable(KEY_LAST_FOCUSED_FIELD) ?: mLastFocusedEditField
|
||||||
|
}
|
||||||
|
|
||||||
|
populateViewsWithEntry()
|
||||||
|
|
||||||
|
return rootView
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
super.onDetach()
|
||||||
|
|
||||||
|
drawFactory = null
|
||||||
|
setOnDateClickListener = null
|
||||||
|
setOnPasswordGeneratorClickListener = null
|
||||||
|
setOnIconViewClickListener = null
|
||||||
|
setOnRemoveAttachment = null
|
||||||
|
setOnEditCustomField = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryInfo(): EntryInfo? {
|
||||||
|
populateEntryWithViews()
|
||||||
|
return mEntryInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generatePasswordEducationPerformed(entryEditActivityEducation: EntryEditActivityEducation): Boolean {
|
||||||
|
return entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
||||||
|
entryPasswordGeneratorView,
|
||||||
|
{
|
||||||
|
GeneratePasswordDialogFragment().show(parentFragmentManager, "PasswordGeneratorFragment")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
(activity as? EntryEditActivity?)?.performedNextEducation(entryEditActivityEducation)
|
||||||
|
} catch (ignore: Exception) {}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateViewsWithEntry() {
|
||||||
|
// Set info in view
|
||||||
|
icon = mEntryInfo.icon
|
||||||
|
title = mEntryInfo.title
|
||||||
|
username = mEntryInfo.username
|
||||||
|
url = mEntryInfo.url
|
||||||
|
password = mEntryInfo.password
|
||||||
|
expires = mEntryInfo.expires
|
||||||
|
expiryTime = mEntryInfo.expiryTime
|
||||||
|
notes = mEntryInfo.notes
|
||||||
|
assignExtraFields(mEntryInfo.customFields) { fields ->
|
||||||
|
setOnEditCustomField?.invoke(fields)
|
||||||
|
}
|
||||||
|
assignAttachments(mEntryInfo.attachments, StreamDirection.UPLOAD) { attachment ->
|
||||||
|
setOnRemoveAttachment?.invoke(attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateEntryWithViews() {
|
||||||
|
// Icon already populate
|
||||||
|
mEntryInfo.title = title
|
||||||
|
mEntryInfo.username = username
|
||||||
|
mEntryInfo.url = url
|
||||||
|
mEntryInfo.password = password
|
||||||
|
mEntryInfo.expires = expires
|
||||||
|
mEntryInfo.expiryTime = expiryTime
|
||||||
|
mEntryInfo.notes = notes
|
||||||
|
mEntryInfo.customFields = getExtraFields()
|
||||||
|
mEntryInfo.otpModel = OtpEntryFields.parseFields { key ->
|
||||||
|
getExtraFields().firstOrNull { it.name == key }?.protectedValue?.toString()
|
||||||
|
}?.otpModel
|
||||||
|
mEntryInfo.attachments = getAttachments()
|
||||||
|
}
|
||||||
|
|
||||||
|
var title: String
|
||||||
|
get() {
|
||||||
|
return entryTitleView.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
entryTitleView.setText(value)
|
||||||
|
if (fontInVisibility)
|
||||||
|
entryTitleView.applyFontVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
var icon: IconImage
|
||||||
|
get() {
|
||||||
|
return mEntryInfo.icon
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
mEntryInfo.icon = value
|
||||||
|
drawFactory?.let { drawFactory ->
|
||||||
|
entryIconView.assignDatabaseIcon(drawFactory, value, iconColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var username: String
|
||||||
|
get() {
|
||||||
|
return entryUserNameView.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
entryUserNameView.setText(value)
|
||||||
|
if (fontInVisibility)
|
||||||
|
entryUserNameView.applyFontVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
var url: String
|
||||||
|
get() {
|
||||||
|
return entryUrlView.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
entryUrlView.setText(value)
|
||||||
|
if (fontInVisibility)
|
||||||
|
entryUrlView.applyFontVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
var password: String
|
||||||
|
get() {
|
||||||
|
return entryPasswordView.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
entryPasswordView.setText(value)
|
||||||
|
if (fontInVisibility) {
|
||||||
|
entryPasswordView.applyFontVisibility()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assignExpiresDateText() {
|
||||||
|
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
|
||||||
|
entryExpiresTextView.setOnClickListener(setOnDateClickListener)
|
||||||
|
expiresInstant.getDateTimeString(resources)
|
||||||
|
} else {
|
||||||
|
entryExpiresTextView.setOnClickListener(null)
|
||||||
|
resources.getString(R.string.never)
|
||||||
|
}
|
||||||
|
if (fontInVisibility)
|
||||||
|
entryExpiresTextView.applyFontVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
var expires: Boolean
|
||||||
|
get() {
|
||||||
|
return entryExpiresCheckBox.isChecked
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
if (!value) {
|
||||||
|
expiresInstant = DateInstant.IN_ONE_MONTH
|
||||||
|
}
|
||||||
|
entryExpiresCheckBox.isChecked = value
|
||||||
|
assignExpiresDateText()
|
||||||
|
}
|
||||||
|
|
||||||
|
var expiryTime: DateInstant
|
||||||
|
get() {
|
||||||
|
return if (expires)
|
||||||
|
expiresInstant
|
||||||
|
else
|
||||||
|
DateInstant.NEVER_EXPIRE
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
if (expires)
|
||||||
|
expiresInstant = value
|
||||||
|
assignExpiresDateText()
|
||||||
|
}
|
||||||
|
|
||||||
|
var notes: String
|
||||||
|
get() {
|
||||||
|
return entryNotesView.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
entryNotesView.setText(value)
|
||||||
|
if (fontInVisibility)
|
||||||
|
entryNotesView.applyFontVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------
|
||||||
|
* Extra Fields
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
private var mExtraFieldsList: MutableList<Field> = ArrayList()
|
||||||
|
private var mOnEditButtonClickListener: ((item: Field)->Unit)? = null
|
||||||
|
|
||||||
|
private fun buildViewFromField(extraField: Field): View? {
|
||||||
|
val inflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||||
|
val itemView: View? = inflater?.inflate(R.layout.item_entry_edit_extra_field, extraFieldsListView, false)
|
||||||
|
itemView?.id = View.NO_ID
|
||||||
|
|
||||||
|
val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container)
|
||||||
|
extraFieldValueContainer?.isPasswordVisibilityToggleEnabled = extraField.protectedValue.isProtected
|
||||||
|
extraFieldValueContainer?.hint = extraField.name
|
||||||
|
extraFieldValueContainer?.id = View.NO_ID
|
||||||
|
|
||||||
|
val extraFieldValue: TextInputEditText? = itemView?.findViewById(R.id.entry_extra_field_value)
|
||||||
|
extraFieldValue?.apply {
|
||||||
|
if (extraField.protectedValue.isProtected) {
|
||||||
|
inputType = extraFieldValue.inputType or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
|
||||||
|
}
|
||||||
|
setText(extraField.protectedValue.toString())
|
||||||
|
if (fontInVisibility)
|
||||||
|
applyFontVisibility()
|
||||||
|
}
|
||||||
|
extraFieldValue?.id = View.NO_ID
|
||||||
|
extraFieldValue?.tag = "FIELD_VALUE_TAG"
|
||||||
|
if (mLastFocusedEditField?.field == extraField) {
|
||||||
|
mExtraViewToRequestFocus = extraFieldValue
|
||||||
|
}
|
||||||
|
|
||||||
|
val extraFieldEditButton: View? = itemView?.findViewById(R.id.entry_extra_field_edit)
|
||||||
|
extraFieldEditButton?.setOnClickListener {
|
||||||
|
mOnEditButtonClickListener?.invoke(extraField)
|
||||||
|
}
|
||||||
|
extraFieldEditButton?.id = View.NO_ID
|
||||||
|
|
||||||
|
return itemView
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getExtraFields(): List<Field> {
|
||||||
|
mLastFocusedEditField = null
|
||||||
|
for (index in 0 until extraFieldsListView.childCount) {
|
||||||
|
val extraFieldValue: EditText = extraFieldsListView.getChildAt(index)
|
||||||
|
.findViewWithTag("FIELD_VALUE_TAG")
|
||||||
|
val extraField = mExtraFieldsList[index]
|
||||||
|
extraField.protectedValue.stringValue = extraFieldValue.text?.toString() ?: ""
|
||||||
|
if (extraFieldValue.isFocused) {
|
||||||
|
mLastFocusedEditField = FocusedEditField().apply {
|
||||||
|
field = extraField
|
||||||
|
cursorSelectionStart = extraFieldValue.selectionStart
|
||||||
|
cursorSelectionEnd = extraFieldValue.selectionEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mExtraFieldsList
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all children and add new views for each field
|
||||||
|
*/
|
||||||
|
fun assignExtraFields(fields: List<Field>,
|
||||||
|
onEditButtonClickListener: ((item: Field)->Unit)?) {
|
||||||
|
extraFieldsContainerView.visibility = if (fields.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
// Reinit focused field
|
||||||
|
mExtraFieldsList.clear()
|
||||||
|
mExtraFieldsList.addAll(fields)
|
||||||
|
extraFieldsListView.removeAllViews()
|
||||||
|
fields.forEach {
|
||||||
|
extraFieldsListView.addView(buildViewFromField(it))
|
||||||
|
}
|
||||||
|
// Request last focus
|
||||||
|
mLastFocusedEditField?.let { focusField ->
|
||||||
|
mExtraViewToRequestFocus?.apply {
|
||||||
|
requestFocus()
|
||||||
|
setSelection(focusField.cursorSelectionStart,
|
||||||
|
focusField.cursorSelectionEnd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mLastFocusedEditField = null
|
||||||
|
mOnEditButtonClickListener = onEditButtonClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an extra field or create a new one if doesn't exists, the old value is lost
|
||||||
|
*/
|
||||||
|
fun putExtraField(extraField: Field) {
|
||||||
|
extraFieldsContainerView.visibility = View.VISIBLE
|
||||||
|
val oldField = mExtraFieldsList.firstOrNull { it.name == extraField.name }
|
||||||
|
oldField?.let {
|
||||||
|
val index = mExtraFieldsList.indexOf(oldField)
|
||||||
|
mExtraFieldsList.removeAt(index)
|
||||||
|
mExtraFieldsList.add(index, extraField)
|
||||||
|
extraFieldsListView.removeViewAt(index)
|
||||||
|
val newView = buildViewFromField(extraField)
|
||||||
|
extraFieldsListView.addView(newView, index)
|
||||||
|
newView?.requestFocus()
|
||||||
|
} ?: kotlin.run {
|
||||||
|
mExtraFieldsList.add(extraField)
|
||||||
|
val newView = buildViewFromField(extraField)
|
||||||
|
extraFieldsListView.addView(newView)
|
||||||
|
newView?.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an extra field and keep the old value
|
||||||
|
*/
|
||||||
|
fun replaceExtraField(oldExtraField: Field, newExtraField: Field) {
|
||||||
|
extraFieldsContainerView.visibility = View.VISIBLE
|
||||||
|
val index = mExtraFieldsList.indexOf(oldExtraField)
|
||||||
|
val oldValueEditText: EditText = extraFieldsListView.getChildAt(index)
|
||||||
|
.findViewWithTag("FIELD_VALUE_TAG")
|
||||||
|
val oldValue = oldValueEditText.text.toString()
|
||||||
|
val newExtraFieldWithOldValue = Field(newExtraField).apply {
|
||||||
|
this.protectedValue.stringValue = oldValue
|
||||||
|
}
|
||||||
|
mExtraFieldsList.removeAt(index)
|
||||||
|
mExtraFieldsList.add(index, newExtraFieldWithOldValue)
|
||||||
|
extraFieldsListView.removeViewAt(index)
|
||||||
|
val newView = buildViewFromField(newExtraFieldWithOldValue)
|
||||||
|
extraFieldsListView.addView(newView, index)
|
||||||
|
newView?.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeExtraField(oldExtraField: Field) {
|
||||||
|
val previousSize = mExtraFieldsList.size
|
||||||
|
val index = mExtraFieldsList.indexOf(oldExtraField)
|
||||||
|
extraFieldsListView.getChildAt(index)?.let {
|
||||||
|
it.collapse(true) {
|
||||||
|
mExtraFieldsList.removeAt(index)
|
||||||
|
extraFieldsListView.removeViewAt(index)
|
||||||
|
val newSize = mExtraFieldsList.size
|
||||||
|
|
||||||
|
if (previousSize > 0 && newSize == 0) {
|
||||||
|
extraFieldsContainerView.collapse(true)
|
||||||
|
} else if (previousSize == 0 && newSize == 1) {
|
||||||
|
extraFieldsContainerView.expand(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------
|
||||||
|
* Attachments
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun getAttachments(): List<Attachment> {
|
||||||
|
return attachmentsAdapter.itemsList.map { it.attachment }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assignAttachments(attachments: List<Attachment>,
|
||||||
|
streamDirection: StreamDirection,
|
||||||
|
onDeleteItem: (attachment: Attachment)->Unit) {
|
||||||
|
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
|
||||||
|
attachmentsAdapter.onDeleteButtonClickListener = { item ->
|
||||||
|
onDeleteItem.invoke(item.attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun containsAttachment(): Boolean {
|
||||||
|
return !attachmentsAdapter.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun containsAttachment(attachment: EntryAttachmentState): Boolean {
|
||||||
|
return attachmentsAdapter.contains(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putAttachment(attachment: EntryAttachmentState) {
|
||||||
|
attachmentsContainerView.visibility = View.VISIBLE
|
||||||
|
attachmentsAdapter.putItem(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeAttachment(attachment: EntryAttachmentState) {
|
||||||
|
attachmentsAdapter.removeItem(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearAttachments() {
|
||||||
|
attachmentsAdapter.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAttachmentViewPosition(attachment: EntryAttachmentState, position: (Float) -> Unit) {
|
||||||
|
attachmentsListView.postDelayed({
|
||||||
|
position.invoke(attachmentsContainerView.y
|
||||||
|
+ attachmentsListView.y
|
||||||
|
+ (attachmentsListView.getChildAt(attachmentsAdapter.indexOf(attachment))?.y
|
||||||
|
?: 0F)
|
||||||
|
)
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
populateEntryWithViews()
|
||||||
|
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
|
||||||
|
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
|
||||||
|
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
|
||||||
|
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
|
||||||
|
|
||||||
|
fun getInstance(entryInfo: EntryInfo?): EntryEditFragment {
|
||||||
|
return EntryEditFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
|
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity to search or select entry in database,
|
||||||
|
* Commonly used with Magikeyboard
|
||||||
|
*/
|
||||||
|
class EntrySelectionLauncherActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
||||||
|
var sharedWebDomain: String? = null
|
||||||
|
|
||||||
|
when (intent?.action) {
|
||||||
|
Intent.ACTION_SEND -> {
|
||||||
|
if ("text/plain" == intent.type) {
|
||||||
|
// Retrieve web domain
|
||||||
|
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
||||||
|
sharedWebDomain = Uri.parse(it).host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setting to integrate Magikeyboard
|
||||||
|
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
|
||||||
|
|
||||||
|
// Build search param
|
||||||
|
val searchInfo = SearchInfo().apply {
|
||||||
|
webDomain = sharedWebDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
// If database is open
|
||||||
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
|
Database.getInstance(),
|
||||||
|
searchInfo,
|
||||||
|
{ items ->
|
||||||
|
// Items found
|
||||||
|
if (searchShareForMagikeyboard) {
|
||||||
|
if (items.size == 1) {
|
||||||
|
// Automatically populate keyboard
|
||||||
|
val entryPopulate = items[0]
|
||||||
|
populateKeyboardAndMoveAppToBackground(this,
|
||||||
|
entryPopulate,
|
||||||
|
intent)
|
||||||
|
} else {
|
||||||
|
// Select the one we want
|
||||||
|
GroupActivity.launchForEntrySelectionResult(this,
|
||||||
|
true,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
GroupActivity.launch(this,
|
||||||
|
true,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Show the database UI to select the entry
|
||||||
|
if (searchShareForMagikeyboard) {
|
||||||
|
GroupActivity.launchForEntrySelectionResult(this,
|
||||||
|
false,
|
||||||
|
searchInfo)
|
||||||
|
} else {
|
||||||
|
GroupActivity.launch(this,
|
||||||
|
false,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// If database not open
|
||||||
|
if (searchShareForMagikeyboard) {
|
||||||
|
FileDatabaseSelectActivity.launchForEntrySelectionResult(this,
|
||||||
|
searchInfo)
|
||||||
|
} else {
|
||||||
|
FileDatabaseSelectActivity.launch(this,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
finish()
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
||||||
|
entry: EntryInfo,
|
||||||
|
intent: Intent,
|
||||||
|
toast: Boolean = true) {
|
||||||
|
// Populate Magikeyboard with entry
|
||||||
|
MagikIME.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
||||||
|
// Consume the selection mode
|
||||||
|
EntrySelectionHelper.removeEntrySelectionModeFromIntent(intent)
|
||||||
|
activity.moveTaskToBack(true)
|
||||||
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
@@ -22,51 +22,59 @@ package com.kunzisoft.keepass.activities
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.preference.PreferenceManager
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.TextView
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||||
|
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.*
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
||||||
import kotlinx.android.synthetic.main.activity_file_selection.*
|
import kotlinx.android.synthetic.main.activity_file_selection.*
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
class FileDatabaseSelectActivity : StylishActivity(),
|
class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||||
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var fileListContainer: View? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var createButtonView: View? = null
|
private var createDatabaseButtonView: View? = null
|
||||||
private var openDatabaseButtonView: View? = null
|
private var openDatabaseButtonView: View? = null
|
||||||
|
|
||||||
|
private val databaseFilesViewModel: DatabaseFilesViewModel by viewModels()
|
||||||
|
|
||||||
// Adapter to manage database history list
|
// Adapter to manage database history list
|
||||||
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
|
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
|
||||||
|
|
||||||
@@ -74,9 +82,9 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
|
|
||||||
private var mDatabaseFileUri: Uri? = null
|
private var mDatabaseFileUri: Uri? = null
|
||||||
|
|
||||||
private var mOpenFileHelper: OpenFileHelper? = null
|
private var mSelectFileHelper: SelectFileHelper? = null
|
||||||
|
|
||||||
private var progressDialogThread: ProgressDialogThread? = null
|
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -84,31 +92,25 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
|
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
|
||||||
|
|
||||||
setContentView(R.layout.activity_file_selection)
|
setContentView(R.layout.activity_file_selection)
|
||||||
fileListContainer = findViewById(R.id.container_file_list)
|
coordinatorLayout = findViewById(R.id.activity_file_selection_coordinator_layout)
|
||||||
|
|
||||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||||
toolbar.title = ""
|
toolbar.title = ""
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
|
|
||||||
// Create button
|
// Create database button
|
||||||
createButtonView = findViewById(R.id.create_database_button)
|
createDatabaseButtonView = findViewById(R.id.create_database_button)
|
||||||
if (Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
createDatabaseButtonView?.setOnClickListener { createNewFile() }
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
type = "application/x-keepass"
|
|
||||||
}.resolveActivity(packageManager) == null) {
|
|
||||||
// No Activity found that can handle this intent.
|
|
||||||
createButtonView?.visibility = View.GONE
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
// There is an activity which can handle this intent.
|
|
||||||
createButtonView?.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
createButtonView?.setOnClickListener { createNewFile() }
|
// Open database button
|
||||||
|
mSelectFileHelper = SelectFileHelper(this)
|
||||||
mOpenFileHelper = OpenFileHelper(this)
|
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
||||||
openDatabaseButtonView = findViewById(R.id.open_database_button)
|
openDatabaseButtonView?.apply {
|
||||||
openDatabaseButtonView?.setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener)
|
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
||||||
|
setOnClickListener(it)
|
||||||
|
setOnLongClickListener(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// History list
|
// History list
|
||||||
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
||||||
@@ -117,28 +119,25 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
(fileDatabaseHistoryRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
(fileDatabaseHistoryRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
// Construct adapter with listeners
|
// Construct adapter with listeners
|
||||||
mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this)
|
mAdapterDatabaseHistory = FileDatabaseHistoryAdapter(this)
|
||||||
|
mAdapterDatabaseHistory?.setOnDefaultDatabaseListener { databaseFile ->
|
||||||
|
databaseFilesViewModel.setDefaultDatabase(databaseFile)
|
||||||
|
}
|
||||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
||||||
UriUtil.parse(fileDatabaseHistoryEntityToOpen.databaseUri)?.let { databaseFileUri ->
|
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
|
||||||
launchPasswordActivity(
|
launchPasswordActivity(
|
||||||
databaseFileUri,
|
databaseFileUri,
|
||||||
UriUtil.parse(fileDatabaseHistoryEntityToOpen.keyFileUri))
|
fileDatabaseHistoryEntityToOpen.keyFileUri
|
||||||
|
)
|
||||||
}
|
}
|
||||||
updateFileListVisibility()
|
|
||||||
}
|
}
|
||||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
||||||
// Remove from app database
|
// Remove from app database
|
||||||
mFileDatabaseHistoryAction?.deleteFileDatabaseHistory(fileDatabaseHistoryToDelete) { fileHistoryDeleted ->
|
databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete)
|
||||||
// Remove from adapter
|
|
||||||
fileHistoryDeleted?.let { databaseFileHistoryDeleted ->
|
|
||||||
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileHistoryDeleted)
|
|
||||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
|
||||||
updateFileListVisibility()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
mAdapterDatabaseHistory?.setOnSaveAliasListener { fileDatabaseHistoryWithNewAlias ->
|
mAdapterDatabaseHistory?.setOnSaveAliasListener { fileDatabaseHistoryWithNewAlias ->
|
||||||
mFileDatabaseHistoryAction?.addOrUpdateFileDatabaseHistory(fileDatabaseHistoryWithNewAlias)
|
// Update in app database
|
||||||
|
databaseFilesViewModel.updateDatabaseFile(fileDatabaseHistoryWithNewAlias)
|
||||||
}
|
}
|
||||||
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
|
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
|
||||||
|
|
||||||
@@ -146,13 +145,12 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
if (!(savedInstanceState != null
|
if (!(savedInstanceState != null
|
||||||
&& savedInstanceState.containsKey(EXTRA_STAY)
|
&& savedInstanceState.containsKey(EXTRA_STAY)
|
||||||
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
|
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
val databasePath = PreferencesUtil.getDefaultDatabasePath(this)
|
||||||
val databasePath = prefs.getString(PasswordActivity.KEY_DEFAULT_DATABASE_PATH, "")
|
|
||||||
|
|
||||||
UriUtil.parse(databasePath)?.let { databaseFileUri ->
|
UriUtil.parse(databasePath)?.let { databaseFileUri ->
|
||||||
launchPasswordActivityWithPath(databaseFileUri)
|
launchPasswordActivityWithPath(databaseFileUri)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
Log.i(TAG, "Unable to launch Password Activity")
|
Log.i(TAG, "No default database to prepare")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,14 +160,48 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
mDatabaseFileUri = savedInstanceState.getParcelable(EXTRA_DATABASE_URI)
|
mDatabaseFileUri = savedInstanceState.getParcelable(EXTRA_DATABASE_URI)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Observe list of databases
|
||||||
|
databaseFilesViewModel.databaseFilesLoaded.observe(this, Observer { databaseFiles ->
|
||||||
|
when (databaseFiles.databaseFileAction) {
|
||||||
|
DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
|
||||||
|
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
|
||||||
|
}
|
||||||
|
DatabaseFilesViewModel.DatabaseFileAction.ADD -> {
|
||||||
|
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
|
||||||
|
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
|
||||||
|
}
|
||||||
|
GroupActivity.launch(this@FileDatabaseSelectActivity)
|
||||||
|
}
|
||||||
|
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
|
||||||
|
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
|
||||||
|
mAdapterDatabaseHistory?.updateDatabaseFileHistory(databaseFileToUpdate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DatabaseFilesViewModel.DatabaseFileAction.DELETE -> {
|
||||||
|
databaseFiles.databaseFileToActivate?.let { databaseFileToDelete ->
|
||||||
|
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileToDelete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
databaseFilesViewModel.consumeAction()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Observe default database
|
||||||
|
databaseFilesViewModel.defaultDatabase.observe(this, Observer {
|
||||||
|
// Retrieve settings for default database
|
||||||
|
mAdapterDatabaseHistory?.setDefaultDatabase(it)
|
||||||
|
})
|
||||||
|
|
||||||
// Attach the dialog thread to this activity
|
// Attach the dialog thread to this activity
|
||||||
progressDialogThread = ProgressDialogThread(this) { actionTask, result ->
|
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
|
||||||
when (actionTask) {
|
onActionFinish = { actionTask, result ->
|
||||||
ACTION_DATABASE_CREATE_TASK -> {
|
when (actionTask) {
|
||||||
// TODO Check
|
ACTION_DATABASE_CREATE_TASK -> {
|
||||||
// mAdapterDatabaseHistory?.notifyDataSetChanged()
|
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
|
||||||
// updateFileListVisibility()
|
val keyFileUri = result.data?.getParcelable<Uri?>(KEY_FILE_URI_KEY)
|
||||||
GroupActivity.launch(this)
|
databaseFilesViewModel.addDatabaseFile(databaseUri, keyFileUri)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,74 +210,83 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
/**
|
/**
|
||||||
* Create a new file by calling the content provider
|
* Create a new file by calling the content provider
|
||||||
*/
|
*/
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
private fun createNewFile() {
|
private fun createNewFile() {
|
||||||
try {
|
createDocument(this, getString(R.string.database_file_name_default) +
|
||||||
startActivityForResult(Intent(
|
getString(R.string.database_file_extension_default), "application/x-keepass")
|
||||||
Intent.ACTION_CREATE_DOCUMENT).apply {
|
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
type = "application/x-keepass"
|
|
||||||
putExtra(Intent.EXTRA_TITLE, getString(R.string.database_file_name_default) +
|
|
||||||
getString(R.string.database_file_extension_default))
|
|
||||||
},
|
|
||||||
CREATE_FILE_REQUEST_CODE)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
BrowserDialogFragment().show(supportFragmentManager, "browserDialog")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fileNoFoundAction(e: FileNotFoundException) {
|
private fun fileNoFoundAction(e: FileNotFoundException) {
|
||||||
val error = getString(R.string.file_not_found_content)
|
val error = getString(R.string.file_not_found_content)
|
||||||
Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show()
|
coordinatorLayout?.let {
|
||||||
|
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
}
|
||||||
Log.e(TAG, error, e)
|
Log.e(TAG, error, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
||||||
|
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
|
||||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
PasswordActivity.launch(this@FileDatabaseSelectActivity,
|
PasswordActivity.launch(this@FileDatabaseSelectActivity,
|
||||||
databaseUri, keyFile)
|
databaseUri, keyFile,
|
||||||
|
searchInfo)
|
||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
fileNoFoundAction(e)
|
fileNoFoundAction(e)
|
||||||
}
|
}
|
||||||
|
// Remove the search info from intent
|
||||||
|
if (searchInfo != null) {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
PasswordActivity.launchForKeyboardResult(this@FileDatabaseSelectActivity,
|
PasswordActivity.launchForKeyboardResult(this@FileDatabaseSelectActivity,
|
||||||
databaseUri, keyFile)
|
databaseUri, keyFile,
|
||||||
finish()
|
searchInfo)
|
||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
fileNoFoundAction(e)
|
fileNoFoundAction(e)
|
||||||
}
|
}
|
||||||
|
finish()
|
||||||
},
|
},
|
||||||
{ assistStructure ->
|
{ assistStructure ->
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
try {
|
try {
|
||||||
PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
PasswordActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
||||||
databaseUri, keyFile,
|
databaseUri, keyFile,
|
||||||
assistStructure)
|
assistStructure,
|
||||||
|
searchInfo)
|
||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
fileNoFoundAction(e)
|
fileNoFoundAction(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchGroupActivity(readOnly: Boolean) {
|
private fun launchGroupActivity(readOnly: Boolean) {
|
||||||
|
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
|
||||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||||
{
|
{
|
||||||
GroupActivity.launch(this@FileDatabaseSelectActivity, readOnly)
|
GroupActivity.launch(this@FileDatabaseSelectActivity,
|
||||||
|
false,
|
||||||
|
searchInfo,
|
||||||
|
readOnly)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
GroupActivity.launchForKeyboardSelection(this@FileDatabaseSelectActivity, readOnly)
|
GroupActivity.launchForEntrySelectionResult(this@FileDatabaseSelectActivity,
|
||||||
|
false,
|
||||||
|
searchInfo,
|
||||||
|
readOnly)
|
||||||
// Do not keep history
|
// Do not keep history
|
||||||
finish()
|
finish()
|
||||||
},
|
},
|
||||||
{ assistStructure ->
|
{ assistStructure ->
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
GroupActivity.launchForAutofillResult(this@FileDatabaseSelectActivity, assistStructure, readOnly)
|
GroupActivity.launchForAutofillResult(this@FileDatabaseSelectActivity,
|
||||||
|
assistStructure,
|
||||||
|
false,
|
||||||
|
searchInfo,
|
||||||
|
readOnly)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -257,51 +298,43 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
overridePendingTransition(0, 0)
|
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() {
|
override fun onResume() {
|
||||||
val database = Database.getInstance()
|
|
||||||
if (database.loaded) {
|
|
||||||
launchGroupActivity(database.isReadOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
updateExternalStorageWarning()
|
// Show open and create button or special mode
|
||||||
|
if (mSelectionMode) {
|
||||||
// Construct adapter with listeners
|
// Disable create button if in selection mode or request for autofill
|
||||||
mFileDatabaseHistoryAction?.getAllFileDatabaseHistories { databaseFileHistoryList ->
|
createDatabaseButtonView?.visibility = View.GONE
|
||||||
databaseFileHistoryList?.let {
|
} else {
|
||||||
mAdapterDatabaseHistory?.addDatabaseFileHistoryList(it)
|
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
||||||
updateFileListVisibility()
|
// There is an activity which can handle this intent.
|
||||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
createDatabaseButtonView?.visibility = View.VISIBLE
|
||||||
|
} else{
|
||||||
|
// No Activity found that can handle this intent.
|
||||||
|
createDatabaseButtonView?.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register progress task
|
val database = Database.getInstance()
|
||||||
progressDialogThread?.registerProgressTask()
|
if (database.loaded) {
|
||||||
|
launchGroupActivity(database.isReadOnly)
|
||||||
|
} else {
|
||||||
|
// Construct adapter with listeners
|
||||||
|
if (PreferencesUtil.showRecentFiles(this)) {
|
||||||
|
databaseFilesViewModel.loadListOfDatabases()
|
||||||
|
} else {
|
||||||
|
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
||||||
|
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register progress task
|
||||||
|
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
// Unregister progress task
|
// Unregister progress task
|
||||||
progressDialogThread?.unregisterProgressTask()
|
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
@@ -314,13 +347,6 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateFileListVisibility() {
|
|
||||||
if (mAdapterDatabaseHistory?.itemCount == 0)
|
|
||||||
fileListContainer?.visibility = View.INVISIBLE
|
|
||||||
else
|
|
||||||
fileListContainer?.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAssignKeyDialogPositiveClick(
|
override fun onAssignKeyDialogPositiveClick(
|
||||||
masterPasswordChecked: Boolean, masterPassword: String?,
|
masterPasswordChecked: Boolean, masterPassword: String?,
|
||||||
keyFileChecked: Boolean, keyFile: Uri?) {
|
keyFileChecked: Boolean, keyFile: Uri?) {
|
||||||
@@ -329,7 +355,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
mDatabaseFileUri?.let { databaseUri ->
|
mDatabaseFileUri?.let { databaseUri ->
|
||||||
|
|
||||||
// Create the new database
|
// Create the new database
|
||||||
progressDialogThread?.startDatabaseCreate(
|
mProgressDatabaseTaskProvider?.startDatabaseCreate(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
masterPasswordChecked,
|
masterPasswordChecked,
|
||||||
masterPassword,
|
masterPassword,
|
||||||
@@ -357,42 +383,48 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||||
) { uri ->
|
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
launchPasswordActivityWithPath(uri)
|
launchPasswordActivityWithPath(uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the created URI from the file manager
|
// Retrieve the created URI from the file manager
|
||||||
if (requestCode == CREATE_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
|
onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
|
||||||
mDatabaseFileUri = data?.data
|
mDatabaseFileUri = databaseFileCreatedUri
|
||||||
if (mDatabaseFileUri != null) {
|
if (mDatabaseFileUri != null) {
|
||||||
AssignMasterKeyDialogFragment.getInstance(true)
|
AssignMasterKeyDialogFragment.getInstance(true)
|
||||||
.show(supportFragmentManager, "passwordDialog")
|
.show(supportFragmentManager, "passwordDialog")
|
||||||
|
} else {
|
||||||
|
val error = getString(R.string.error_create_database)
|
||||||
|
coordinatorLayout?.let {
|
||||||
|
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
}
|
||||||
|
Log.e(TAG, error)
|
||||||
}
|
}
|
||||||
// else {
|
|
||||||
// TODO Show error
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
MenuUtil.defaultMenuInflater(menuInflater, menu)
|
|
||||||
|
|
||||||
Handler().post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
if (!mSelectionMode) {
|
||||||
|
MenuUtil.defaultMenuInflater(menuInflater, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
Handler(Looper.getMainLooper()).post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
|
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
|
||||||
// If no recent files
|
// If no recent files
|
||||||
val createDatabaseEducationPerformed = createButtonView != null && createButtonView!!.visibility == View.VISIBLE
|
val createDatabaseEducationPerformed =
|
||||||
|
createDatabaseButtonView != null && createDatabaseButtonView!!.visibility == View.VISIBLE
|
||||||
&& mAdapterDatabaseHistory != null
|
&& mAdapterDatabaseHistory != null
|
||||||
&& mAdapterDatabaseHistory!!.itemCount > 0
|
&& mAdapterDatabaseHistory!!.itemCount > 0
|
||||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
||||||
createButtonView!!,
|
createDatabaseButtonView!!,
|
||||||
{
|
{
|
||||||
createNewFile()
|
createNewFile()
|
||||||
},
|
},
|
||||||
@@ -407,7 +439,7 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
openDatabaseButtonView!!,
|
openDatabaseButtonView!!,
|
||||||
{tapTargetView ->
|
{tapTargetView ->
|
||||||
tapTargetView?.let {
|
tapTargetView?.let {
|
||||||
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it)
|
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
@@ -416,6 +448,10 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
|
||||||
|
}
|
||||||
|
|
||||||
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
|
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,22 +461,32 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
private const val EXTRA_STAY = "EXTRA_STAY"
|
private const val EXTRA_STAY = "EXTRA_STAY"
|
||||||
private const val EXTRA_DATABASE_URI = "EXTRA_DATABASE_URI"
|
private const val EXTRA_DATABASE_URI = "EXTRA_DATABASE_URI"
|
||||||
|
|
||||||
private const val CREATE_FILE_REQUEST_CODE = 3853
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* No Standard Launch, pass by PasswordActivity
|
* Launch only to standard search, else pass by PasswordActivity
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
fun launch(context: Context,
|
||||||
|
searchInfo: SearchInfo? = null) {
|
||||||
|
val intent = Intent(context, FileDatabaseSelectActivity::class.java)
|
||||||
|
searchInfo?.let {
|
||||||
|
intent.putExtra(KEY_SEARCH_INFO, it)
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Keyboard Launch
|
* Keyboard Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun launchForKeyboardSelection(activity: Activity) {
|
fun launchForEntrySelectionResult(activity: Activity,
|
||||||
EntrySelectionHelper.startActivityForEntrySelection(activity, Intent(activity, FileDatabaseSelectActivity::class.java))
|
searchInfo: SearchInfo? = null) {
|
||||||
|
EntrySelectionHelper.startActivityForEntrySelectionResult(activity,
|
||||||
|
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||||
|
searchInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -450,10 +496,13 @@ class FileDatabaseSelectActivity : StylishActivity(),
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
fun launchForAutofillResult(activity: Activity, assistStructure: AssistStructure) {
|
fun launchForAutofillResult(activity: Activity,
|
||||||
|
assistStructure: AssistStructure,
|
||||||
|
searchInfo: SearchInfo? = null) {
|
||||||
AutofillHelper.startActivityForAutofillResult(activity,
|
AutofillHelper.startActivityForAutofillResult(activity,
|
||||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||||
assistStructure)
|
assistStructure,
|
||||||
|
searchInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.preference.PreferenceManager
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -18,16 +35,16 @@ import androidx.appcompat.view.ActionMode
|
|||||||
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.adapters.NodeAdapter
|
import com.kunzisoft.keepass.adapters.NodeAdapter
|
||||||
import com.kunzisoft.keepass.database.SortNodeEnum
|
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||||
import com.kunzisoft.keepass.database.element.GroupVersioned
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
import com.kunzisoft.keepass.database.element.NodeVersioned
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener {
|
class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener {
|
||||||
@@ -35,8 +52,8 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
private var nodeClickListener: NodeClickListener? = null
|
private var nodeClickListener: NodeClickListener? = null
|
||||||
private var onScrollListener: OnScrollListener? = null
|
private var onScrollListener: OnScrollListener? = null
|
||||||
|
|
||||||
private var listView: RecyclerView? = null
|
private var mNodesRecyclerView: RecyclerView? = null
|
||||||
var mainGroup: GroupVersioned? = null
|
var mainGroup: Group? = null
|
||||||
private set
|
private set
|
||||||
private var mAdapter: NodeAdapter? = null
|
private var mAdapter: NodeAdapter? = null
|
||||||
|
|
||||||
@@ -44,14 +61,12 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
private set
|
private set
|
||||||
var nodeActionPasteMode: PasteMode = PasteMode.UNDEFINED
|
var nodeActionPasteMode: PasteMode = PasteMode.UNDEFINED
|
||||||
private set
|
private set
|
||||||
private val listActionNodes = LinkedList<NodeVersioned>()
|
private val listActionNodes = LinkedList<Node>()
|
||||||
private val listPasteNodes = LinkedList<NodeVersioned>()
|
private val listPasteNodes = LinkedList<Node>()
|
||||||
|
|
||||||
private var notFoundView: View? = null
|
private var notFoundView: View? = null
|
||||||
private var isASearchResult: Boolean = false
|
private var isASearchResult: Boolean = false
|
||||||
|
|
||||||
// Preferences for sorting
|
|
||||||
private var prefs: SharedPreferences? = null
|
|
||||||
|
|
||||||
private var readOnly: Boolean = false
|
private var readOnly: Boolean = false
|
||||||
get() {
|
get() {
|
||||||
@@ -82,6 +97,12 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
nodeClickListener = null
|
||||||
|
onScrollListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -103,7 +124,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
mAdapter = NodeAdapter(context)
|
mAdapter = NodeAdapter(context)
|
||||||
mAdapter?.apply {
|
mAdapter?.apply {
|
||||||
setOnNodeClickListener(object : NodeAdapter.NodeClickCallback {
|
setOnNodeClickListener(object : NodeAdapter.NodeClickCallback {
|
||||||
override fun onNodeClick(node: NodeVersioned) {
|
override fun onNodeClick(node: Node) {
|
||||||
if (nodeActionSelectionMode) {
|
if (nodeActionSelectionMode) {
|
||||||
if (listActionNodes.contains(node)) {
|
if (listActionNodes.contains(node)) {
|
||||||
// Remove selected item if already selected
|
// Remove selected item if already selected
|
||||||
@@ -120,7 +141,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNodeLongClick(node: NodeVersioned): Boolean {
|
override fun onNodeLongClick(node: Node): Boolean {
|
||||||
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
|
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
|
||||||
// Select the first item after a long click
|
// Select the first item after a long click
|
||||||
if (!listActionNodes.contains(node))
|
if (!listActionNodes.contains(node))
|
||||||
@@ -136,7 +157,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
@@ -150,11 +170,17 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
// To apply theme
|
// To apply theme
|
||||||
val rootView = inflater.cloneInContext(contextThemed)
|
val rootView = inflater.cloneInContext(contextThemed)
|
||||||
.inflate(R.layout.fragment_list_nodes, container, false)
|
.inflate(R.layout.fragment_list_nodes, container, false)
|
||||||
listView = rootView.findViewById(R.id.nodes_list)
|
mNodesRecyclerView = rootView.findViewById(R.id.nodes_list)
|
||||||
notFoundView = rootView.findViewById(R.id.not_found_container)
|
notFoundView = rootView.findViewById(R.id.not_found_container)
|
||||||
|
|
||||||
|
mNodesRecyclerView?.apply {
|
||||||
|
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
||||||
|
layoutManager = LinearLayoutManager(context)
|
||||||
|
adapter = mAdapter
|
||||||
|
}
|
||||||
|
|
||||||
onScrollListener?.let { onScrollListener ->
|
onScrollListener?.let { onScrollListener ->
|
||||||
listView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
mNodesRecyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
onScrollListener.onScrolled(dy)
|
onScrollListener.onScrolled(dy)
|
||||||
@@ -162,8 +188,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
rebuildList()
|
|
||||||
|
|
||||||
return rootView
|
return rootView
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,14 +199,14 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Refresh data
|
// Refresh data
|
||||||
mAdapter?.notifyDataSetChanged()
|
rebuildList()
|
||||||
|
|
||||||
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
|
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
|
||||||
// To show the " no search entry found "
|
// To show the " no search entry found "
|
||||||
listView?.visibility = View.GONE
|
mNodesRecyclerView?.visibility = View.GONE
|
||||||
notFoundView?.visibility = View.VISIBLE
|
notFoundView?.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
listView?.visibility = View.VISIBLE
|
mNodesRecyclerView?.visibility = View.VISIBLE
|
||||||
notFoundView?.visibility = View.GONE
|
notFoundView?.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,30 +214,27 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
fun rebuildList() {
|
fun rebuildList() {
|
||||||
// Add elements to the list
|
// Add elements to the list
|
||||||
mainGroup?.let { mainGroup ->
|
mainGroup?.let { mainGroup ->
|
||||||
mAdapter?.rebuildList(mainGroup)
|
mAdapter?.apply {
|
||||||
}
|
rebuildList(mainGroup)
|
||||||
listView?.apply {
|
// To visually change the elements
|
||||||
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
if (PreferencesUtil.APPEARANCE_CHANGED) {
|
||||||
layoutManager = LinearLayoutManager(context)
|
notifyDataSetChanged()
|
||||||
adapter = mAdapter
|
PreferencesUtil.APPEARANCE_CHANGED = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSortSelected(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean, recycleBinBottom: Boolean) {
|
override fun onSortSelected(sortNodeEnum: SortNodeEnum,
|
||||||
// Toggle setting
|
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
||||||
prefs?.edit()?.apply {
|
// Save setting
|
||||||
putString(getString(R.string.sort_node_key), sortNodeEnum.name)
|
context?.let {
|
||||||
putBoolean(getString(R.string.sort_ascending_key), ascending)
|
PreferencesUtil.saveNodeSort(it, sortNodeEnum, sortNodeParameters)
|
||||||
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
|
// Tell the adapter to refresh it's list
|
||||||
mAdapter?.notifyChangeSort(sortNodeEnum, ascending, groupsBefore)
|
mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters)
|
||||||
mainGroup?.let { mainGroup ->
|
rebuildList()
|
||||||
mAdapter?.rebuildList(mainGroup)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
@@ -228,8 +249,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
R.id.menu_sort -> {
|
R.id.menu_sort -> {
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
val sortDialogFragment: SortDialogFragment =
|
val sortDialogFragment: SortDialogFragment =
|
||||||
if (Database.getInstance().allowRecycleBin
|
if (Database.getInstance().isRecycleBinEnabled) {
|
||||||
&& Database.getInstance().isRecycleBinEnabled) {
|
|
||||||
SortDialogFragment.getInstance(
|
SortDialogFragment.getInstance(
|
||||||
PreferencesUtil.getListSort(context),
|
PreferencesUtil.getListSort(context),
|
||||||
PreferencesUtil.getAscendingSort(context),
|
PreferencesUtil.getAscendingSort(context),
|
||||||
@@ -251,15 +271,16 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun actionNodesCallback(nodes: List<NodeVersioned>,
|
fun actionNodesCallback(nodes: List<Node>,
|
||||||
menuListener: NodesActionMenuListener?) : ActionMode.Callback {
|
menuListener: NodesActionMenuListener?,
|
||||||
|
actionModeCallback: ActionMode.Callback) : ActionMode.Callback {
|
||||||
|
|
||||||
return object : ActionMode.Callback {
|
return object : ActionMode.Callback {
|
||||||
|
|
||||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||||
nodeActionSelectionMode = false
|
nodeActionSelectionMode = false
|
||||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||||
return true
|
return actionModeCallback.onCreateActionMode(mode, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||||
@@ -276,7 +297,8 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
// Open and Edit for a single item
|
// Open and Edit for a single item
|
||||||
if (nodes.size == 1) {
|
if (nodes.size == 1) {
|
||||||
// Edition
|
// Edition
|
||||||
if (readOnly || nodes[0] == database.recycleBin) {
|
if (readOnly
|
||||||
|
|| (database.isRecycleBinEnabled && nodes[0] == database.recycleBin)) {
|
||||||
menu?.removeItem(R.id.menu_edit)
|
menu?.removeItem(R.id.menu_edit)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -287,15 +309,15 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
// Copy and Move (not for groups)
|
// Copy and Move (not for groups)
|
||||||
if (readOnly
|
if (readOnly
|
||||||
|| isASearchResult
|
|| isASearchResult
|
||||||
|| nodes.any { it == database.recycleBin }
|
|
||||||
|| nodes.any { it.type == Type.GROUP }) {
|
|| nodes.any { it.type == Type.GROUP }) {
|
||||||
// TODO COPY For Group
|
// TODO Copy For Group
|
||||||
menu?.removeItem(R.id.menu_copy)
|
menu?.removeItem(R.id.menu_copy)
|
||||||
menu?.removeItem(R.id.menu_move)
|
menu?.removeItem(R.id.menu_move)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deletion
|
// Deletion
|
||||||
if (readOnly || nodes.any { it == database.recycleBin }) {
|
if (readOnly
|
||||||
|
|| (database.isRecycleBinEnabled && nodes.any { it == database.recycleBin })) {
|
||||||
menu?.removeItem(R.id.menu_delete)
|
menu?.removeItem(R.id.menu_delete)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -303,7 +325,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
// Add the number of items selected in title
|
// Add the number of items selected in title
|
||||||
mode?.title = nodes.size.toString()
|
mode?.title = nodes.size.toString()
|
||||||
|
|
||||||
return true
|
return actionModeCallback.onPrepareActionMode(mode, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
||||||
@@ -333,7 +355,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
nodeActionSelectionMode = false
|
nodeActionSelectionMode = false
|
||||||
returnValue
|
returnValue
|
||||||
}
|
}
|
||||||
else -> false
|
else -> actionModeCallback.onActionItemClicked(mode, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,6 +365,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
mAdapter?.unselectActionNodes()
|
mAdapter?.unselectActionNodes()
|
||||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||||
nodeActionSelectionMode = false
|
nodeActionSelectionMode = false
|
||||||
|
actionModeCallback.onDestroyActionMode(mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -354,46 +377,42 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
||||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE
|
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE
|
||||||
|| resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
|
|| resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
|
||||||
data?.getParcelableExtra<NodeVersioned>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { newNode ->
|
data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { changedNode ->
|
||||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
|
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
|
||||||
mAdapter?.addNode(newNode)
|
addNode(changedNode)
|
||||||
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
|
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE)
|
||||||
//mAdapter.updateLastNodeRegister(newNode);
|
mAdapter?.notifyDataSetChanged()
|
||||||
mainGroup?.let { mainGroup ->
|
|
||||||
mAdapter?.rebuildList(mainGroup)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
|
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun contains(node: NodeVersioned): Boolean {
|
fun contains(node: Node): Boolean {
|
||||||
return mAdapter?.contains(node) ?: false
|
return mAdapter?.contains(node) ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addNode(newNode: NodeVersioned) {
|
fun addNode(newNode: Node) {
|
||||||
mAdapter?.addNode(newNode)
|
mAdapter?.addNode(newNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addNodes(newNodes: List<NodeVersioned>) {
|
fun addNodes(newNodes: List<Node>) {
|
||||||
mAdapter?.addNodes(newNodes)
|
mAdapter?.addNodes(newNodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned? = null) {
|
fun updateNode(oldNode: Node, newNode: Node? = null) {
|
||||||
mAdapter?.updateNode(oldNode, newNode ?: oldNode)
|
mAdapter?.updateNode(oldNode, newNode ?: oldNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateNodes(oldNodes: List<NodeVersioned>, newNodes: List<NodeVersioned>) {
|
fun updateNodes(oldNodes: List<Node>, newNodes: List<Node>) {
|
||||||
mAdapter?.updateNodes(oldNodes, newNodes)
|
mAdapter?.updateNodes(oldNodes, newNodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeNode(pwNode: NodeVersioned) {
|
fun removeNode(pwNode: Node) {
|
||||||
mAdapter?.removeNode(pwNode)
|
mAdapter?.removeNode(pwNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeNodes(nodes: List<NodeVersioned>) {
|
fun removeNodes(nodes: List<Node>) {
|
||||||
mAdapter?.removeNodes(nodes)
|
mAdapter?.removeNodes(nodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,20 +428,20 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
* Callback listener to redefine to do an action when a node is click
|
* Callback listener to redefine to do an action when a node is click
|
||||||
*/
|
*/
|
||||||
interface NodeClickListener {
|
interface NodeClickListener {
|
||||||
fun onNodeClick(node: NodeVersioned)
|
fun onNodeClick(node: Node)
|
||||||
fun onNodeSelected(nodes: List<NodeVersioned>): Boolean
|
fun onNodeSelected(nodes: List<Node>): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Menu listener to redefine to do an action in menu
|
* Menu listener to redefine to do an action in menu
|
||||||
*/
|
*/
|
||||||
interface NodesActionMenuListener {
|
interface NodesActionMenuListener {
|
||||||
fun onOpenMenuClick(node: NodeVersioned): Boolean
|
fun onOpenMenuClick(node: Node): Boolean
|
||||||
fun onEditMenuClick(node: NodeVersioned): Boolean
|
fun onEditMenuClick(node: Node): Boolean
|
||||||
fun onCopyMenuClick(nodes: List<NodeVersioned>): Boolean
|
fun onCopyMenuClick(nodes: List<Node>): Boolean
|
||||||
fun onMoveMenuClick(nodes: List<NodeVersioned>): Boolean
|
fun onMoveMenuClick(nodes: List<Node>): Boolean
|
||||||
fun onDeleteMenuClick(nodes: List<NodeVersioned>): Boolean
|
fun onDeleteMenuClick(nodes: List<Node>): Boolean
|
||||||
fun onPasteMenuClick(pasteMode: PasteMode?, nodes: List<NodeVersioned>): Boolean
|
fun onPasteMenuClick(pasteMode: PasteMode?, nodes: List<Node>): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class PasteMode {
|
enum class PasteMode {
|
||||||
@@ -447,7 +466,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
private const val GROUP_KEY = "GROUP_KEY"
|
private const val GROUP_KEY = "GROUP_KEY"
|
||||||
private const val IS_SEARCH = "IS_SEARCH"
|
private const val IS_SEARCH = "IS_SEARCH"
|
||||||
|
|
||||||
fun newInstance(group: GroupVersioned?, readOnly: Boolean, isASearch: Boolean): ListNodesFragment {
|
fun newInstance(group: Group?, readOnly: Boolean, isASearch: Boolean): ListNodesFragment {
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
if (group != null) {
|
if (group != null) {
|
||||||
bundle.putParcelable(GROUP_KEY, group)
|
bundle.putParcelable(GROUP_KEY, group)
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity to select entry in database and populate it in Magikeyboard
|
||||||
|
*/
|
||||||
|
class MagikeyboardLauncherActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
|
Database.getInstance(),
|
||||||
|
null,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
GroupActivity.launchForEntrySelectionResult(this)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Pass extra to get entry
|
||||||
|
FileDatabaseSelectActivity.launchForEntrySelectionResult(this)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
finish()
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,118 +1,124 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
import android.app.backup.BackupManager
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.preference.PreferenceManager
|
import android.os.Looper
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.*
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
||||||
import android.widget.Button
|
import android.widget.*
|
||||||
import android.widget.CompoundButton
|
import androidx.activity.viewModels
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||||
import com.kunzisoft.keepass.activities.dialogs.FingerPrintExplanationDialog
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
|
import com.kunzisoft.keepass.biometric.AdvancedUnlockedManager
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDialogThread
|
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseDuplicateUuidException
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_KEY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.MASTER_PASSWORD_KEY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.MASTER_PASSWORD_KEY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.FileDatabaseInfo
|
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.utils.closeDatabase
|
||||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||||
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||||
import kotlinx.android.synthetic.main.activity_password.*
|
import kotlinx.android.synthetic.main.activity_password.*
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
class PasswordActivity : StylishActivity() {
|
open class PasswordActivity : SpecialModeActivity() {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
|
|
||||||
private var containerView: View? = null
|
|
||||||
private var filenameView: TextView? = null
|
private var filenameView: TextView? = null
|
||||||
private var passwordView: EditText? = null
|
private var passwordView: EditText? = null
|
||||||
private var keyFileView: EditText? = null
|
private var keyFileSelectionView: KeyFileSelectionView? = null
|
||||||
private var confirmButtonView: Button? = null
|
private var confirmButtonView: Button? = null
|
||||||
private var checkboxPasswordView: CompoundButton? = null
|
private var checkboxPasswordView: CompoundButton? = null
|
||||||
private var checkboxKeyFileView: CompoundButton? = null
|
private var checkboxKeyFileView: CompoundButton? = null
|
||||||
private var checkboxDefaultDatabaseView: CompoundButton? = null
|
|
||||||
private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null
|
private var advancedUnlockInfoView: AdvancedUnlockInfoView? = null
|
||||||
|
private var infoContainerView: ViewGroup? = null
|
||||||
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
|
private var enableButtonOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
|
||||||
|
|
||||||
|
private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||||
|
|
||||||
private var mDatabaseFileUri: Uri? = null
|
private var mDatabaseFileUri: Uri? = null
|
||||||
private var mDatabaseKeyFileUri: Uri? = null
|
private var mDatabaseKeyFileUri: Uri? = null
|
||||||
|
|
||||||
private var prefs: SharedPreferences? = null
|
|
||||||
|
|
||||||
private var mRememberKeyFile: Boolean = false
|
private var mRememberKeyFile: Boolean = false
|
||||||
private var mOpenFileHelper: OpenFileHelper? = null
|
private var mSelectFileHelper: SelectFileHelper? = null
|
||||||
|
|
||||||
|
private var mPermissionAsked = false
|
||||||
private var readOnly: Boolean = false
|
private var readOnly: Boolean = false
|
||||||
|
private var mForceReadOnly: Boolean = false
|
||||||
|
set(value) {
|
||||||
|
infoContainerView?.visibility = if (value) {
|
||||||
|
readOnly = true
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
private var progressDialogThread: ProgressDialogThread? = null
|
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||||
|
|
||||||
private var advancedUnlockedManager: AdvancedUnlockedManager? = null
|
private var advancedUnlockedManager: AdvancedUnlockedManager? = null
|
||||||
|
private var mAllowAutoOpenBiometricPrompt: Boolean = true
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
|
||||||
|
|
||||||
mRememberKeyFile = PreferencesUtil.rememberKeyFiles(this)
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_password)
|
setContentView(R.layout.activity_password)
|
||||||
|
|
||||||
toolbar = findViewById(R.id.toolbar)
|
toolbar = findViewById(R.id.toolbar)
|
||||||
@@ -121,21 +127,26 @@ class PasswordActivity : StylishActivity() {
|
|||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
containerView = findViewById(R.id.container)
|
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
||||||
confirmButtonView = findViewById(R.id.pass_ok)
|
|
||||||
filenameView = findViewById(R.id.filename)
|
filenameView = findViewById(R.id.filename)
|
||||||
passwordView = findViewById(R.id.password)
|
passwordView = findViewById(R.id.password)
|
||||||
keyFileView = findViewById(R.id.pass_keyfile)
|
keyFileSelectionView = findViewById(R.id.keyfile_selection)
|
||||||
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
||||||
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
||||||
checkboxDefaultDatabaseView = findViewById(R.id.default_database)
|
advancedUnlockInfoView = findViewById(R.id.biometric_info)
|
||||||
advancedUnlockInfoView = findViewById(R.id.fingerprint_info)
|
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||||
|
|
||||||
|
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
||||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
||||||
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||||
|
|
||||||
val browseView = findViewById<View>(R.id.open_database_button)
|
mSelectFileHelper = SelectFileHelper(this@PasswordActivity)
|
||||||
mOpenFileHelper = OpenFileHelper(this@PasswordActivity)
|
keyFileSelectionView?.apply {
|
||||||
browseView.setOnClickListener(mOpenFileHelper!!.openFileOnClickViewListener)
|
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
||||||
|
setOnClickListener(it)
|
||||||
|
setOnLongClickListener(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
||||||
passwordView?.addTextChangedListener(object : TextWatcher {
|
passwordView?.addTextChangedListener(object : TextWatcher {
|
||||||
@@ -148,52 +159,58 @@ class PasswordActivity : StylishActivity() {
|
|||||||
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 { _, _ ->
|
enableButtonOnCheckedChangeListener = CompoundButton.OnCheckedChangeListener { _, _ ->
|
||||||
enableOrNotTheConfirmationButton()
|
enableOrNotTheConfirmationButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
progressDialogThread = ProgressDialogThread(this) { actionTask, result ->
|
// If is a view intent
|
||||||
when (actionTask) {
|
getUriFromIntent(intent)
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
|
||||||
// Recheck fingerprint if error
|
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
}
|
||||||
if (PreferencesUtil.isBiometricUnlockEnable(this@PasswordActivity)) {
|
if (savedInstanceState?.containsKey(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT) == true) {
|
||||||
// Stay with the same mode and init it
|
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
|
||||||
advancedUnlockedManager?.initBiometricMode()
|
}
|
||||||
|
|
||||||
|
// Observe database file change
|
||||||
|
databaseFileViewModel.databaseFileLoaded.observe(this, Observer { databaseFile ->
|
||||||
|
// Force read only if the file does not exists
|
||||||
|
mForceReadOnly = databaseFile?.let {
|
||||||
|
!it.databaseFileExists
|
||||||
|
} ?: true
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
|
||||||
|
// Post init uri with KeyFile only if needed
|
||||||
|
val keyFileUri =
|
||||||
|
if (mRememberKeyFile
|
||||||
|
&& (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) {
|
||||||
|
databaseFile?.keyFileUri
|
||||||
|
} else {
|
||||||
|
mDatabaseKeyFileUri
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define title
|
||||||
|
filenameView?.text = databaseFile?.databaseAlias ?: ""
|
||||||
|
|
||||||
|
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
|
||||||
|
})
|
||||||
|
|
||||||
|
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
|
||||||
|
onActionFinish = { actionTask, result ->
|
||||||
|
when (actionTask) {
|
||||||
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
|
// Recheck biometric if error
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
if (PreferencesUtil.isBiometricUnlockEnable(this@PasswordActivity)) {
|
||||||
|
// Stay with the same mode and init it
|
||||||
|
advancedUnlockedManager?.initBiometricMode()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var databaseUri: Uri? = null
|
|
||||||
var masterPassword: String? = null
|
|
||||||
var keyFileUri: Uri? = null
|
|
||||||
var readOnly = true
|
|
||||||
var cipherEntity: CipherDatabaseEntity? = null
|
|
||||||
|
|
||||||
result.data?.let { resultData ->
|
|
||||||
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
|
||||||
masterPassword = resultData.getString(MASTER_PASSWORD_KEY)
|
|
||||||
keyFileUri = resultData.getParcelable(KEY_FILE_KEY)
|
|
||||||
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
|
||||||
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
|
|
||||||
}
|
|
||||||
|
|
||||||
databaseUri?.let { databaseFileUri ->
|
|
||||||
// Remove the password in view in all cases
|
|
||||||
removePassword()
|
|
||||||
|
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
|
mDatabaseKeyFileUri = null
|
||||||
|
clearCredentialsViews(true)
|
||||||
launchGroupActivity()
|
launchGroupActivity()
|
||||||
} else {
|
} else {
|
||||||
var resultError = ""
|
var resultError = ""
|
||||||
@@ -202,24 +219,43 @@ class PasswordActivity : StylishActivity() {
|
|||||||
|
|
||||||
if (resultException != null) {
|
if (resultException != null) {
|
||||||
resultError = resultException.getLocalizedMessage(resources)
|
resultError = resultException.getLocalizedMessage(resources)
|
||||||
if (resultException is LoadDatabaseDuplicateUuidException)
|
|
||||||
|
// Relaunch loading if we need to fix UUID
|
||||||
|
if (resultException is DuplicateUuidDatabaseException) {
|
||||||
showLoadDatabaseDuplicateUuidMessage {
|
showLoadDatabaseDuplicateUuidMessage {
|
||||||
showProgressDialogAndLoadDatabase(
|
|
||||||
databaseFileUri,
|
var databaseUri: Uri? = null
|
||||||
masterPassword,
|
var masterPassword: String? = null
|
||||||
keyFileUri,
|
var keyFileUri: Uri? = null
|
||||||
readOnly,
|
var readOnly = true
|
||||||
cipherEntity,
|
var cipherEntity: CipherDatabaseEntity? = null
|
||||||
true)
|
|
||||||
|
result.data?.let { resultData ->
|
||||||
|
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
||||||
|
masterPassword = resultData.getString(MASTER_PASSWORD_KEY)
|
||||||
|
keyFileUri = resultData.getParcelable(KEY_FILE_URI_KEY)
|
||||||
|
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
||||||
|
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
databaseUri?.let { databaseFileUri ->
|
||||||
|
showProgressDialogAndLoadDatabase(
|
||||||
|
databaseFileUri,
|
||||||
|
masterPassword,
|
||||||
|
keyFileUri,
|
||||||
|
readOnly,
|
||||||
|
cipherEntity,
|
||||||
|
true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show error message
|
||||||
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
||||||
resultError = "$resultError $resultMessage"
|
resultError = "$resultError $resultMessage"
|
||||||
}
|
}
|
||||||
|
Log.e(TAG, resultError)
|
||||||
Log.e(TAG, resultError, resultException)
|
|
||||||
|
|
||||||
Snackbar.make(activity_password_coordinator_layout,
|
Snackbar.make(activity_password_coordinator_layout,
|
||||||
resultError,
|
resultError,
|
||||||
Snackbar.LENGTH_LONG).asError().show()
|
Snackbar.LENGTH_LONG).asError().show()
|
||||||
@@ -230,19 +266,91 @@ class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getUriFromIntent(intent: Intent?) {
|
||||||
|
// If is a view intent
|
||||||
|
val action = intent?.action
|
||||||
|
if (action != null
|
||||||
|
&& action == VIEW_INTENT) {
|
||||||
|
mDatabaseFileUri = intent.data
|
||||||
|
mDatabaseKeyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
|
||||||
|
} else {
|
||||||
|
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
|
||||||
|
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
getUriFromIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
private fun launchGroupActivity() {
|
private fun launchGroupActivity() {
|
||||||
|
val searchInfo: SearchInfo? = intent.getParcelableExtra(KEY_SEARCH_INFO)
|
||||||
EntrySelectionHelper.doEntrySelectionAction(intent,
|
EntrySelectionHelper.doEntrySelectionAction(intent,
|
||||||
{
|
{
|
||||||
GroupActivity.launch(this@PasswordActivity, readOnly)
|
GroupActivity.launch(this@PasswordActivity,
|
||||||
|
true,
|
||||||
|
searchInfo,
|
||||||
|
readOnly)
|
||||||
|
// Finish activity if no search info
|
||||||
|
if (searchInfo != null) {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
GroupActivity.launchForKeyboardSelection(this@PasswordActivity, readOnly)
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
|
Database.getInstance(),
|
||||||
|
searchInfo,
|
||||||
|
{ items ->
|
||||||
|
// Response is build
|
||||||
|
if (items.size == 1) {
|
||||||
|
populateKeyboardAndMoveAppToBackground(this@PasswordActivity,
|
||||||
|
items[0],
|
||||||
|
intent)
|
||||||
|
} else {
|
||||||
|
// Select the one we want
|
||||||
|
GroupActivity.launchForEntrySelectionResult(this,
|
||||||
|
true,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Here no search info found, disable auto search
|
||||||
|
GroupActivity.launchForEntrySelectionResult(this@PasswordActivity,
|
||||||
|
false,
|
||||||
|
searchInfo,
|
||||||
|
readOnly)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Simply close if database not opened, normally not happened
|
||||||
|
}
|
||||||
|
)
|
||||||
// Do not keep history
|
// Do not keep history
|
||||||
finish()
|
finish()
|
||||||
},
|
},
|
||||||
{ assistStructure ->
|
{ assistStructure ->
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
GroupActivity.launchForAutofillResult(this@PasswordActivity, assistStructure, readOnly)
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
|
Database.getInstance(),
|
||||||
|
searchInfo,
|
||||||
|
{ items ->
|
||||||
|
// Response is build
|
||||||
|
AutofillHelper.buildResponse(this, items)
|
||||||
|
finish()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Here no search info found, disable auto search
|
||||||
|
GroupActivity.launchForAutofillResult(this@PasswordActivity,
|
||||||
|
assistStructure,
|
||||||
|
false,
|
||||||
|
searchInfo,
|
||||||
|
readOnly)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Simply close if database not opened, normally not happened
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -258,107 +366,54 @@ class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
if (Database.getInstance().loaded)
|
|
||||||
launchGroupActivity()
|
|
||||||
|
|
||||||
// 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()
|
super.onResume()
|
||||||
|
|
||||||
progressDialogThread?.registerProgressTask()
|
if (Database.getInstance().loaded) {
|
||||||
|
launchGroupActivity()
|
||||||
initUriFromIntent()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initUriFromIntent() {
|
|
||||||
|
|
||||||
val databaseUri: Uri?
|
|
||||||
val keyFileUri: Uri?
|
|
||||||
|
|
||||||
// If is a view intent
|
|
||||||
val action = intent.action
|
|
||||||
if (action != null
|
|
||||||
&& action == VIEW_INTENT) {
|
|
||||||
databaseUri = intent.data
|
|
||||||
keyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
|
|
||||||
} else {
|
} else {
|
||||||
databaseUri = intent.getParcelableExtra(KEY_FILENAME)
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||||
keyFileUri = intent.getParcelableExtra(KEY_KEYFILE)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post init uri with KeyFile if needed
|
// If the database isn't accessible make sure to clear the password field, if it
|
||||||
if (mRememberKeyFile && (keyFileUri == null || keyFileUri.toString().isEmpty())) {
|
// was saved in the instance state
|
||||||
// Retrieve KeyFile in a thread
|
if (Database.getInstance().loaded) {
|
||||||
databaseUri?.let { databaseUriNotNull ->
|
clearCredentialsViews()
|
||||||
FileDatabaseHistoryAction.getInstance(applicationContext)
|
|
||||||
.getKeyFileUriByDatabaseUri(databaseUriNotNull) {
|
|
||||||
onPostInitUri(databaseUri, it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
onPostInitUri(databaseUri, keyFileUri)
|
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||||
|
|
||||||
|
// Back to previous keyboard is setting activated
|
||||||
|
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this)) {
|
||||||
|
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow auto open prompt if lock become when UI visible
|
||||||
|
mAllowAutoOpenBiometricPrompt = if (LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true)
|
||||||
|
false
|
||||||
|
else
|
||||||
|
mAllowAutoOpenBiometricPrompt
|
||||||
|
|
||||||
|
mDatabaseFileUri?.let { databaseFileUri ->
|
||||||
|
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPermission()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onPostInitUri(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
||||||
mDatabaseFileUri = databaseFileUri
|
|
||||||
mDatabaseKeyFileUri = keyFileUri
|
|
||||||
|
|
||||||
// Define title
|
|
||||||
databaseFileUri?.let {
|
|
||||||
FileDatabaseInfo(this, it).retrieveDatabaseTitle { title ->
|
|
||||||
filenameView?.text = title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define Key File text
|
// Define Key File text
|
||||||
val keyUriString = keyFileUri?.toString() ?: ""
|
if (mRememberKeyFile) {
|
||||||
if (keyUriString.isNotEmpty() && mRememberKeyFile) { // Bug KeepassDX #18
|
populateKeyFileTextView(keyFileUri)
|
||||||
populateKeyFileTextView(keyUriString)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define listeners for default database checkbox and validate button
|
// Define listener for validate button
|
||||||
checkboxDefaultDatabaseView?.setOnCheckedChangeListener { _, isChecked ->
|
|
||||||
var newDefaultFileName: Uri? = null
|
|
||||||
if (isChecked) {
|
|
||||||
newDefaultFileName = databaseFileUri ?: newDefaultFileName
|
|
||||||
}
|
|
||||||
|
|
||||||
prefs?.edit()?.apply {
|
|
||||||
newDefaultFileName?.let {
|
|
||||||
putString(KEY_DEFAULT_DATABASE_PATH, newDefaultFileName.toString())
|
|
||||||
} ?: kotlin.run {
|
|
||||||
remove(KEY_DEFAULT_DATABASE_PATH)
|
|
||||||
}
|
|
||||||
apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
val backupManager = BackupManager(this@PasswordActivity)
|
|
||||||
backupManager.dataChanged()
|
|
||||||
}
|
|
||||||
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
|
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
|
||||||
|
|
||||||
// Retrieve settings for default database
|
|
||||||
val defaultFilename = prefs?.getString(KEY_DEFAULT_DATABASE_PATH, "")
|
|
||||||
if (databaseFileUri != null
|
|
||||||
&& databaseFileUri.path != null && databaseFileUri.path!!.isNotEmpty()
|
|
||||||
&& databaseFileUri == UriUtil.parse(defaultFilename)) {
|
|
||||||
checkboxDefaultDatabaseView?.isChecked = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// If Activity is launch with a password and want to open directly
|
// If Activity is launch with a password and want to open directly
|
||||||
val intent = intent
|
val intent = intent
|
||||||
val password = intent.getStringExtra(KEY_PASSWORD)
|
val password = intent.getStringExtra(KEY_PASSWORD)
|
||||||
|
// Consume the intent extra password
|
||||||
|
intent.removeExtra(KEY_PASSWORD)
|
||||||
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
|
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
|
||||||
if (password != null) {
|
if (password != null) {
|
||||||
populatePasswordTextView(password)
|
populatePasswordTextView(password)
|
||||||
@@ -366,15 +421,10 @@ class PasswordActivity : StylishActivity() {
|
|||||||
if (launchImmediately) {
|
if (launchImmediately) {
|
||||||
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
|
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
|
||||||
} else {
|
} else {
|
||||||
// Init FingerPrint elements
|
// Init Biometric elements
|
||||||
var fingerPrintInit = false
|
var biometricInitialize = false
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
if (PreferencesUtil.isBiometricUnlockEnable(this)) {
|
if (PreferencesUtil.isBiometricUnlockEnable(this)) {
|
||||||
|
|
||||||
advancedUnlockInfoView?.setOnClickListener {
|
|
||||||
FingerPrintExplanationDialog().show(supportFragmentManager, "fingerPrintExplanationDialog")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (advancedUnlockedManager == null && databaseFileUri != null) {
|
if (advancedUnlockedManager == null && databaseFileUri != null) {
|
||||||
advancedUnlockedManager = AdvancedUnlockedManager(this,
|
advancedUnlockedManager = AdvancedUnlockedManager(this,
|
||||||
databaseFileUri,
|
databaseFileUri,
|
||||||
@@ -396,18 +446,20 @@ class PasswordActivity : StylishActivity() {
|
|||||||
{ passwordDecrypted ->
|
{ passwordDecrypted ->
|
||||||
// Load the database if password is retrieve from biometric
|
// Load the database if password is retrieve from biometric
|
||||||
passwordDecrypted?.let {
|
passwordDecrypted?.let {
|
||||||
// Retrieve from fingerprint
|
// Retrieve from biometric
|
||||||
verifyKeyFileCheckboxAndLoadDatabase(it)
|
verifyKeyFileCheckboxAndLoadDatabase(it)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
advancedUnlockedManager?.initBiometric()
|
advancedUnlockedManager?.isBiometricPromptAutoOpenEnable = mAllowAutoOpenBiometricPrompt
|
||||||
fingerPrintInit = true
|
advancedUnlockedManager?.checkBiometricAvailability()
|
||||||
|
biometricInitialize = true
|
||||||
} else {
|
} else {
|
||||||
advancedUnlockedManager?.destroy()
|
advancedUnlockedManager?.destroy()
|
||||||
|
advancedUnlockInfoView?.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!fingerPrintInit) {
|
if (!biometricInitialize) {
|
||||||
checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
checkboxPasswordView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
||||||
}
|
}
|
||||||
checkboxKeyFileView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
checkboxKeyFileView?.setOnCheckedChangeListener(enableButtonOnCheckedChangeListener)
|
||||||
@@ -428,10 +480,9 @@ class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setEmptyViews() {
|
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
|
||||||
populatePasswordTextView(null)
|
populatePasswordTextView(null)
|
||||||
// Bug KeepassDX #18
|
if (clearKeyFile) {
|
||||||
if (!mRememberKeyFile) {
|
|
||||||
populateKeyFileTextView(null)
|
populateKeyFileTextView(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -448,38 +499,46 @@ class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun populateKeyFileTextView(text: String?) {
|
private fun populateKeyFileTextView(uri: Uri?) {
|
||||||
if (text == null || text.isEmpty()) {
|
if (uri == null || uri.toString().isEmpty()) {
|
||||||
keyFileView?.setText("")
|
keyFileSelectionView?.uri = null
|
||||||
if (checkboxKeyFileView?.isChecked == true)
|
if (checkboxKeyFileView?.isChecked == true)
|
||||||
checkboxKeyFileView?.isChecked = false
|
checkboxKeyFileView?.isChecked = false
|
||||||
} else {
|
} else {
|
||||||
keyFileView?.setText(text)
|
keyFileSelectionView?.uri = uri
|
||||||
if (checkboxKeyFileView?.isChecked != true)
|
if (checkboxKeyFileView?.isChecked != true)
|
||||||
checkboxKeyFileView?.isChecked = true
|
checkboxKeyFileView?.isChecked = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
|
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
advancedUnlockedManager?.pause()
|
advancedUnlockedManager?.destroy()
|
||||||
|
advancedUnlockedManager = null
|
||||||
}
|
}
|
||||||
|
|
||||||
progressDialogThread?.unregisterProgressTask()
|
// Reinit locking activity UI variable
|
||||||
|
LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
||||||
|
mAllowAutoOpenBiometricPrompt = true
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
|
||||||
advancedUnlockedManager?.destroy()
|
mDatabaseKeyFileUri?.let {
|
||||||
|
outState.putString(KEY_KEYFILE, it.toString())
|
||||||
}
|
}
|
||||||
super.onDestroy()
|
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
||||||
|
outState.putBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT, false)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||||
val password: String? = passwordView?.text?.toString()
|
val password: String? = passwordView?.text?.toString()
|
||||||
val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString())
|
val keyFile: Uri? = keyFileSelectionView?.uri
|
||||||
verifyCheckboxesAndLoadDatabase(password, keyFile, cipherDatabaseEntity)
|
verifyCheckboxesAndLoadDatabase(password, keyFile, cipherDatabaseEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,7 +551,7 @@ class PasswordActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) {
|
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) {
|
||||||
val keyFile: Uri? = UriUtil.parse(keyFileView?.text?.toString())
|
val keyFile: Uri? = keyFileSelectionView?.uri
|
||||||
verifyKeyFileCheckbox(keyFile)
|
verifyKeyFileCheckbox(keyFile)
|
||||||
loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri)
|
loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri)
|
||||||
}
|
}
|
||||||
@@ -501,18 +560,13 @@ class PasswordActivity : StylishActivity() {
|
|||||||
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
|
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removePassword() {
|
|
||||||
passwordView?.setText("")
|
|
||||||
checkboxPasswordView?.isChecked = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadDatabase(databaseFileUri: Uri?,
|
private fun loadDatabase(databaseFileUri: Uri?,
|
||||||
password: String?,
|
password: String?,
|
||||||
keyFileUri: Uri?,
|
keyFileUri: Uri?,
|
||||||
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
||||||
|
|
||||||
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
|
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
|
||||||
removePassword()
|
clearCredentialsViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
databaseFileUri?.let { databaseUri ->
|
databaseFileUri?.let { databaseUri ->
|
||||||
@@ -533,7 +587,7 @@ class PasswordActivity : StylishActivity() {
|
|||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
cipherDatabaseEntity: CipherDatabaseEntity?,
|
cipherDatabaseEntity: CipherDatabaseEntity?,
|
||||||
fixDuplicateUUID: Boolean) {
|
fixDuplicateUUID: Boolean) {
|
||||||
progressDialogThread?.startDatabaseLoad(
|
mProgressDatabaseTaskProvider?.startDatabaseLoad(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
password,
|
password,
|
||||||
keyFile,
|
keyFile,
|
||||||
@@ -549,39 +603,77 @@ class PasswordActivity : StylishActivity() {
|
|||||||
}.show(supportFragmentManager, "duplicateUUIDDialog")
|
}.show(supportFragmentManager, "duplicateUUIDDialog")
|
||||||
}
|
}
|
||||||
|
|
||||||
// To fix multiple view education
|
|
||||||
private var performedEductionInProgress = false
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
val inflater = menuInflater
|
val inflater = menuInflater
|
||||||
// Read menu
|
// Read menu
|
||||||
inflater.inflate(R.menu.open_file, menu)
|
inflater.inflate(R.menu.open_file, menu)
|
||||||
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
|
if (mSelectionMode || mForceReadOnly) {
|
||||||
|
menu.removeItem(R.id.menu_open_file_read_mode_key)
|
||||||
|
} else {
|
||||||
|
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
|
||||||
|
}
|
||||||
|
|
||||||
MenuUtil.defaultMenuInflater(inflater, menu)
|
if (!mSelectionMode) {
|
||||||
|
MenuUtil.defaultMenuInflater(inflater, menu)
|
||||||
|
}
|
||||||
|
|
||||||
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
// Fingerprint menu
|
// biometric menu
|
||||||
advancedUnlockedManager?.inflateOptionsMenu(inflater, menu)
|
advancedUnlockedManager?.inflateOptionsMenu(inflater, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
|
|
||||||
if (!performedEductionInProgress) {
|
launchEducation(menu)
|
||||||
performedEductionInProgress = true
|
|
||||||
// Show education views
|
|
||||||
Handler().post { performedNextEducation(PasswordActivityEducation(this), menu) }
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check permission
|
||||||
|
private fun checkPermission() {
|
||||||
|
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
val permissions = arrayOf(writePermission)
|
||||||
|
if (Build.VERSION.SDK_INT >= 23
|
||||||
|
&& !readOnly
|
||||||
|
&& !mPermissionAsked) {
|
||||||
|
mPermissionAsked = true
|
||||||
|
// Check self permission to show or not the dialog
|
||||||
|
if (toolbar != null
|
||||||
|
&& ActivityCompat.checkSelfPermission(this, writePermission) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
ActivityCompat.requestPermissions(this, permissions, WRITE_EXTERNAL_STORAGE_REQUEST)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
|
||||||
|
when (requestCode) {
|
||||||
|
WRITE_EXTERNAL_STORAGE_REQUEST -> {
|
||||||
|
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
if (ActivityCompat.shouldShowRequestPermissionRationale(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE))
|
||||||
|
Toast.makeText(this, R.string.read_only_warning, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// To fix multiple view education
|
||||||
|
private var performedEductionInProgress = false
|
||||||
|
private fun launchEducation(menu: Menu) {
|
||||||
|
if (!performedEductionInProgress) {
|
||||||
|
performedEductionInProgress = true
|
||||||
|
// Show education views
|
||||||
|
Handler(Looper.getMainLooper()).post { performedNextEducation(PasswordActivityEducation(this), menu) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
|
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
|
||||||
menu: Menu) {
|
menu: Menu) {
|
||||||
val educationContainerView = containerView
|
val educationToolbar = toolbar
|
||||||
val unlockEducationPerformed = educationContainerView != null
|
val unlockEducationPerformed = educationToolbar != null
|
||||||
&& passwordActivityEducation.checkAndPerformedUnlockEducation(
|
&& passwordActivityEducation.checkAndPerformedUnlockEducation(
|
||||||
educationContainerView,
|
educationToolbar,
|
||||||
{
|
{
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
},
|
},
|
||||||
@@ -589,7 +681,6 @@ class PasswordActivity : StylishActivity() {
|
|||||||
performedNextEducation(passwordActivityEducation, menu)
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
})
|
})
|
||||||
if (!unlockEducationPerformed) {
|
if (!unlockEducationPerformed) {
|
||||||
val educationToolbar = toolbar
|
|
||||||
val readOnlyEducationPerformed =
|
val readOnlyEducationPerformed =
|
||||||
educationToolbar?.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
|
educationToolbar?.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
|
||||||
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
|
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
|
||||||
@@ -603,15 +694,18 @@ class PasswordActivity : StylishActivity() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!readOnlyEducationPerformed) {
|
if (!readOnlyEducationPerformed) {
|
||||||
|
|
||||||
val biometricCanAuthenticate = BiometricManager.from(this).canAuthenticate()
|
val biometricCanAuthenticate = BiometricManager.from(this).canAuthenticate()
|
||||||
// fingerprintEducationPerformed
|
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||||
&& PreferencesUtil.isBiometricUnlockEnable(applicationContext)
|
&& PreferencesUtil.isBiometricUnlockEnable(applicationContext)
|
||||||
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED || biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
||||||
&& advancedUnlockInfoView != null && advancedUnlockInfoView?.unlockIconImageView != null
|
&& advancedUnlockInfoView != null && advancedUnlockInfoView?.unlockIconImageView != null
|
||||||
&& passwordActivityEducation.checkAndPerformedFingerprintEducation(advancedUnlockInfoView?.unlockIconImageView!!)
|
&& passwordActivityEducation.checkAndPerformedBiometricEducation(advancedUnlockInfoView?.unlockIconImageView!!,
|
||||||
|
{
|
||||||
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
performedNextEducation(passwordActivityEducation, menu)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -634,7 +728,7 @@ class PasswordActivity : StylishActivity() {
|
|||||||
readOnly = !readOnly
|
readOnly = !readOnly
|
||||||
changeOpenFileReadIcon(item)
|
changeOpenFileReadIcon(item)
|
||||||
}
|
}
|
||||||
R.id.menu_fingerprint_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
R.id.menu_biometric_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
advancedUnlockedManager?.deleteEntryKey()
|
advancedUnlockedManager?.deleteEntryKey()
|
||||||
}
|
}
|
||||||
else -> return MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
else -> return MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
||||||
@@ -649,26 +743,32 @@ class PasswordActivity : StylishActivity() {
|
|||||||
data: Intent?) {
|
data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
|
mAllowAutoOpenBiometricPrompt = false
|
||||||
|
|
||||||
// To get entry in result
|
// To get entry in result
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
var keyFileResult = false
|
var keyFileResult = false
|
||||||
mOpenFileHelper?.let {
|
mSelectFileHelper?.let {
|
||||||
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
||||||
) { uri ->
|
) { uri ->
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
populateKeyFileTextView(uri.toString())
|
mDatabaseKeyFileUri = uri
|
||||||
|
populateKeyFileTextView(uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!keyFileResult) {
|
if (!keyFileResult) {
|
||||||
// this block if not a key file response
|
// this block if not a key file response
|
||||||
when (resultCode) {
|
when (resultCode) {
|
||||||
LockingActivity.RESULT_EXIT_LOCK, Activity.RESULT_CANCELED -> {
|
LockingActivity.RESULT_EXIT_LOCK -> {
|
||||||
setEmptyViews()
|
clearCredentialsViews()
|
||||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this))
|
||||||
|
}
|
||||||
|
Activity.RESULT_CANCELED -> {
|
||||||
|
clearCredentialsViews()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -678,14 +778,16 @@ class PasswordActivity : StylishActivity() {
|
|||||||
|
|
||||||
private val TAG = PasswordActivity::class.java.name
|
private val TAG = PasswordActivity::class.java.name
|
||||||
|
|
||||||
const val KEY_DEFAULT_DATABASE_PATH = "KEY_DEFAULT_DATABASE_PATH"
|
|
||||||
|
|
||||||
private const val KEY_FILENAME = "fileName"
|
private const val KEY_FILENAME = "fileName"
|
||||||
private const val KEY_KEYFILE = "keyFile"
|
private const val KEY_KEYFILE = "keyFile"
|
||||||
private const val VIEW_INTENT = "android.intent.action.VIEW"
|
private const val VIEW_INTENT = "android.intent.action.VIEW"
|
||||||
|
|
||||||
private const val KEY_PASSWORD = "password"
|
private const val KEY_PASSWORD = "password"
|
||||||
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||||
|
private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED"
|
||||||
|
private const val WRITE_EXTERNAL_STORAGE_REQUEST = 647
|
||||||
|
|
||||||
|
private const val ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT = "ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT"
|
||||||
|
|
||||||
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
|
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
|
||||||
intentBuildLauncher: (Intent) -> Unit) {
|
intentBuildLauncher: (Intent) -> Unit) {
|
||||||
@@ -706,8 +808,12 @@ class PasswordActivity : StylishActivity() {
|
|||||||
fun launch(
|
fun launch(
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?) {
|
keyFile: Uri?,
|
||||||
|
searchInfo: SearchInfo?) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
|
searchInfo?.let {
|
||||||
|
intent.putExtra(KEY_SEARCH_INFO, it)
|
||||||
|
}
|
||||||
activity.startActivity(intent)
|
activity.startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -722,9 +828,13 @@ class PasswordActivity : StylishActivity() {
|
|||||||
fun launchForKeyboardResult(
|
fun launchForKeyboardResult(
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?) {
|
keyFile: Uri?,
|
||||||
|
searchInfo: SearchInfo?) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
EntrySelectionHelper.startActivityForEntrySelection(activity, intent)
|
EntrySelectionHelper.startActivityForEntrySelectionResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
searchInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -740,16 +850,18 @@ class PasswordActivity : StylishActivity() {
|
|||||||
activity: Activity,
|
activity: Activity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
assistStructure: AssistStructure?) {
|
assistStructure: AssistStructure?,
|
||||||
|
searchInfo: SearchInfo?) {
|
||||||
if (assistStructure != null) {
|
if (assistStructure != null) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
AutofillHelper.startActivityForAutofillResult(
|
AutofillHelper.startActivityForAutofillResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
assistStructure)
|
assistStructure,
|
||||||
|
searchInfo)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
launch(activity, databaseFile, keyFile)
|
launch(activity, databaseFile, keyFile, searchInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
@@ -25,17 +25,20 @@ import android.content.DialogInterface
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
|
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
|
|
||||||
class AssignMasterKeyDialogFragment : DialogFragment() {
|
class AssignMasterKeyDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
@@ -51,13 +54,16 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
private var passwordRepeatTextInputLayout: TextInputLayout? = null
|
private var passwordRepeatTextInputLayout: TextInputLayout? = null
|
||||||
private var passwordRepeatView: TextView? = null
|
private var passwordRepeatView: TextView? = null
|
||||||
|
|
||||||
private var keyFileTextInputLayout: TextInputLayout? = null
|
|
||||||
private var keyFileCheckBox: CompoundButton? = null
|
private var keyFileCheckBox: CompoundButton? = null
|
||||||
private var keyFileView: TextView? = null
|
private var keyFileSelectionView: KeyFileSelectionView? = null
|
||||||
|
|
||||||
private var mListener: AssignPasswordDialogListener? = null
|
private var mListener: AssignPasswordDialogListener? = null
|
||||||
|
|
||||||
private var mOpenFileHelper: OpenFileHelper? = null
|
private var mSelectFileHelper: SelectFileHelper? = null
|
||||||
|
|
||||||
|
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
|
||||||
|
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
||||||
|
private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null
|
||||||
|
|
||||||
private val passwordTextWatcher = object : TextWatcher {
|
private val passwordTextWatcher = object : TextWatcher {
|
||||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||||
@@ -69,16 +75,6 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val keyFileTextWatcher = object : TextWatcher {
|
|
||||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
|
||||||
|
|
||||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
|
||||||
|
|
||||||
override fun afterTextChanged(editable: Editable) {
|
|
||||||
keyFileCheckBox?.isChecked = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AssignPasswordDialogListener {
|
interface AssignPasswordDialogListener {
|
||||||
fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?,
|
fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?,
|
||||||
keyFileChecked: Boolean, keyFile: Uri?)
|
keyFileChecked: Boolean, keyFile: Uri?)
|
||||||
@@ -96,6 +92,17 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
mEmptyPasswordConfirmationDialog?.dismiss()
|
||||||
|
mEmptyPasswordConfirmationDialog = null
|
||||||
|
mNoKeyConfirmationDialog?.dismiss()
|
||||||
|
mNoKeyConfirmationDialog = null
|
||||||
|
mEmptyKeyFileConfirmationDialog?.dismiss()
|
||||||
|
mEmptyKeyFileConfirmationDialog = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
|
|
||||||
@@ -110,24 +117,29 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
rootView = inflater.inflate(R.layout.fragment_set_password, null)
|
rootView = inflater.inflate(R.layout.fragment_set_password, null)
|
||||||
builder.setView(rootView)
|
builder.setView(rootView)
|
||||||
.setTitle(R.string.assign_master_key)
|
|
||||||
// Add action buttons
|
// Add action buttons
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
|
||||||
|
val credentialsInfo: ImageView? = rootView?.findViewById(R.id.credentials_information)
|
||||||
|
credentialsInfo?.setOnClickListener {
|
||||||
|
UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
|
||||||
|
}
|
||||||
|
|
||||||
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
||||||
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
|
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
|
||||||
passwordView = rootView?.findViewById(R.id.pass_password)
|
passwordView = rootView?.findViewById(R.id.pass_password)
|
||||||
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
|
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
|
||||||
passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password)
|
passwordRepeatView = rootView?.findViewById(R.id.pass_conf_password)
|
||||||
|
|
||||||
keyFileTextInputLayout = rootView?.findViewById(R.id.keyfile_input_layout)
|
|
||||||
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
||||||
keyFileView = rootView?.findViewById(R.id.pass_keyfile)
|
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
||||||
|
|
||||||
mOpenFileHelper = OpenFileHelper(this)
|
mSelectFileHelper = SelectFileHelper(this)
|
||||||
rootView?.findViewById<View>(R.id.open_database_button)?.setOnClickListener { view ->
|
keyFileSelectionView?.apply {
|
||||||
mOpenFileHelper?.openFileOnClickViewListener?.onClick(view) }
|
setOnClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
||||||
|
setOnLongClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
||||||
|
}
|
||||||
|
|
||||||
val dialog = builder.create()
|
val dialog = builder.create()
|
||||||
|
|
||||||
@@ -139,7 +151,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
mMasterPassword = ""
|
mMasterPassword = ""
|
||||||
mKeyFile = null
|
mKeyFile = null
|
||||||
|
|
||||||
var error = verifyPassword() || verifyFile()
|
var error = verifyPassword() || verifyKeyFile()
|
||||||
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
|
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
|
||||||
error = true
|
error = true
|
||||||
if (allowNoMasterKey)
|
if (allowNoMasterKey)
|
||||||
@@ -176,14 +188,12 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
// To check checkboxes if a text is present
|
// To check checkboxes if a text is present
|
||||||
passwordView?.addTextChangedListener(passwordTextWatcher)
|
passwordView?.addTextChangedListener(passwordTextWatcher)
|
||||||
keyFileView?.addTextChangedListener(keyFileTextWatcher)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
passwordView?.removeTextChangedListener(passwordTextWatcher)
|
passwordView?.removeTextChangedListener(passwordTextWatcher)
|
||||||
keyFileView?.removeTextChangedListener(keyFileTextWatcher)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyPassword(): Boolean {
|
private fun verifyPassword(): Boolean {
|
||||||
@@ -211,16 +221,16 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
return error
|
return error
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyFile(): Boolean {
|
private fun verifyKeyFile(): Boolean {
|
||||||
var error = false
|
var error = false
|
||||||
if (keyFileCheckBox != null
|
if (keyFileCheckBox != null
|
||||||
&& keyFileCheckBox!!.isChecked) {
|
&& keyFileCheckBox!!.isChecked) {
|
||||||
|
|
||||||
UriUtil.parse(keyFileView?.text?.toString())?.let { uri ->
|
keyFileSelectionView?.uri?.let { uri ->
|
||||||
mKeyFile = uri
|
mKeyFile = uri
|
||||||
} ?: run {
|
} ?: run {
|
||||||
error = true
|
error = true
|
||||||
keyFileTextInputLayout?.error = getString(R.string.error_nokeyfile)
|
keyFileSelectionView?.error = getString(R.string.error_nokeyfile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return error
|
return error
|
||||||
@@ -231,7 +241,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
val builder = AlertDialog.Builder(it)
|
val builder = AlertDialog.Builder(it)
|
||||||
builder.setMessage(R.string.warning_empty_password)
|
builder.setMessage(R.string.warning_empty_password)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
if (!verifyFile()) {
|
if (!verifyKeyFile()) {
|
||||||
mListener?.onAssignKeyDialogPositiveClick(
|
mListener?.onAssignKeyDialogPositiveClick(
|
||||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
passwordCheckBox!!.isChecked, mMasterPassword,
|
||||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
keyFileCheckBox!!.isChecked, mKeyFile)
|
||||||
@@ -239,7 +249,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
builder.create().show()
|
mEmptyPasswordConfirmationDialog = builder.create()
|
||||||
|
mEmptyPasswordConfirmationDialog?.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,19 +265,44 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
this@AssignMasterKeyDialogFragment.dismiss()
|
this@AssignMasterKeyDialogFragment.dismiss()
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
builder.create().show()
|
mNoKeyConfirmationDialog = builder.create()
|
||||||
|
mNoKeyConfirmationDialog?.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showEmptyKeyFileConfirmationDialog() {
|
||||||
|
activity?.let {
|
||||||
|
val builder = AlertDialog.Builder(it)
|
||||||
|
builder.setMessage(SpannableStringBuilder().apply {
|
||||||
|
append(getString(R.string.warning_empty_keyfile))
|
||||||
|
append("\n\n")
|
||||||
|
append(getString(R.string.warning_empty_keyfile_explanation))
|
||||||
|
append("\n\n")
|
||||||
|
append(getString(R.string.warning_sure_add_file))
|
||||||
|
})
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
keyFileCheckBox?.isChecked = false
|
||||||
|
keyFileSelectionView?.uri = null
|
||||||
|
}
|
||||||
|
mEmptyKeyFileConfirmationDialog = builder.create()
|
||||||
|
mEmptyKeyFileConfirmationDialog?.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
|
||||||
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
|
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||||
) { uri ->
|
|
||||||
uri?.let { pathUri ->
|
uri?.let { pathUri ->
|
||||||
keyFileCheckBox?.isChecked = true
|
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
||||||
keyFileView?.text = pathUri.toString()
|
keyFileSelectionView?.error = null
|
||||||
|
keyFileCheckBox?.isChecked = true
|
||||||
|
keyFileSelectionView?.uri = pathUri
|
||||||
|
if (lengthFile <= 0L) {
|
||||||
|
showEmptyKeyFileConfirmationDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.DatePickerDialog
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
|
||||||
|
class DatePickerFragment : DialogFragment() {
|
||||||
|
|
||||||
|
private var mDefaultYear: Int = 2000
|
||||||
|
private var mDefaultMonth: Int = 1
|
||||||
|
private var mDefaultDay: Int = 1
|
||||||
|
|
||||||
|
private var mListener: DatePickerDialog.OnDateSetListener? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
try {
|
||||||
|
mListener = context as DatePickerDialog.OnDateSetListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
// Create a new instance of DatePickerDialog and return it
|
||||||
|
return context?.let {
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(DEFAULT_YEAR_BUNDLE_KEY))
|
||||||
|
mDefaultYear = getInt(DEFAULT_YEAR_BUNDLE_KEY)
|
||||||
|
if (containsKey(DEFAULT_MONTH_BUNDLE_KEY))
|
||||||
|
mDefaultMonth = getInt(DEFAULT_MONTH_BUNDLE_KEY)
|
||||||
|
if (containsKey(DEFAULT_DAY_BUNDLE_KEY))
|
||||||
|
mDefaultDay = getInt(DEFAULT_DAY_BUNDLE_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
DatePickerDialog(it, mListener, mDefaultYear, mDefaultMonth, mDefaultDay)
|
||||||
|
} ?: super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val DEFAULT_YEAR_BUNDLE_KEY = "DEFAULT_YEAR_BUNDLE_KEY"
|
||||||
|
private const val DEFAULT_MONTH_BUNDLE_KEY = "DEFAULT_MONTH_BUNDLE_KEY"
|
||||||
|
private const val DEFAULT_DAY_BUNDLE_KEY = "DEFAULT_DAY_BUNDLE_KEY"
|
||||||
|
|
||||||
|
fun getInstance(defaultYear: Int,
|
||||||
|
defaultMonth: Int,
|
||||||
|
defaultDay: Int): DatePickerFragment {
|
||||||
|
return DatePickerFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putInt(DEFAULT_YEAR_BUNDLE_KEY, defaultYear)
|
||||||
|
putInt(DEFAULT_MONTH_BUNDLE_KEY, defaultMonth)
|
||||||
|
putInt(DEFAULT_DAY_BUNDLE_KEY, defaultDay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
||||||
|
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
||||||
|
|
||||||
|
class DeleteNodesDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
|
private var mNodesToDelete: List<Node> = ArrayList()
|
||||||
|
private var mListener: DeleteNodeListener? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
try {
|
||||||
|
mListener = context as DeleteNodeListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + DeleteNodeListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY)
|
||||||
|
&& containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) {
|
||||||
|
mNodesToDelete = getListNodesFromBundle(Database.getInstance(), this)
|
||||||
|
}
|
||||||
|
} ?: savedInstanceState?.apply {
|
||||||
|
if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY)
|
||||||
|
&& containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) {
|
||||||
|
mNodesToDelete = getListNodesFromBundle(Database.getInstance(), savedInstanceState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activity?.let { activity ->
|
||||||
|
// Use the Builder class for convenient dialog construction
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|
||||||
|
builder.setMessage(getString(R.string.warning_permanently_delete_nodes))
|
||||||
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
mListener?.permanentlyDeleteNodes(mNodesToDelete)
|
||||||
|
}
|
||||||
|
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||||
|
// Create the AlertDialog object and return it
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putAll(getBundleFromListNodes(mNodesToDelete))
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteNodeListener {
|
||||||
|
fun permanentlyDeleteNodes(nodes: List<Node>)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getInstance(nodesToDelete: List<Node>): DeleteNodesDialogFragment {
|
||||||
|
return DeleteNodesDialogFragment().apply {
|
||||||
|
arguments = getBundleFromListNodes(nodesToDelete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
|
import com.kunzisoft.keepass.model.Field
|
||||||
|
|
||||||
|
|
||||||
|
class EntryCustomFieldDialogFragment: DialogFragment() {
|
||||||
|
|
||||||
|
private var oldField: Field? = null
|
||||||
|
|
||||||
|
private var entryCustomFieldListener: EntryCustomFieldListener? = null
|
||||||
|
|
||||||
|
private var customFieldLabelContainer: TextInputLayout? = null
|
||||||
|
private var customFieldLabel: TextView? = null
|
||||||
|
private var customFieldDeleteButton: ImageView? = null
|
||||||
|
private var customFieldProtectionButton: CompoundButton? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
try {
|
||||||
|
entryCustomFieldListener = context as EntryCustomFieldListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
// The activity doesn't implement the interface, throw exception
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + EntryCustomFieldListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
entryCustomFieldListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
val root = activity.layoutInflater.inflate(R.layout.fragment_entry_new_field, null)
|
||||||
|
customFieldLabelContainer = root?.findViewById(R.id.entry_custom_field_label_container)
|
||||||
|
customFieldLabel = root?.findViewById(R.id.entry_custom_field_label)
|
||||||
|
customFieldDeleteButton = root?.findViewById(R.id.entry_custom_field_delete)
|
||||||
|
customFieldProtectionButton = root?.findViewById(R.id.entry_custom_field_protection)
|
||||||
|
|
||||||
|
oldField = arguments?.getParcelable(KEY_FIELD)
|
||||||
|
oldField?.let { oldCustomField ->
|
||||||
|
customFieldLabel?.text = oldCustomField.name
|
||||||
|
customFieldProtectionButton?.isChecked = oldCustomField.protectedValue.isProtected
|
||||||
|
|
||||||
|
customFieldDeleteButton?.visibility = View.VISIBLE
|
||||||
|
customFieldDeleteButton?.setOnClickListener {
|
||||||
|
entryCustomFieldListener?.onDeleteCustomFieldApproved(oldCustomField)
|
||||||
|
(dialog as AlertDialog?)?.dismiss()
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
customFieldDeleteButton?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
builder.setView(root)
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
val dialogCreated = builder.create()
|
||||||
|
|
||||||
|
customFieldLabel?.requestFocus()
|
||||||
|
customFieldLabel?.imeOptions = EditorInfo.IME_ACTION_DONE
|
||||||
|
customFieldLabel?.setOnEditorActionListener { _, actionId, _ ->
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||||
|
approveIfValid()
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogCreated.window?.setSoftInputMode(SOFT_INPUT_STATE_VISIBLE)
|
||||||
|
return dialogCreated
|
||||||
|
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// To prevent auto dismiss
|
||||||
|
val d = dialog as AlertDialog?
|
||||||
|
if (d != null) {
|
||||||
|
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
|
||||||
|
positiveButton.setOnClickListener {
|
||||||
|
approveIfValid()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun approveIfValid() {
|
||||||
|
if (isValid()) {
|
||||||
|
oldField?.let {
|
||||||
|
// New property with old value
|
||||||
|
entryCustomFieldListener?.onEditCustomFieldApproved(it,
|
||||||
|
Field(customFieldLabel?.text?.toString() ?: "",
|
||||||
|
ProtectedString(customFieldProtectionButton?.isChecked == true,
|
||||||
|
it.protectedValue.stringValue))
|
||||||
|
)
|
||||||
|
} ?: run {
|
||||||
|
entryCustomFieldListener?.onNewCustomFieldApproved(
|
||||||
|
Field(customFieldLabel?.text?.toString() ?: "",
|
||||||
|
ProtectedString(customFieldProtectionButton?.isChecked == true))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
(dialog as AlertDialog?)?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isValid(): Boolean {
|
||||||
|
return if (customFieldLabel?.text?.toString()?.isNotEmpty() != true) {
|
||||||
|
setError(R.string.error_string_key)
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
setError(null)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setError(@StringRes errorId: Int?) {
|
||||||
|
customFieldLabelContainer?.error = if (errorId == null) null else {
|
||||||
|
requireContext().getString(errorId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntryCustomFieldListener {
|
||||||
|
fun onNewCustomFieldApproved(newField: Field)
|
||||||
|
fun onEditCustomFieldApproved(oldField: Field, newField: Field)
|
||||||
|
fun onDeleteCustomFieldApproved(oldField: Field)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val KEY_FIELD = "KEY_FIELD"
|
||||||
|
|
||||||
|
fun getInstance(): EntryCustomFieldDialogFragment {
|
||||||
|
return EntryCustomFieldDialogFragment()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getInstance(field: Field): EntryCustomFieldDialogFragment {
|
||||||
|
return EntryCustomFieldDialogFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putParcelable(KEY_FIELD, field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
@@ -28,7 +28,7 @@ import android.widget.TextView
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
class BrowserDialogFragment : DialogFragment() {
|
class FileManagerDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
@@ -41,15 +41,8 @@ class BrowserDialogFragment : DialogFragment() {
|
|||||||
val textDescription = root.findViewById<TextView>(R.id.file_manager_install_description)
|
val textDescription = root.findViewById<TextView>(R.id.file_manager_install_description)
|
||||||
textDescription.text = getString(R.string.file_manager_install_description)
|
textDescription.text = getString(R.string.file_manager_install_description)
|
||||||
|
|
||||||
val market = root.findViewById<Button>(R.id.file_manager_install_play_store)
|
root.findViewById<Button>(R.id.file_manager_button).setOnClickListener {
|
||||||
market.setOnClickListener {
|
UriUtil.gotoUrl(requireContext(), R.string.file_manager_explanation_url)
|
||||||
UriUtil.gotoUrl(context!!, R.string.file_manager_play_store)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
val web = root.findViewById<Button>(R.id.file_manager_install_f_droid)
|
|
||||||
web.setOnClickListener {
|
|
||||||
UriUtil.gotoUrl(context!!, R.string.file_manager_f_droid)
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Dialog to confirm big file to upload
|
||||||
|
*/
|
||||||
|
class FileTooBigDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
|
private var mActionChooseListener: ActionChooseListener? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
// Verify that the host activity implements the callback interface
|
||||||
|
try {
|
||||||
|
mActionChooseListener = context as ActionChooseListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + ActionChooseListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mActionChooseListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
// Use the Builder class for convenient dialog construction
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
builder.setMessage(SpannableStringBuilder().apply {
|
||||||
|
append(getString(R.string.warning_file_too_big))
|
||||||
|
append("\n\n")
|
||||||
|
append(getString(R.string.warning_sure_add_file))
|
||||||
|
})
|
||||||
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
mActionChooseListener?.onValidateUploadFileTooBig(
|
||||||
|
arguments?.getParcelable(KEY_FILE_URI),
|
||||||
|
arguments?.getString(KEY_FILE_NAME))
|
||||||
|
}
|
||||||
|
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
// Create the AlertDialog object and return it
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionChooseListener {
|
||||||
|
fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MAX_WARNING_BINARY_FILE = 5242880
|
||||||
|
|
||||||
|
private const val KEY_FILE_URI = "KEY_FILE_URI"
|
||||||
|
private const val KEY_FILE_NAME = "KEY_FILE_NAME"
|
||||||
|
|
||||||
|
fun build(attachmentToUploadUri: Uri,
|
||||||
|
fileName: String): FileTooBigDialogFragment {
|
||||||
|
val fragment = FileTooBigDialogFragment()
|
||||||
|
fragment.arguments = Bundle().apply {
|
||||||
|
putParcelable(KEY_FILE_URI, attachmentToUploadUri)
|
||||||
|
putString(KEY_FILE_NAME, fileName)
|
||||||
|
}
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePass DX.
|
|
||||||
*
|
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePass DX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import android.view.View
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.biometric.FingerPrintAnimatedVector
|
|
||||||
import com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
|
||||||
class FingerPrintExplanationDialog : DialogFragment() {
|
|
||||||
|
|
||||||
private var fingerPrintAnimatedVector: FingerPrintAnimatedVector? = null
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
activity?.let { activity ->
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
|
||||||
val inflater = activity.layoutInflater
|
|
||||||
|
|
||||||
val rootView = inflater.inflate(R.layout.fragment_fingerprint_explanation, null)
|
|
||||||
|
|
||||||
rootView.findViewById<View>(R.id.fingerprint_setting_link_text).setOnClickListener {
|
|
||||||
startActivity(Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS))
|
|
||||||
}
|
|
||||||
|
|
||||||
rootView.findViewById<View>(R.id.auto_open_biometric_prompt_button).setOnClickListener {
|
|
||||||
startActivity(Intent(activity, SettingsAdvancedUnlockActivity::class.java))
|
|
||||||
}
|
|
||||||
|
|
||||||
fingerPrintAnimatedVector = FingerPrintAnimatedVector(activity,
|
|
||||||
rootView.findViewById(R.id.biometric_image))
|
|
||||||
|
|
||||||
builder.setView(rootView)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
|
||||||
return builder.create()
|
|
||||||
}
|
|
||||||
return super.onCreateDialog(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
fingerPrintAnimatedVector?.startScan()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
fingerPrintAnimatedVector?.stopScan()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
@@ -64,6 +64,11 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
@@ -167,7 +172,6 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
var password = ""
|
var password = ""
|
||||||
try {
|
try {
|
||||||
val length = Integer.valueOf(root?.findViewById<EditText>(R.id.length)?.text.toString())
|
val length = Integer.valueOf(root?.findViewById<EditText>(R.id.length)?.text.toString())
|
||||||
|
|
||||||
password = PasswordGenerator(resources).generatePassword(length,
|
password = PasswordGenerator(resources).generatePassword(length,
|
||||||
uppercaseBox?.isChecked == true,
|
uppercaseBox?.isChecked == true,
|
||||||
lowercaseBox?.isChecked == true,
|
lowercaseBox?.isChecked == true,
|
||||||
@@ -178,6 +182,7 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
specialsBox?.isChecked == true,
|
specialsBox?.isChecked == true,
|
||||||
bracketsBox?.isChecked == true,
|
bracketsBox?.isChecked == true,
|
||||||
extendedBox?.isChecked == true)
|
extendedBox?.isChecked == true)
|
||||||
|
passwordInputLayoutView?.error = null
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) {
|
||||||
passwordInputLayoutView?.error = getString(R.string.error_wrong_length)
|
passwordInputLayoutView?.error = getString(R.string.error_wrong_length)
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
@@ -193,7 +198,6 @@ class GeneratePasswordDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val KEY_PASSWORD_ID = "KEY_PASSWORD_ID"
|
const val KEY_PASSWORD_ID = "KEY_PASSWORD_ID"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
@@ -33,8 +33,8 @@ import com.kunzisoft.keepass.R
|
|||||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION
|
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
|
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.GroupVersioned
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
import com.kunzisoft.keepass.database.element.PwIcon
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
|
|
||||||
class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconPickerListener {
|
class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconPickerListener {
|
||||||
@@ -45,7 +45,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
|
|
||||||
private var editGroupDialogAction: EditGroupDialogAction? = null
|
private var editGroupDialogAction: EditGroupDialogAction? = null
|
||||||
private var nameGroup: String? = null
|
private var nameGroup: String? = null
|
||||||
private var iconGroup: PwIcon? = null
|
private var iconGroup: IconImage? = null
|
||||||
|
|
||||||
private var nameTextLayoutView: TextInputLayout? = null
|
private var nameTextLayoutView: TextInputLayout? = null
|
||||||
private var nameTextView: TextView? = null
|
private var nameTextView: TextView? = null
|
||||||
@@ -73,7 +73,11 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
throw ClassCastException(context.toString()
|
throw ClassCastException(context.toString()
|
||||||
+ " must implement " + GroupEditDialogFragment::class.java.name)
|
+ " must implement " + GroupEditDialogFragment::class.java.name)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
editGroupListener = null
|
||||||
|
super.onDetach()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
@@ -130,9 +134,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
}
|
}
|
||||||
|
|
||||||
iconButtonView?.setOnClickListener { _ ->
|
iconButtonView?.setOnClickListener { _ ->
|
||||||
fragmentManager?.let {
|
IconPickerDialogFragment().show(parentFragmentManager, "IconPickerDialogFragment")
|
||||||
IconPickerDialogFragment().show(it, "IconPickerDialogFragment")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.create()
|
return builder.create()
|
||||||
@@ -186,8 +188,8 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface EditGroupListener {
|
interface EditGroupListener {
|
||||||
fun approveEditGroup(action: EditGroupDialogAction?, name: String?, icon: PwIcon?)
|
fun approveEditGroup(action: EditGroupDialogAction?, name: String?, icon: IconImage?)
|
||||||
fun cancelEditGroup(action: EditGroupDialogAction?, name: String?, icon: PwIcon?)
|
fun cancelEditGroup(action: EditGroupDialogAction?, name: String?, icon: IconImage?)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -206,7 +208,7 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
return fragment
|
return fragment
|
||||||
}
|
}
|
||||||
|
|
||||||
fun build(group: GroupVersioned): GroupEditDialogFragment {
|
fun build(group: Group): GroupEditDialogFragment {
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
bundle.putString(KEY_NAME, group.title)
|
bundle.putString(KEY_NAME, group.title)
|
||||||
bundle.putParcelable(KEY_ICON, group.icon)
|
bundle.putParcelable(KEY_ICON, group.icon)
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
@@ -24,18 +24,19 @@ import android.content.Context
|
|||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.core.widget.ImageViewCompat
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.BaseAdapter
|
import android.widget.BaseAdapter
|
||||||
import android.widget.GridView
|
import android.widget.GridView
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.widget.ImageViewCompat
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
import com.kunzisoft.keepass.database.element.PwIconStandard
|
|
||||||
import com.kunzisoft.keepass.icons.IconPack
|
import com.kunzisoft.keepass.icons.IconPack
|
||||||
import com.kunzisoft.keepass.icons.IconPackChooser
|
import com.kunzisoft.keepass.icons.IconPackChooser
|
||||||
|
|
||||||
@@ -56,11 +57,16 @@ class IconPickerDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
iconPickerListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|
||||||
iconPack = IconPackChooser.getSelectedIconPack(context!!)
|
iconPack = IconPackChooser.getSelectedIconPack(requireContext())
|
||||||
|
|
||||||
// Inflate and set the layout for the dialog
|
// Inflate and set the layout for the dialog
|
||||||
// Pass null as the parent view because its going in the dialog layout
|
// Pass null as the parent view because its going in the dialog layout
|
||||||
@@ -72,7 +78,7 @@ class IconPickerDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
currIconGridView.setOnItemClickListener { _, _, position, _ ->
|
currIconGridView.setOnItemClickListener { _, _, position, _ ->
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
bundle.putParcelable(KEY_ICON_STANDARD, PwIconStandard(position))
|
bundle.putParcelable(KEY_ICON_STANDARD, IconImageStandard(position))
|
||||||
iconPickerListener?.iconPicked(bundle)
|
iconPickerListener?.iconPicked(bundle)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
@@ -112,7 +118,7 @@ class IconPickerDialogFragment : DialogFragment() {
|
|||||||
// Retrieve the textColor to tint the icon
|
// Retrieve the textColor to tint the icon
|
||||||
val ta = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
val ta = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
ImageViewCompat.setImageTintList(iconImageView, ColorStateList.valueOf(ta.getColor(0, Color.BLACK)))
|
ImageViewCompat.setImageTintList(iconImageView, ColorStateList.valueOf(ta.getColor(0, Color.BLACK)))
|
||||||
ta?.recycle()
|
ta.recycle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,11 +134,11 @@ class IconPickerDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
private const val KEY_ICON_STANDARD = "KEY_ICON_STANDARD"
|
private const val KEY_ICON_STANDARD = "KEY_ICON_STANDARD"
|
||||||
|
|
||||||
fun getIconStandardFromBundle(bundle: Bundle): PwIconStandard? {
|
fun getIconStandardFromBundle(bundle: Bundle): IconImageStandard? {
|
||||||
return bundle.getParcelable(KEY_ICON_STANDARD)
|
return bundle.getParcelable(KEY_ICON_STANDARD)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launch(activity: StylishActivity) {
|
fun launch(activity: FragmentActivity) {
|
||||||
// Create an instance of the dialog fragment and show it
|
// Create an instance of the dialog fragment and show it
|
||||||
val dialog = IconPickerDialogFragment()
|
val dialog = IconPickerDialogFragment()
|
||||||
dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment")
|
dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment")
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePass DX.
|
|
||||||
*
|
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePass DX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.provider.Settings
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import android.view.View
|
|
||||||
import com.kunzisoft.keepass.BuildConfig
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
|
||||||
|
|
||||||
class KeyboardExplanationDialogFragment : DialogFragment() {
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
activity?.let {
|
|
||||||
val builder = AlertDialog.Builder(activity!!)
|
|
||||||
val inflater = activity!!.layoutInflater
|
|
||||||
|
|
||||||
val rootView = inflater.inflate(R.layout.fragment_keyboard_explanation, null)
|
|
||||||
|
|
||||||
rootView.findViewById<View>(R.id.keyboards_activate_device_setting_button)
|
|
||||||
.setOnClickListener { launchActivateKeyboardSetting() }
|
|
||||||
|
|
||||||
val containerKeyboardSwitcher = rootView.findViewById<View>(R.id.container_keyboard_switcher)
|
|
||||||
if (BuildConfig.CLOSED_STORE) {
|
|
||||||
containerKeyboardSwitcher.setOnClickListener { UriUtil.gotoUrl(context!!, R.string.keyboard_switcher_play_store) }
|
|
||||||
} else {
|
|
||||||
containerKeyboardSwitcher.setOnClickListener { UriUtil.gotoUrl(context!!, R.string.keyboard_switcher_f_droid) }
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.setView(rootView)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
|
||||||
return builder.create()
|
|
||||||
}
|
|
||||||
return super.onCreateDialog(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchActivateKeyboardSetting() {
|
|
||||||
val intent = Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)
|
|
||||||
intent.addFlags(FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +1,71 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.DialogInterface
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
class PasswordEncodingDialogFragment : DialogFragment() {
|
class PasswordEncodingDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
var positiveButtonClickListener: DialogInterface.OnClickListener? = null
|
private var mListener: Listener? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
try {
|
||||||
|
mListener = context as Listener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + Listener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|
||||||
|
val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY)
|
||||||
|
val masterPasswordChecked: Boolean = savedInstanceState?.getBoolean(MASTER_PASSWORD_CHECKED_KEY) ?: false
|
||||||
|
val masterPassword: String? = savedInstanceState?.getString(MASTER_PASSWORD_KEY)
|
||||||
|
val keyFileChecked: Boolean = savedInstanceState?.getBoolean(KEY_FILE_CHECKED_KEY) ?: false
|
||||||
|
val keyFile: Uri? = savedInstanceState?.getParcelable(KEY_FILE_URI_KEY)
|
||||||
|
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning)
|
builder.setMessage(activity.getString(R.string.warning_password_encoding)).setTitle(R.string.warning)
|
||||||
builder.setPositiveButton(android.R.string.ok, positiveButtonClickListener)
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
mListener?.onPasswordEncodingValidateListener(
|
||||||
|
databaseUri,
|
||||||
|
masterPasswordChecked,
|
||||||
|
masterPassword,
|
||||||
|
keyFileChecked,
|
||||||
|
keyFile
|
||||||
|
)
|
||||||
|
}
|
||||||
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
|
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
|
||||||
|
|
||||||
return builder.create()
|
return builder.create()
|
||||||
@@ -42,5 +73,36 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
|||||||
return super.onCreateDialog(savedInstanceState)
|
return super.onCreateDialog(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun onPasswordEncodingValidateListener(databaseUri: Uri?,
|
||||||
|
masterPasswordChecked: Boolean,
|
||||||
|
masterPassword: String?,
|
||||||
|
keyFileChecked: Boolean,
|
||||||
|
keyFile: Uri?)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
|
||||||
|
private const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY"
|
||||||
|
private const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY"
|
||||||
|
private const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY"
|
||||||
|
private const val KEY_FILE_URI_KEY = "KEY_FILE_URI_KEY"
|
||||||
|
|
||||||
|
fun getInstance(databaseUri: Uri,
|
||||||
|
masterPasswordChecked: Boolean,
|
||||||
|
masterPassword: String?,
|
||||||
|
keyFileChecked: Boolean,
|
||||||
|
keyFile: Uri?): SortDialogFragment {
|
||||||
|
val fragment = SortDialogFragment()
|
||||||
|
fragment.arguments = Bundle().apply {
|
||||||
|
putParcelable(DATABASE_URI_KEY, databaseUri)
|
||||||
|
putBoolean(MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
||||||
|
putString(MASTER_PASSWORD_KEY, masterPassword)
|
||||||
|
putBoolean(KEY_FILE_CHECKED_KEY, keyFileChecked)
|
||||||
|
putParcelable(KEY_FILE_URI_KEY, keyFile)
|
||||||
|
}
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
@@ -45,13 +45,13 @@ class ProFeatureDialogFragment : DialogFragment() {
|
|||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_ad_free), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_ad_free), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_buy_pro), FROM_HTML_MODE_LEGACY))
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_buy_pro), FROM_HTML_MODE_LEGACY))
|
||||||
builder.setPositiveButton(R.string.download) { _, _ ->
|
builder.setPositiveButton(R.string.download) { _, _ ->
|
||||||
UriUtil.gotoUrl(context!!, R.string.app_pro_url)
|
UriUtil.gotoUrl(requireContext(), R.string.app_pro_url)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_feature_generosity), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_feature_generosity), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_donation), FROM_HTML_MODE_LEGACY))
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_donation), FROM_HTML_MODE_LEGACY))
|
||||||
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||||
UriUtil.gotoUrl(context!!, R.string.contribution_url)
|
UriUtil.gotoUrl(requireContext(), R.string.contribution_url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
builder.setMessage(stringBuilder)
|
builder.setMessage(stringBuilder)
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePass DX.
|
|
||||||
*
|
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePass DX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.preference.PreferenceManager
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
|
|
||||||
class ReadOnlyDialog : DialogFragment() {
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
activity?.let { activity ->
|
|
||||||
// Use the Builder class for convenient dialog construction
|
|
||||||
val builder = androidx.appcompat.app.AlertDialog.Builder(activity)
|
|
||||||
|
|
||||||
var warning = getString(R.string.read_only_warning)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
|
||||||
warning = warning + "\n\n" + getString(R.string.read_only_kitkat_warning)
|
|
||||||
}
|
|
||||||
builder.setMessage(warning)
|
|
||||||
|
|
||||||
builder.setPositiveButton(getString(android.R.string.ok)) { _, _ -> dismiss() }
|
|
||||||
builder.setNegativeButton(getString(R.string.beta_dontask)) { _, _ ->
|
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
val edit = prefs.edit()
|
|
||||||
edit.putBoolean(getString(R.string.show_read_only_warning), false)
|
|
||||||
edit.apply()
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the AlertDialog object and return it
|
|
||||||
return builder.create()
|
|
||||||
}
|
|
||||||
return super.onCreateDialog(savedInstanceState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Dialog to confirm big file to upload
|
||||||
|
*/
|
||||||
|
class ReplaceFileDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
|
private var mActionChooseListener: ActionChooseListener? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
// Verify that the host activity implements the callback interface
|
||||||
|
try {
|
||||||
|
mActionChooseListener = context as ActionChooseListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + ActionChooseListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mActionChooseListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
// Use the Builder class for convenient dialog construction
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
builder.setMessage(SpannableStringBuilder().apply {
|
||||||
|
append(getString(R.string.warning_replace_file))
|
||||||
|
append("\n\n")
|
||||||
|
append(getString(R.string.warning_sure_add_file))
|
||||||
|
})
|
||||||
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
mActionChooseListener?.onValidateReplaceFile(
|
||||||
|
arguments?.getParcelable(KEY_FILE_URI),
|
||||||
|
arguments?.getParcelable(KEY_ENTRY_ATTACHMENT))
|
||||||
|
}
|
||||||
|
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
// Create the AlertDialog object and return it
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionChooseListener {
|
||||||
|
fun onValidateReplaceFile(attachmentToUploadUri: Uri?, attachment: Attachment?)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY_FILE_URI = "KEY_FILE_URI"
|
||||||
|
private const val KEY_ENTRY_ATTACHMENT = "KEY_ENTRY_ATTACHMENT"
|
||||||
|
|
||||||
|
fun build(attachmentToUploadUri: Uri,
|
||||||
|
attachment: Attachment): ReplaceFileDialogFragment {
|
||||||
|
val fragment = ReplaceFileDialogFragment()
|
||||||
|
fragment.arguments = Bundle().apply {
|
||||||
|
putParcelable(KEY_FILE_URI, attachmentToUploadUri)
|
||||||
|
putParcelable(KEY_ENTRY_ATTACHMENT, attachment)
|
||||||
|
}
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,441 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.Spinner
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.model.OtpModel
|
||||||
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_HOTP_COUNTER
|
||||||
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_OTP_DIGITS
|
||||||
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_TOTP_PERIOD
|
||||||
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_HOTP_COUNTER
|
||||||
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_DIGITS
|
||||||
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
|
||||||
|
import com.kunzisoft.keepass.otp.OtpTokenType
|
||||||
|
import com.kunzisoft.keepass.otp.OtpType
|
||||||
|
import com.kunzisoft.keepass.otp.TokenCalculator
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class SetOTPDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
|
private var mCreateOTPElementListener: CreateOtpListener? = null
|
||||||
|
|
||||||
|
private var mOtpElement: OtpElement = OtpElement()
|
||||||
|
|
||||||
|
private var otpTypeSpinner: Spinner? = null
|
||||||
|
private var otpTokenTypeSpinner: Spinner? = null
|
||||||
|
private var otpSecretContainer: TextInputLayout? = null
|
||||||
|
private var otpSecretTextView: EditText? = null
|
||||||
|
private var otpPeriodContainer: TextInputLayout? = null
|
||||||
|
private var otpPeriodTextView: EditText? = null
|
||||||
|
private var otpCounterContainer: TextInputLayout? = null
|
||||||
|
private var otpCounterTextView: EditText? = null
|
||||||
|
private var otpDigitsContainer: TextInputLayout? = null
|
||||||
|
private var otpDigitsTextView: EditText? = null
|
||||||
|
private var otpAlgorithmSpinner: Spinner? = null
|
||||||
|
|
||||||
|
private var otpTypeAdapter: ArrayAdapter<OtpType>? = null
|
||||||
|
private var otpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
||||||
|
private var totpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
||||||
|
private var hotpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
||||||
|
private var otpAlgorithmAdapter: ArrayAdapter<TokenCalculator.HashAlgorithm>? = null
|
||||||
|
|
||||||
|
private var mManualEvent = false
|
||||||
|
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
|
||||||
|
if (!isFocus)
|
||||||
|
mManualEvent = true
|
||||||
|
}
|
||||||
|
private var mOnTouchListener = View.OnTouchListener { _, event ->
|
||||||
|
when (event.action) {
|
||||||
|
MotionEvent.ACTION_DOWN -> {
|
||||||
|
mManualEvent = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mSecretWellFormed = false
|
||||||
|
private var mCounterWellFormed = false
|
||||||
|
private var mPeriodWellFormed = false
|
||||||
|
private var mDigitsWellFormed = false
|
||||||
|
|
||||||
|
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
|
||||||
|
mCreateOTPElementListener = context as CreateOtpListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
// The activity doesn't implement the interface, throw exception
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + CreateOtpListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mCreateOTPElementListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|
||||||
|
// Retrieve OTP model from instance state
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
if (savedInstanceState.containsKey(KEY_OTP)) {
|
||||||
|
savedInstanceState.getParcelable<OtpModel>(KEY_OTP)?.let { otpModel ->
|
||||||
|
mOtpElement = OtpElement(otpModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(KEY_OTP)) {
|
||||||
|
getParcelable<OtpModel?>(KEY_OTP)?.let { otpModel ->
|
||||||
|
mOtpElement = OtpElement(otpModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activity?.let { activity ->
|
||||||
|
val root = activity.layoutInflater.inflate(R.layout.fragment_set_otp, null) as ViewGroup?
|
||||||
|
otpTypeSpinner = root?.findViewById(R.id.setup_otp_type)
|
||||||
|
otpTokenTypeSpinner = root?.findViewById(R.id.setup_otp_token_type)
|
||||||
|
otpSecretContainer = root?.findViewById(R.id.setup_otp_secret_label)
|
||||||
|
otpSecretTextView = root?.findViewById(R.id.setup_otp_secret)
|
||||||
|
otpAlgorithmSpinner = root?.findViewById(R.id.setup_otp_algorithm)
|
||||||
|
otpPeriodContainer= root?.findViewById(R.id.setup_otp_period_label)
|
||||||
|
otpPeriodTextView = root?.findViewById(R.id.setup_otp_period)
|
||||||
|
otpCounterContainer= root?.findViewById(R.id.setup_otp_counter_label)
|
||||||
|
otpCounterTextView = root?.findViewById(R.id.setup_otp_counter)
|
||||||
|
otpDigitsContainer = root?.findViewById(R.id.setup_otp_digits_label)
|
||||||
|
otpDigitsTextView = root?.findViewById(R.id.setup_otp_digits)
|
||||||
|
|
||||||
|
// To fix init element
|
||||||
|
// With tab keyboard selection
|
||||||
|
otpSecretTextView?.onFocusChangeListener = mOnFocusChangeListener
|
||||||
|
// With finger selection
|
||||||
|
otpTypeSpinner?.setOnTouchListener(mOnTouchListener)
|
||||||
|
otpTokenTypeSpinner?.setOnTouchListener(mOnTouchListener)
|
||||||
|
otpSecretTextView?.setOnTouchListener(mOnTouchListener)
|
||||||
|
otpAlgorithmSpinner?.setOnTouchListener(mOnTouchListener)
|
||||||
|
otpPeriodTextView?.setOnTouchListener(mOnTouchListener)
|
||||||
|
otpCounterTextView?.setOnTouchListener(mOnTouchListener)
|
||||||
|
otpDigitsTextView?.setOnTouchListener(mOnTouchListener)
|
||||||
|
|
||||||
|
// To manage focus
|
||||||
|
otpPeriodTextView?.setOnEditorActionListener { _, actionId, _ ->
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_NEXT) {
|
||||||
|
otpDigitsTextView?.requestFocus()
|
||||||
|
true
|
||||||
|
} else
|
||||||
|
false
|
||||||
|
}
|
||||||
|
otpCounterTextView?.setOnEditorActionListener { _, actionId, _ ->
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_NEXT) {
|
||||||
|
otpDigitsTextView?.requestFocus()
|
||||||
|
true
|
||||||
|
} else
|
||||||
|
false
|
||||||
|
}
|
||||||
|
otpCounterTextView?.setOnEditorActionListener { _, actionId, _ ->
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_NEXT) {
|
||||||
|
root?.requestFocus(View.FOCUS_DOWN)
|
||||||
|
true
|
||||||
|
} else
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
// HOTP / TOTP Type selection
|
||||||
|
val otpTypeArray = OtpType.values()
|
||||||
|
otpTypeAdapter = ArrayAdapter<OtpType>(activity,
|
||||||
|
android.R.layout.simple_spinner_item, otpTypeArray).apply {
|
||||||
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
|
}
|
||||||
|
otpTypeSpinner?.adapter = otpTypeAdapter
|
||||||
|
|
||||||
|
// Otp Token type selection
|
||||||
|
val hotpTokenTypeArray = OtpTokenType.getHotpTokenTypeValues()
|
||||||
|
hotpTokenTypeAdapter = ArrayAdapter(activity,
|
||||||
|
android.R.layout.simple_spinner_item, hotpTokenTypeArray).apply {
|
||||||
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
|
}
|
||||||
|
// Proprietary only on closed and full version
|
||||||
|
val totpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
|
||||||
|
BuildConfig.CLOSED_STORE && BuildConfig.FULL_VERSION)
|
||||||
|
totpTokenTypeAdapter = ArrayAdapter(activity,
|
||||||
|
android.R.layout.simple_spinner_item, totpTokenTypeArray).apply {
|
||||||
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
|
}
|
||||||
|
otpTokenTypeAdapter = hotpTokenTypeAdapter
|
||||||
|
otpTokenTypeSpinner?.adapter = otpTokenTypeAdapter
|
||||||
|
|
||||||
|
// OTP Algorithm
|
||||||
|
val otpAlgorithmArray = TokenCalculator.HashAlgorithm.values()
|
||||||
|
otpAlgorithmAdapter = ArrayAdapter<TokenCalculator.HashAlgorithm>(activity,
|
||||||
|
android.R.layout.simple_spinner_item, otpAlgorithmArray).apply {
|
||||||
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
|
}
|
||||||
|
otpAlgorithmSpinner?.adapter = otpAlgorithmAdapter
|
||||||
|
|
||||||
|
// Set the default value of OTP element
|
||||||
|
upgradeType()
|
||||||
|
upgradeTokenType()
|
||||||
|
upgradeParameters()
|
||||||
|
|
||||||
|
attachListeners()
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
builder.apply {
|
||||||
|
setTitle(R.string.entry_setup_otp)
|
||||||
|
setView(root)
|
||||||
|
.setPositiveButton(android.R.string.ok) {_, _ -> }
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
(dialog as AlertDialog).getButton(Dialog.BUTTON_POSITIVE).setOnClickListener {
|
||||||
|
if (mSecretWellFormed
|
||||||
|
&& mCounterWellFormed
|
||||||
|
&& mPeriodWellFormed
|
||||||
|
&& mDigitsWellFormed) {
|
||||||
|
mCreateOTPElementListener?.onOtpCreated(mOtpElement)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun attachListeners() {
|
||||||
|
// Set Type listener
|
||||||
|
otpTypeSpinner?.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||||
|
|
||||||
|
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||||
|
if (mManualEvent) {
|
||||||
|
(parent?.selectedItem as OtpType?)?.let {
|
||||||
|
mOtpElement.type = it
|
||||||
|
upgradeTokenType()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set type token listener
|
||||||
|
otpTokenTypeSpinner?.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||||
|
|
||||||
|
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||||
|
if (mManualEvent) {
|
||||||
|
(parent?.selectedItem as OtpTokenType?)?.let {
|
||||||
|
mOtpElement.tokenType = it
|
||||||
|
upgradeParameters()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set algorithm spinner
|
||||||
|
otpAlgorithmSpinner?.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||||
|
|
||||||
|
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||||
|
if (mManualEvent) {
|
||||||
|
(parent?.selectedItem as TokenCalculator.HashAlgorithm?)?.let {
|
||||||
|
mOtpElement.algorithm = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set secret in OtpElement
|
||||||
|
otpSecretTextView?.addTextChangedListener(object: TextWatcher {
|
||||||
|
override fun afterTextChanged(s: Editable?) {
|
||||||
|
s?.toString()?.let { userString ->
|
||||||
|
try {
|
||||||
|
mOtpElement.setBase32Secret(userString.toUpperCase(Locale.ENGLISH))
|
||||||
|
otpSecretContainer?.error = null
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
|
||||||
|
}
|
||||||
|
mSecretWellFormed = otpSecretContainer?.error == null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set counter in OtpElement
|
||||||
|
otpCounterTextView?.addTextChangedListener(object: TextWatcher {
|
||||||
|
override fun afterTextChanged(s: Editable?) {
|
||||||
|
if (mManualEvent) {
|
||||||
|
s?.toString()?.toLongOrNull()?.let {
|
||||||
|
try {
|
||||||
|
mOtpElement.counter = it
|
||||||
|
otpCounterContainer?.error = null
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
otpCounterContainer?.error = getString(R.string.error_otp_counter,
|
||||||
|
MIN_HOTP_COUNTER, MAX_HOTP_COUNTER)
|
||||||
|
}
|
||||||
|
mCounterWellFormed = otpCounterContainer?.error == null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set period in OtpElement
|
||||||
|
otpPeriodTextView?.addTextChangedListener(object: TextWatcher {
|
||||||
|
override fun afterTextChanged(s: Editable?) {
|
||||||
|
if (mManualEvent) {
|
||||||
|
s?.toString()?.toIntOrNull()?.let {
|
||||||
|
try {
|
||||||
|
mOtpElement.period = it
|
||||||
|
otpPeriodContainer?.error = null
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
otpPeriodContainer?.error = getString(R.string.error_otp_period,
|
||||||
|
MIN_TOTP_PERIOD, MAX_TOTP_PERIOD)
|
||||||
|
}
|
||||||
|
mPeriodWellFormed = otpPeriodContainer?.error == null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set digits in OtpElement
|
||||||
|
otpDigitsTextView?.addTextChangedListener(object: TextWatcher {
|
||||||
|
override fun afterTextChanged(s: Editable?) {
|
||||||
|
if (mManualEvent) {
|
||||||
|
s?.toString()?.toIntOrNull()?.let {
|
||||||
|
try {
|
||||||
|
mOtpElement.digits = it
|
||||||
|
otpDigitsContainer?.error = null
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
otpDigitsContainer?.error = getString(R.string.error_otp_digits,
|
||||||
|
MIN_OTP_DIGITS, MAX_OTP_DIGITS)
|
||||||
|
}
|
||||||
|
mDigitsWellFormed = otpDigitsContainer?.error == null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun upgradeType() {
|
||||||
|
otpTypeSpinner?.setSelection(OtpType.values().indexOf(mOtpElement.type))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun upgradeTokenType() {
|
||||||
|
when (mOtpElement.type) {
|
||||||
|
OtpType.HOTP -> {
|
||||||
|
otpPeriodContainer?.visibility = View.GONE
|
||||||
|
otpCounterContainer?.visibility = View.VISIBLE
|
||||||
|
otpTokenTypeSpinner?.adapter = hotpTokenTypeAdapter
|
||||||
|
otpTokenTypeSpinner?.setSelection(OtpTokenType
|
||||||
|
.getHotpTokenTypeValues().indexOf(mOtpElement.tokenType))
|
||||||
|
}
|
||||||
|
OtpType.TOTP -> {
|
||||||
|
otpPeriodContainer?.visibility = View.VISIBLE
|
||||||
|
otpCounterContainer?.visibility = View.GONE
|
||||||
|
otpTokenTypeSpinner?.adapter = totpTokenTypeAdapter
|
||||||
|
otpTokenTypeSpinner?.setSelection(OtpTokenType
|
||||||
|
.getTotpTokenTypeValues().indexOf(mOtpElement.tokenType))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun upgradeParameters() {
|
||||||
|
otpAlgorithmSpinner?.setSelection(TokenCalculator.HashAlgorithm.values()
|
||||||
|
.indexOf(mOtpElement.algorithm))
|
||||||
|
|
||||||
|
val secret = mOtpElement.getBase32Secret()
|
||||||
|
otpSecretTextView?.apply {
|
||||||
|
setText(secret)
|
||||||
|
// Cursor at end
|
||||||
|
setSelection(this.text.length)
|
||||||
|
}
|
||||||
|
mSecretWellFormed = OtpElement.isValidBase32(secret)
|
||||||
|
|
||||||
|
val counter = mOtpElement.counter
|
||||||
|
otpCounterTextView?.setText(counter.toString())
|
||||||
|
mCounterWellFormed = OtpElement.isValidCounter(counter)
|
||||||
|
|
||||||
|
val period = mOtpElement.period
|
||||||
|
otpPeriodTextView?.setText(period.toString())
|
||||||
|
mPeriodWellFormed = OtpElement.isValidPeriod(period)
|
||||||
|
|
||||||
|
val digits = mOtpElement.digits
|
||||||
|
otpDigitsTextView?.setText(digits.toString())
|
||||||
|
mDigitsWellFormed = OtpElement.isValidDigits(digits)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putParcelable(KEY_OTP, mOtpElement.otpModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateOtpListener {
|
||||||
|
fun onOtpCreated(otpElement: OtpElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val KEY_OTP = "KEY_OTP"
|
||||||
|
|
||||||
|
fun build(otpModel: OtpModel? = null): SetOTPDialogFragment {
|
||||||
|
return SetOTPDialogFragment().apply {
|
||||||
|
if (otpModel != null) {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putParcelable(KEY_OTP, otpModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
@@ -29,7 +29,7 @@ import android.view.View
|
|||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import android.widget.RadioGroup
|
import android.widget.RadioGroup
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.SortNodeEnum
|
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||||
|
|
||||||
class SortDialogFragment : DialogFragment() {
|
class SortDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
@@ -54,6 +54,11 @@ class SortDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
@@ -82,7 +87,9 @@ class SortDialogFragment : DialogFragment() {
|
|||||||
builder.setView(rootView)
|
builder.setView(rootView)
|
||||||
// Add action buttons
|
// Add action buttons
|
||||||
.setPositiveButton(android.R.string.ok
|
.setPositiveButton(android.R.string.ok
|
||||||
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum, mAscending, mGroupsBefore, mRecycleBinBottom) }
|
) { _, _ -> mListener?.onSortSelected(mSortNodeEnum,
|
||||||
|
SortNodeEnum.SortNodeParameters(mAscending, mGroupsBefore, mRecycleBinBottom))
|
||||||
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
|
||||||
val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending)
|
val ascendingView = rootView.findViewById<CompoundButton>(R.id.sort_selection_ascending)
|
||||||
@@ -150,10 +157,7 @@ class SortDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SortSelectionListener {
|
interface SortSelectionListener {
|
||||||
fun onSortSelected(sortNodeEnum: SortNodeEnum,
|
fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters)
|
||||||
ascending: Boolean,
|
|
||||||
groupsBefore: Boolean,
|
|
||||||
recycleBinBottom: Boolean)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.DatePickerDialog
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.app.TimePickerDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.format.DateFormat
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
|
||||||
|
class TimePickerFragment : DialogFragment() {
|
||||||
|
|
||||||
|
private var defaultHour: Int = 0
|
||||||
|
private var defaultMinute: Int = 0
|
||||||
|
|
||||||
|
private var mListener: TimePickerDialog.OnTimeSetListener? = null
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
try {
|
||||||
|
mListener = context as TimePickerDialog.OnTimeSetListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
// Create a new instance of DatePickerDialog and return it
|
||||||
|
return context?.let {
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(DEFAULT_HOUR_BUNDLE_KEY))
|
||||||
|
defaultHour = getInt(DEFAULT_HOUR_BUNDLE_KEY)
|
||||||
|
if (containsKey(DEFAULT_MINUTE_BUNDLE_KEY))
|
||||||
|
defaultMinute = getInt(DEFAULT_MINUTE_BUNDLE_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
TimePickerDialog(it, mListener, defaultHour, defaultMinute, DateFormat.is24HourFormat(activity))
|
||||||
|
} ?: super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val DEFAULT_HOUR_BUNDLE_KEY = "DEFAULT_HOUR_BUNDLE_KEY"
|
||||||
|
private const val DEFAULT_MINUTE_BUNDLE_KEY = "DEFAULT_MINUTE_BUNDLE_KEY"
|
||||||
|
|
||||||
|
fun getInstance(defaultHour: Int,
|
||||||
|
defaultMinute: Int): TimePickerFragment {
|
||||||
|
return TimePickerFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putInt(DEFAULT_HOUR_BUNDLE_KEY, defaultHour)
|
||||||
|
putInt(DEFAULT_MINUTE_BUNDLE_KEY, defaultMinute)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
@@ -24,7 +24,6 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import android.text.Html
|
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
@@ -52,7 +52,7 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
|
|||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_buy_pro), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
|
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_buy_pro), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
|
||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||||
builder.setPositiveButton(R.string.download) { _, _ ->
|
builder.setPositiveButton(R.string.download) { _, _ ->
|
||||||
UriUtil.gotoUrl(context!!, R.string.app_pro_url)
|
UriUtil.gotoUrl(requireContext(), R.string.app_pro_url)
|
||||||
}
|
}
|
||||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
|
|||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_contibute), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_contibute), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||||
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||||
UriUtil.gotoUrl(context!!, R.string.contribution_url)
|
UriUtil.gotoUrl(requireContext(), R.string.contribution_url)
|
||||||
}
|
}
|
||||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
package com.kunzisoft.keepass.activities.helpers
|
package com.kunzisoft.keepass.activities.helpers
|
||||||
|
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
@@ -5,15 +24,22 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
|
||||||
object EntrySelectionHelper {
|
object EntrySelectionHelper {
|
||||||
|
|
||||||
private const val EXTRA_ENTRY_SELECTION_MODE = "com.kunzisoft.keepass.extra.ENTRY_SELECTION_MODE"
|
private const val EXTRA_ENTRY_SELECTION_MODE = "com.kunzisoft.keepass.extra.ENTRY_SELECTION_MODE"
|
||||||
private const val DEFAULT_ENTRY_SELECTION_MODE = false
|
private const val DEFAULT_ENTRY_SELECTION_MODE = false
|
||||||
|
// Key to retrieve search in intent
|
||||||
|
const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
||||||
|
|
||||||
fun startActivityForEntrySelection(context: Context, intent: Intent) {
|
fun startActivityForEntrySelectionResult(context: Context,
|
||||||
|
intent: Intent,
|
||||||
|
searchInfo: SearchInfo?) {
|
||||||
addEntrySelectionModeExtraInIntent(intent)
|
addEntrySelectionModeExtraInIntent(intent)
|
||||||
// only to avoid visible flickering when redirecting
|
searchInfo?.let {
|
||||||
|
intent.putExtra(KEY_SEARCH_INFO, it)
|
||||||
|
}
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
package com.kunzisoft.keepass.activities.helpers
|
package com.kunzisoft.keepass.activities.helpers
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.helpers
|
package com.kunzisoft.keepass.activities.helpers
|
||||||
@@ -28,19 +28,20 @@ import android.content.pm.PackageManager
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.kunzisoft.keepass.activities.dialogs.BrowserDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
class OpenFileHelper {
|
class SelectFileHelper {
|
||||||
|
|
||||||
private var activity: Activity? = null
|
private var activity: Activity? = null
|
||||||
private var fragment: Fragment? = null
|
private var fragment: Fragment? = null
|
||||||
|
|
||||||
val openFileOnClickViewListener: OpenFileOnClickViewListener
|
val selectFileOnClickViewListener: SelectFileOnClickViewListener
|
||||||
get() = OpenFileOnClickViewListener()
|
get() = SelectFileOnClickViewListener()
|
||||||
|
|
||||||
constructor(context: Activity) {
|
constructor(context: Activity) {
|
||||||
this.activity = context
|
this.activity = context
|
||||||
@@ -52,14 +53,25 @@ class OpenFileHelper {
|
|||||||
this.fragment = context
|
this.fragment = context
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class OpenFileOnClickViewListener : View.OnClickListener {
|
inner class SelectFileOnClickViewListener :
|
||||||
|
View.OnClickListener,
|
||||||
|
View.OnLongClickListener,
|
||||||
|
MenuItem.OnMenuItemClickListener {
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
private fun onAbstractClick(longClick: Boolean = false) {
|
||||||
try {
|
try {
|
||||||
try {
|
if (longClick) {
|
||||||
openActivityWithActionOpenDocument()
|
try {
|
||||||
} catch(e: Exception) {
|
openActivityWithActionGetContent()
|
||||||
openActivityWithActionGetContent()
|
} catch (e: Exception) {
|
||||||
|
openActivityWithActionOpenDocument()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
openActivityWithActionOpenDocument()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
openActivityWithActionGetContent()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Enable to start the file picker activity", e)
|
Log.e(TAG, "Enable to start the file picker activity", e)
|
||||||
@@ -68,16 +80,31 @@ class OpenFileHelper {
|
|||||||
showBrowserDialog()
|
showBrowserDialog()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onClick(v: View) {
|
||||||
|
onAbstractClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLongClick(v: View?): Boolean {
|
||||||
|
onAbstractClick(true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
||||||
|
onAbstractClick()
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
private fun openActivityWithActionOpenDocument() {
|
private fun openActivityWithActionOpenDocument() {
|
||||||
val intentOpenDocument = Intent(APP_ACTION_OPEN_DOCUMENT).apply {
|
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
type = "*/*"
|
type = "*/*"
|
||||||
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
|
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||||
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
}
|
}
|
||||||
if (fragment != null)
|
if (fragment != null)
|
||||||
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||||
@@ -85,10 +112,15 @@ class OpenFileHelper {
|
|||||||
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
private fun openActivityWithActionGetContent() {
|
private fun openActivityWithActionGetContent() {
|
||||||
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
type = "*/*"
|
type = "*/*"
|
||||||
|
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
|
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
}
|
}
|
||||||
if (fragment != null)
|
if (fragment != null)
|
||||||
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
|
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
|
||||||
@@ -141,11 +173,10 @@ class OpenFileHelper {
|
|||||||
*/
|
*/
|
||||||
private fun showBrowserDialog() {
|
private fun showBrowserDialog() {
|
||||||
try {
|
try {
|
||||||
val browserDialogFragment = BrowserDialogFragment()
|
val fileManagerDialogFragment = FileManagerDialogFragment()
|
||||||
if (fragment != null && fragment!!.fragmentManager != null)
|
fragment?.let {
|
||||||
browserDialogFragment.show(fragment!!.fragmentManager!!, "browserDialog")
|
fileManagerDialogFragment.show(it.parentFragmentManager, "browserDialog")
|
||||||
else
|
} ?: fileManagerDialogFragment.show((activity as FragmentActivity).supportFragmentManager, "browserDialog")
|
||||||
browserDialogFragment.show((activity as FragmentActivity).supportFragmentManager, "browserDialog")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Can't open BrowserDialog", e)
|
Log.e(TAG, "Can't open BrowserDialog", e)
|
||||||
}
|
}
|
||||||
@@ -204,12 +235,6 @@ class OpenFileHelper {
|
|||||||
|
|
||||||
private const val TAG = "OpenFileHelper"
|
private const val TAG = "OpenFileHelper"
|
||||||
|
|
||||||
private var APP_ACTION_OPEN_DOCUMENT: String = try {
|
|
||||||
Intent::class.java.getField("ACTION_OPEN_DOCUMENT").get(null) as String
|
|
||||||
} catch (e: Exception) {
|
|
||||||
"android.intent.action.OPEN_DOCUMENT"
|
|
||||||
}
|
|
||||||
|
|
||||||
const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE"
|
const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE"
|
||||||
|
|
||||||
private const val GET_CONTENT = 25745
|
private const val GET_CONTENT = 25745
|
||||||
@@ -1,55 +1,39 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.lock
|
package com.kunzisoft.keepass.activities.lock
|
||||||
|
|
||||||
import android.app.Activity
|
import android.annotation.SuppressLint
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
||||||
|
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.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.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
import com.kunzisoft.keepass.utils.*
|
||||||
|
|
||||||
abstract class LockingActivity : StylishActivity() {
|
abstract class LockingActivity : SpecialModeActivity() {
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val TAG = "LockingActivity"
|
|
||||||
|
|
||||||
const val RESULT_EXIT_LOCK = 1450
|
|
||||||
|
|
||||||
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
|
||||||
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
|
||||||
}
|
|
||||||
|
|
||||||
protected var mTimeoutEnable: Boolean = true
|
protected var mTimeoutEnable: Boolean = true
|
||||||
|
|
||||||
@@ -57,11 +41,18 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
private var mExitLock: Boolean = false
|
private var mExitLock: Boolean = false
|
||||||
|
|
||||||
// Force readOnly if Entry Selection mode
|
// Force readOnly if Entry Selection mode
|
||||||
protected var mReadOnly: Boolean = false
|
protected var mReadOnly: Boolean
|
||||||
get() {
|
get() {
|
||||||
return field || mSelectionMode
|
return mReadOnlyToSave || mSelectionMode
|
||||||
}
|
}
|
||||||
protected var mSelectionMode: Boolean = false
|
set(value) {
|
||||||
|
mReadOnlyToSave = value
|
||||||
|
}
|
||||||
|
private var mReadOnlyToSave: Boolean = false
|
||||||
|
protected var mAutoSaveEnable: Boolean = true
|
||||||
|
|
||||||
|
var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||||
|
private set
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -75,16 +66,21 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mTimeoutEnable) {
|
if (mTimeoutEnable) {
|
||||||
mLockReceiver = LockReceiver()
|
mLockReceiver = LockReceiver {
|
||||||
val intentFilter = IntentFilter().apply {
|
closeDatabase()
|
||||||
addAction(Intent.ACTION_SCREEN_OFF)
|
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
|
||||||
addAction(LOCK_ACTION)
|
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
|
||||||
|
// Add onActivityForResult response
|
||||||
|
setResult(RESULT_EXIT_LOCK)
|
||||||
|
closeOptionsMenu()
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
registerReceiver(mLockReceiver, intentFilter)
|
registerLockReceiver(mLockReceiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
mExitLock = false
|
mExitLock = false
|
||||||
mReadOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState, intent)
|
|
||||||
|
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
@@ -100,8 +96,13 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
|
mProgressDatabaseTaskProvider?.registerProgressTask()
|
||||||
|
|
||||||
// To refresh when back to normal workflow from selection workflow
|
// To refresh when back to normal workflow from selection workflow
|
||||||
mSelectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(intent)
|
mReadOnlyToSave = ReadOnlyHelper.retrieveReadOnlyFromIntent(intent)
|
||||||
|
mAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(this)
|
||||||
|
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
|
||||||
if (mTimeoutEnable) {
|
if (mTimeoutEnable) {
|
||||||
// End activity if database not loaded
|
// End activity if database not loaded
|
||||||
@@ -119,16 +120,19 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
TimeoutHelper.recordTime(this)
|
TimeoutHelper.recordTime(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidateOptionsMenu()
|
LOCKING_ACTIVITY_UI_VISIBLE = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
ReadOnlyHelper.onSaveInstanceState(outState, mReadOnly)
|
|
||||||
outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable)
|
outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable)
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
|
LOCKING_ACTIVITY_UI_VISIBLE = false
|
||||||
|
|
||||||
|
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
if (mTimeoutEnable) {
|
if (mTimeoutEnable) {
|
||||||
@@ -138,40 +142,32 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
unregisterLockReceiver(mLockReceiver)
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
if (mLockReceiver != null)
|
|
||||||
unregisterReceiver(mLockReceiver)
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class LockReceiver : BroadcastReceiver() {
|
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
// If allowed, lock and exit
|
|
||||||
if (!TimeoutHelper.temporarilyDisableTimeout) {
|
|
||||||
intent.action?.let {
|
|
||||||
when (it) {
|
|
||||||
Intent.ACTION_SCREEN_OFF -> if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(this@LockingActivity)) {
|
|
||||||
lockAndExit()
|
|
||||||
}
|
|
||||||
LOCK_ACTION -> lockAndExit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun lockAndExit() {
|
protected fun lockAndExit() {
|
||||||
lock()
|
sendBroadcast(Intent(LOCK_ACTION))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To reset the app timeout when a view is focused or changed
|
* To reset the app timeout when a view is focused or changed
|
||||||
*/
|
*/
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
protected fun resetAppTimeoutWhenViewFocusedOrChanged(vararg views: View?) {
|
protected fun resetAppTimeoutWhenViewFocusedOrChanged(vararg views: View?) {
|
||||||
views.forEach {
|
views.forEach {
|
||||||
|
it?.setOnTouchListener { _, event ->
|
||||||
|
when (event.action) {
|
||||||
|
MotionEvent.ACTION_DOWN -> {
|
||||||
|
// Log.d(TAG, "View touched, try to reset app timeout")
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
it?.setOnFocusChangeListener { _, hasFocus ->
|
it?.setOnFocusChangeListener { _, hasFocus ->
|
||||||
if (hasFocus) {
|
if (hasFocus) {
|
||||||
Log.d(TAG, "View focused, reset app timeout")
|
// Log.d(TAG, "View focused, try to reset app timeout")
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,21 +188,17 @@ abstract class LockingActivity : StylishActivity() {
|
|||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun Activity.lock() {
|
companion object {
|
||||||
// Stop the Magikeyboard service
|
|
||||||
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
|
||||||
MagikIME.removeEntry(this)
|
|
||||||
|
|
||||||
Log.i(Activity::class.java.name, "Shutdown " + localClassName +
|
private const val TAG = "LockingActivity"
|
||||||
" after inactivity or manual lock")
|
|
||||||
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).apply {
|
const val RESULT_EXIT_LOCK = 1450
|
||||||
cancelAll()
|
|
||||||
|
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
||||||
|
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
||||||
|
|
||||||
|
private var LOCKING_ACTIVITY_UI_VISIBLE = false
|
||||||
|
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
|
||||||
}
|
}
|
||||||
// Clear data
|
|
||||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
|
||||||
// Add onActivityForResult response
|
|
||||||
setResult(LockingActivity.RESULT_EXIT_LOCK)
|
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePass DX.
|
|
||||||
*
|
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePass DX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.activities.lock
|
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.WindowManager
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Locking Hide Activity that sets FLAG_SECURE to prevent screenshots, and from
|
|
||||||
* appearing in the recent app preview
|
|
||||||
*/
|
|
||||||
abstract class LockingHideActivity : LockingActivity() {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
// Several gingerbread devices have problems with FLAG_SECURE
|
|
||||||
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* (non-Javadoc) Workaround for HTC Linkify issues
|
|
||||||
* @see android.app.Activity#startActivity(android.content.Intent)
|
|
||||||
*/
|
|
||||||
override fun startActivity(intent: Intent) {
|
|
||||||
try {
|
|
||||||
if (intent.component != null && intent.component!!.shortClassName == ".HtcLinkifyDispatcherActivity") {
|
|
||||||
intent.component = null
|
|
||||||
}
|
|
||||||
super.startActivity(intent)
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
/* Catch the bad HTC implementation case */
|
|
||||||
super.startActivity(Intent.createChooser(intent, null))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.selection
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.view.SpecialModeView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity to manage special mode (ie: selection mode)
|
||||||
|
*/
|
||||||
|
abstract class SpecialModeActivity : StylishActivity() {
|
||||||
|
|
||||||
|
protected var mSelectionMode: Boolean = false
|
||||||
|
|
||||||
|
protected var mAutofillSelection: Boolean = false
|
||||||
|
|
||||||
|
private var specialModeView: SpecialModeView? = null
|
||||||
|
|
||||||
|
open fun onCancelSpecialMode() {
|
||||||
|
onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
mSelectionMode = EntrySelectionHelper.retrieveEntrySelectionModeFromIntent(intent)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
mAutofillSelection = AutofillHelper.retrieveAssistStructure(intent) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
val searchInfo: SearchInfo? = intent.getParcelableExtra(EntrySelectionHelper.KEY_SEARCH_INFO)
|
||||||
|
|
||||||
|
// To show the selection mode
|
||||||
|
specialModeView = findViewById(R.id.special_mode_view)
|
||||||
|
specialModeView?.apply {
|
||||||
|
// Populate title
|
||||||
|
val typeModeId = if (mAutofillSelection)
|
||||||
|
R.string.autofill
|
||||||
|
else
|
||||||
|
R.string.magic_keyboard_title
|
||||||
|
title = "${resources.getString(R.string.selection_mode)} (${getString(typeModeId)})"
|
||||||
|
// Populate subtitle
|
||||||
|
subtitle = searchInfo?.getName(resources)
|
||||||
|
|
||||||
|
// Show the toolbar or not
|
||||||
|
visible = mSelectionMode
|
||||||
|
|
||||||
|
// Add back listener
|
||||||
|
onCancelButtonClickListener = View.OnClickListener {
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create menu
|
||||||
|
menu.clear()
|
||||||
|
if (mAutofillSelection) {
|
||||||
|
menuInflater.inflate(R.menu.autofill, menu)
|
||||||
|
setOnMenuItemClickListener { menuItem ->
|
||||||
|
when (menuItem.itemId) {
|
||||||
|
R.id.menu_block_autofill -> {
|
||||||
|
blockAutofill(searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun blockAutofill(searchInfo: SearchInfo?) {
|
||||||
|
val webDomain = searchInfo?.webDomain
|
||||||
|
val applicationId = searchInfo?.applicationId
|
||||||
|
if (webDomain != null) {
|
||||||
|
PreferencesUtil.addWebDomainToBlocklist(this,
|
||||||
|
webDomain)
|
||||||
|
} else if (applicationId != null) {
|
||||||
|
PreferencesUtil.addApplicationIdToBlocklist(this,
|
||||||
|
applicationId)
|
||||||
|
}
|
||||||
|
onCancelSpecialMode()
|
||||||
|
Toast.makeText(this.applicationContext,
|
||||||
|
R.string.autofill_block_restart,
|
||||||
|
Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.stylish
|
package com.kunzisoft.keepass.activities.stylish
|
||||||
@@ -61,6 +61,7 @@ object Stylish {
|
|||||||
|
|
||||||
return when (themeString) {
|
return when (themeString) {
|
||||||
context.getString(R.string.list_style_name_night) -> R.style.KeepassDXStyle_Night
|
context.getString(R.string.list_style_name_night) -> R.style.KeepassDXStyle_Night
|
||||||
|
context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black
|
||||||
context.getString(R.string.list_style_name_dark) -> R.style.KeepassDXStyle_Dark
|
context.getString(R.string.list_style_name_dark) -> R.style.KeepassDXStyle_Dark
|
||||||
context.getString(R.string.list_style_name_blue) -> R.style.KeepassDXStyle_Blue
|
context.getString(R.string.list_style_name_blue) -> R.style.KeepassDXStyle_Blue
|
||||||
context.getString(R.string.list_style_name_red) -> R.style.KeepassDXStyle_Red
|
context.getString(R.string.list_style_name_red) -> R.style.KeepassDXStyle_Red
|
||||||
|
|||||||
@@ -1,38 +1,64 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.stylish
|
package com.kunzisoft.keepass.activities.stylish
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.annotation.StyleRes
|
import androidx.annotation.StyleRes
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.WindowManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
|
||||||
|
* appearing in the recent app preview
|
||||||
|
*/
|
||||||
abstract class StylishActivity : AppCompatActivity() {
|
abstract class StylishActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@StyleRes
|
@StyleRes
|
||||||
private var themeId: Int = 0
|
private var themeId: Int = 0
|
||||||
|
|
||||||
|
/* (non-Javadoc) Workaround for HTC Linkify issues
|
||||||
|
* @see android.app.Activity#startActivity(android.content.Intent)
|
||||||
|
*/
|
||||||
|
override fun startActivity(intent: Intent) {
|
||||||
|
try {
|
||||||
|
intent.component?.let {
|
||||||
|
if (it.shortClassName == ".HtcLinkifyDispatcherActivity")
|
||||||
|
intent.component = null
|
||||||
|
}
|
||||||
|
super.startActivity(intent)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
/* Catch the bad HTC implementation case */
|
||||||
|
super.startActivity(Intent.createChooser(intent, null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
this.themeId = Stylish.getThemeId(this)
|
this.themeId = Stylish.getThemeId(this)
|
||||||
setTheme(themeId)
|
setTheme(themeId)
|
||||||
|
|
||||||
|
// Several gingerbread devices have problems with FLAG_SECURE
|
||||||
|
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.stylish
|
package com.kunzisoft.keepass.activities.stylish
|
||||||
@@ -44,14 +44,21 @@ abstract class StylishFragment : Fragment() {
|
|||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
// To fix status bar color
|
// To fix status bar color
|
||||||
if (activity != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
val window = activity!!.window
|
val window = requireActivity().window
|
||||||
|
|
||||||
val attrColorPrimaryDark = intArrayOf(android.R.attr.colorPrimaryDark)
|
|
||||||
val taColorPrimaryDark = contextThemed?.theme?.obtainStyledAttributes(attrColorPrimaryDark)
|
|
||||||
val defaultColor = Color.BLACK
|
val defaultColor = Color.BLACK
|
||||||
window.statusBarColor = taColorPrimaryDark?.getColor(0, defaultColor) ?: defaultColor
|
|
||||||
taColorPrimaryDark?.recycle()
|
try {
|
||||||
|
val taStatusBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.statusBarColor))
|
||||||
|
window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor
|
||||||
|
taStatusBarColor?.recycle()
|
||||||
|
} catch (e: Exception) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor))
|
||||||
|
window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor
|
||||||
|
taNavigationBarColor?.recycle()
|
||||||
|
} catch (e: Exception) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onCreateView(inflater, container, savedInstanceState)
|
return super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.kunzisoft.keepass.view.collapse
|
||||||
|
|
||||||
|
abstract class AnimatedItemsAdapter<Item, T: RecyclerView.ViewHolder>(val context: Context)
|
||||||
|
: RecyclerView.Adapter<T>() {
|
||||||
|
|
||||||
|
protected val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
|
var itemsList: MutableList<Item> = ArrayList()
|
||||||
|
private set
|
||||||
|
|
||||||
|
var onDeleteButtonClickListener: ((item: Item)->Unit)? = null
|
||||||
|
private var mItemToRemove: Item? = null
|
||||||
|
|
||||||
|
var onListSizeChangedListener: ((previousSize: Int, newSize: Int)->Unit)? = null
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return itemsList.size
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun assignItems(items: List<Item>) {
|
||||||
|
val previousSize = itemsList.size
|
||||||
|
itemsList.apply {
|
||||||
|
clear()
|
||||||
|
addAll(items)
|
||||||
|
}
|
||||||
|
notifyDataSetChanged()
|
||||||
|
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun isEmpty(): Boolean {
|
||||||
|
return itemsList.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun contains(item: Item): Boolean {
|
||||||
|
return itemsList.contains(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun indexOf(item: Item): Int {
|
||||||
|
return itemsList.indexOf(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun putItem(item: Item) {
|
||||||
|
val previousSize = itemsList.size
|
||||||
|
if (itemsList.contains(item)) {
|
||||||
|
val index = itemsList.indexOf(item)
|
||||||
|
itemsList.removeAt(index)
|
||||||
|
itemsList.add(index, item)
|
||||||
|
notifyItemChanged(index)
|
||||||
|
} else {
|
||||||
|
itemsList.add(item)
|
||||||
|
notifyItemInserted(itemsList.indexOf(item))
|
||||||
|
}
|
||||||
|
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only replace [oldItem] by [newItem] if [oldItem] exists
|
||||||
|
*/
|
||||||
|
open fun replaceItem(oldItem: Item, newItem: Item) {
|
||||||
|
if (itemsList.contains(oldItem)) {
|
||||||
|
val index = itemsList.indexOf(oldItem)
|
||||||
|
itemsList.removeAt(index)
|
||||||
|
itemsList.add(index, newItem)
|
||||||
|
notifyItemChanged(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only remove [item] if doesn't exists
|
||||||
|
*/
|
||||||
|
open fun removeItem(item: Item) {
|
||||||
|
if (itemsList.contains(item)) {
|
||||||
|
mItemToRemove = item
|
||||||
|
notifyItemChanged(itemsList.indexOf(item))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun performDeletion(holder: T, item: Item): Boolean {
|
||||||
|
val effectivelyDeletionPerformed = mItemToRemove == item
|
||||||
|
if (effectivelyDeletionPerformed) {
|
||||||
|
holder.itemView.collapse(true) {
|
||||||
|
deleteItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return effectivelyDeletionPerformed
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun onBindDeleteButton(holder: T, deleteButton: View, item: Item, position: Int) {
|
||||||
|
deleteButton.apply {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
if (performDeletion(holder, item)) {
|
||||||
|
setOnClickListener(null)
|
||||||
|
} else {
|
||||||
|
setOnClickListener {
|
||||||
|
onDeleteButtonClickListener?.invoke(item)
|
||||||
|
mItemToRemove = item
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteItem(item: Item) {
|
||||||
|
val previousSize = itemsList.size
|
||||||
|
val position = itemsList.indexOf(item)
|
||||||
|
if (position >= 0) {
|
||||||
|
itemsList.removeAt(position)
|
||||||
|
notifyItemRemoved(position)
|
||||||
|
mItemToRemove = null
|
||||||
|
for (i in 0 until itemsList.size) {
|
||||||
|
notifyItemChanged(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
itemsList.clear()
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.TypedArray
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.text.format.Formatter
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.model.AttachmentState
|
||||||
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
|
|
||||||
|
|
||||||
|
class EntryAttachmentsItemsAdapter(context: Context)
|
||||||
|
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
|
||||||
|
|
||||||
|
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
|
||||||
|
|
||||||
|
private var mTitleColor: Int
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Get the primary text color of the theme
|
||||||
|
val typedValue = TypedValue()
|
||||||
|
context.theme.resolveAttribute(android.R.attr.textColorPrimary, typedValue, true)
|
||||||
|
val typedArray: TypedArray = context.obtainStyledAttributes(typedValue.data, intArrayOf(
|
||||||
|
android.R.attr.textColor))
|
||||||
|
mTitleColor = typedArray.getColor(0, -1)
|
||||||
|
typedArray.recycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryBinariesViewHolder {
|
||||||
|
return EntryBinariesViewHolder(inflater.inflate(R.layout.item_attachment, parent, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: EntryBinariesViewHolder, position: Int) {
|
||||||
|
val entryAttachmentState = itemsList[position]
|
||||||
|
|
||||||
|
holder.itemView.visibility = View.VISIBLE
|
||||||
|
holder.binaryFileBroken.apply {
|
||||||
|
setColorFilter(Color.RED)
|
||||||
|
visibility = if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
holder.binaryFileTitle.text = entryAttachmentState.attachment.name
|
||||||
|
if (entryAttachmentState.attachment.binaryAttachment.isCorrupted) {
|
||||||
|
holder.binaryFileTitle.setTextColor(Color.RED)
|
||||||
|
} else {
|
||||||
|
holder.binaryFileTitle.setTextColor(mTitleColor)
|
||||||
|
}
|
||||||
|
holder.binaryFileSize.text = Formatter.formatFileSize(context,
|
||||||
|
entryAttachmentState.attachment.binaryAttachment.length())
|
||||||
|
holder.binaryFileCompression.apply {
|
||||||
|
if (entryAttachmentState.attachment.binaryAttachment.isCompressed) {
|
||||||
|
text = CompressionAlgorithm.GZip.getName(context.resources)
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
text = ""
|
||||||
|
visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
when (entryAttachmentState.streamDirection) {
|
||||||
|
StreamDirection.UPLOAD -> {
|
||||||
|
holder.binaryFileProgressIcon.isActivated = true
|
||||||
|
when (entryAttachmentState.downloadState) {
|
||||||
|
AttachmentState.START,
|
||||||
|
AttachmentState.IN_PROGRESS -> {
|
||||||
|
holder.binaryFileProgressContainer.visibility = View.VISIBLE
|
||||||
|
holder.binaryFileProgress.apply {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
progress = entryAttachmentState.downloadProgression
|
||||||
|
}
|
||||||
|
holder.binaryFileDeleteButton.apply {
|
||||||
|
visibility = View.GONE
|
||||||
|
setOnClickListener(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AttachmentState.NULL,
|
||||||
|
AttachmentState.ERROR,
|
||||||
|
AttachmentState.COMPLETE -> {
|
||||||
|
holder.binaryFileProgressContainer.visibility = View.GONE
|
||||||
|
holder.binaryFileProgress.visibility = View.GONE
|
||||||
|
holder.binaryFileDeleteButton.apply {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
onBindDeleteButton(holder, this, entryAttachmentState, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
holder.itemView.setOnClickListener(null)
|
||||||
|
}
|
||||||
|
StreamDirection.DOWNLOAD -> {
|
||||||
|
holder.binaryFileProgressIcon.isActivated = false
|
||||||
|
holder.binaryFileProgressContainer.visibility = View.VISIBLE
|
||||||
|
holder.binaryFileDeleteButton.visibility = View.GONE
|
||||||
|
holder.binaryFileProgress.apply {
|
||||||
|
visibility = when (entryAttachmentState.downloadState) {
|
||||||
|
AttachmentState.NULL, AttachmentState.COMPLETE, AttachmentState.ERROR -> View.GONE
|
||||||
|
AttachmentState.START, AttachmentState.IN_PROGRESS -> View.VISIBLE
|
||||||
|
}
|
||||||
|
progress = entryAttachmentState.downloadProgression
|
||||||
|
}
|
||||||
|
holder.itemView.setOnClickListener {
|
||||||
|
onItemClickListener?.invoke(entryAttachmentState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
|
var binaryFileBroken: ImageView = itemView.findViewById(R.id.item_attachment_broken)
|
||||||
|
var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title)
|
||||||
|
var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size)
|
||||||
|
var binaryFileCompression: TextView = itemView.findViewById(R.id.item_attachment_compression)
|
||||||
|
var binaryFileProgressContainer: View = itemView.findViewById(R.id.item_attachment_progress_container)
|
||||||
|
var binaryFileProgressIcon: ImageView = itemView.findViewById(R.id.item_attachment_icon)
|
||||||
|
var binaryFileProgress: ProgressBar = itemView.findViewById(R.id.item_attachment_progress)
|
||||||
|
var binaryFileDeleteButton: View = itemView.findViewById(R.id.item_attachment_delete_button)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
package com.kunzisoft.keepass.adapters
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -7,13 +26,13 @@ import android.view.ViewGroup
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.EntryVersioned
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
|
|
||||||
class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHistoryAdapter.EntryHistoryViewHolder>() {
|
class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHistoryAdapter.EntryHistoryViewHolder>() {
|
||||||
|
|
||||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
var entryHistoryList: MutableList<EntryVersioned> = ArrayList()
|
var entryHistoryList: MutableList<Entry> = ArrayList()
|
||||||
var onItemClickListener: ((item: EntryVersioned, position: Int)->Unit)? = null
|
var onItemClickListener: ((item: Entry, position: Int)->Unit)? = null
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryHistoryViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryHistoryViewHolder {
|
||||||
return EntryHistoryViewHolder(inflater.inflate(R.layout.item_list_entry_history, parent, false))
|
return EntryHistoryViewHolder(inflater.inflate(R.layout.item_list_entry_history, parent, false))
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
package com.kunzisoft.keepass.adapters
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -15,10 +34,9 @@ import java.util.ArrayList
|
|||||||
class FieldsAdapter(context: Context) : RecyclerView.Adapter<FieldsAdapter.FieldViewHolder>() {
|
class FieldsAdapter(context: Context) : RecyclerView.Adapter<FieldsAdapter.FieldViewHolder>() {
|
||||||
|
|
||||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
var fields: MutableList<Field> = ArrayList()
|
private var fields: MutableList<Field> = ArrayList()
|
||||||
var onItemClickListener: OnItemClickListener? = null
|
var onItemClickListener: OnItemClickListener? = null
|
||||||
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FieldViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FieldViewHolder {
|
||||||
val view = inflater.inflate(R.layout.keyboard_popup_fields_item, parent, false)
|
val view = inflater.inflate(R.layout.keyboard_popup_fields_item, parent, false)
|
||||||
return FieldViewHolder(view)
|
return FieldViewHolder(view)
|
||||||
@@ -34,6 +52,11 @@ class FieldsAdapter(context: Context) : RecyclerView.Adapter<FieldsAdapter.Field
|
|||||||
return fields.size
|
return fields.size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setFields(fieldsToAdd: List<Field>) {
|
||||||
|
fields.clear()
|
||||||
|
fields.addAll(fieldsToAdd)
|
||||||
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
fields.clear()
|
fields.clear()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,54 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.adapters
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.PorterDuff
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.*
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.*
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.ViewSwitcher
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryEntity
|
import com.kunzisoft.keepass.model.DatabaseFile
|
||||||
import com.kunzisoft.keepass.utils.FileDatabaseInfo
|
import com.kunzisoft.keepass.view.collapse
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.view.expand
|
||||||
|
|
||||||
class FileDatabaseHistoryAdapter(private val context: Context)
|
class FileDatabaseHistoryAdapter(context: Context)
|
||||||
: RecyclerView.Adapter<FileDatabaseHistoryAdapter.FileDatabaseHistoryViewHolder>() {
|
: RecyclerView.Adapter<FileDatabaseHistoryAdapter.FileDatabaseHistoryViewHolder>() {
|
||||||
|
|
||||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
private var fileItemOpenListener: ((FileDatabaseHistoryEntity)->Unit)? = null
|
private var defaultDatabaseListener: ((DatabaseFile?) -> Unit)? = null
|
||||||
private var fileSelectClearListener: ((FileDatabaseHistoryEntity)->Boolean)? = null
|
private var fileItemOpenListener: ((DatabaseFile)->Unit)? = null
|
||||||
private var saveAliasListener: ((FileDatabaseHistoryEntity)->Unit)? = null
|
private var fileSelectClearListener: ((DatabaseFile)->Boolean)? = null
|
||||||
|
private var saveAliasListener: ((DatabaseFile)->Unit)? = null
|
||||||
|
|
||||||
private val listDatabaseFiles = ArrayList<FileDatabaseHistoryEntity>()
|
private val listDatabaseFiles = ArrayList<DatabaseFile>()
|
||||||
|
|
||||||
private var mExpandedPosition = -1
|
private var mDefaultDatabaseFile: DatabaseFile? = null
|
||||||
private var mPreviousExpandedPosition = -1
|
private var mExpandedDatabaseFile: DatabaseFile? = null
|
||||||
|
private var mPreviousExpandedDatabaseFile: DatabaseFile? = null
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private val defaultColor: Int
|
private val defaultColor: Int
|
||||||
@@ -61,47 +65,74 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileDatabaseHistoryViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileDatabaseHistoryViewHolder {
|
||||||
val view = inflater.inflate(R.layout.item_file_row, parent, false)
|
val view = inflater.inflate(R.layout.item_file_info, parent, false)
|
||||||
return FileDatabaseHistoryViewHolder(view)
|
return FileDatabaseHistoryViewHolder(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) {
|
||||||
// Get info from position
|
// Get info from position
|
||||||
val fileHistoryEntity = listDatabaseFiles[position]
|
val databaseFile = listDatabaseFiles[position]
|
||||||
val fileDatabaseInfo = FileDatabaseInfo(context, fileHistoryEntity.databaseUri)
|
|
||||||
|
|
||||||
// Click item to open file
|
// Click item to open file
|
||||||
if (fileItemOpenListener != null)
|
holder.fileContainer.setOnClickListener {
|
||||||
holder.fileContainer.setOnClickListener {
|
fileItemOpenListener?.invoke(databaseFile)
|
||||||
fileItemOpenListener?.invoke(fileHistoryEntity)
|
}
|
||||||
|
|
||||||
|
// Default database
|
||||||
|
holder.defaultFileButton.apply {
|
||||||
|
this.isChecked = mDefaultDatabaseFile == databaseFile
|
||||||
|
setOnClickListener {
|
||||||
|
defaultDatabaseListener?.invoke(if (isChecked) databaseFile else null)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// File alias
|
// File alias
|
||||||
holder.fileAlias.text = fileDatabaseInfo.retrieveDatabaseAlias(fileHistoryEntity.databaseAlias)
|
holder.fileAlias.text = databaseFile.databaseAlias
|
||||||
|
|
||||||
// File path
|
// File path
|
||||||
holder.filePath.text = UriUtil.decode(fileDatabaseInfo.fileUri?.toString())
|
holder.filePath.text = databaseFile.databaseDecodedPath
|
||||||
|
|
||||||
holder.filePreciseInfoContainer.visibility = if (fileDatabaseInfo.found()) {
|
if (databaseFile.databaseFileExists) {
|
||||||
// Modification
|
holder.fileInformationButton.clearColorFilter()
|
||||||
holder.fileModification.text = fileDatabaseInfo.getModificationString()
|
} else {
|
||||||
// Size
|
holder.fileInformationButton.setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY)
|
||||||
holder.fileSize.text = fileDatabaseInfo.getSizeString()
|
}
|
||||||
|
|
||||||
View.VISIBLE
|
// Modification
|
||||||
} else
|
databaseFile.databaseLastModified?.let {
|
||||||
View.GONE
|
holder.fileModification.text = it
|
||||||
|
holder.fileModificationContainer.visibility = View.VISIBLE
|
||||||
|
} ?: run {
|
||||||
|
holder.fileModificationContainer.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size
|
||||||
|
databaseFile.databaseSize?.let {
|
||||||
|
holder.fileSize.text = it
|
||||||
|
holder.fileSize.visibility = View.VISIBLE
|
||||||
|
} ?: run {
|
||||||
|
holder.fileSize.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
// Click on information
|
// Click on information
|
||||||
val isExpanded = position == mExpandedPosition
|
val isExpanded = databaseFile == mExpandedDatabaseFile
|
||||||
//This line hides or shows the layout in question
|
// Hides or shows info
|
||||||
holder.fileExpandContainer.visibility = if (isExpanded) View.VISIBLE else View.GONE
|
holder.fileExpandContainer.apply {
|
||||||
|
if (isExpanded) {
|
||||||
|
if (visibility != View.VISIBLE) {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
expand(true, resources.getDimensionPixelSize(R.dimen.item_file_info_height))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
collapse(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Save alias modification
|
// Save alias modification
|
||||||
holder.fileAliasCloseButton.setOnClickListener {
|
holder.fileAliasCloseButton.setOnClickListener {
|
||||||
// Change the alias
|
// Change the alias
|
||||||
fileHistoryEntity.databaseAlias = holder.fileAliasEdit.text.toString()
|
databaseFile.databaseAlias = holder.fileAliasEdit.text.toString()
|
||||||
saveAliasListener?.invoke(fileHistoryEntity)
|
saveAliasListener?.invoke(databaseFile)
|
||||||
|
|
||||||
// Finish save mode
|
// Finish save mode
|
||||||
holder.fileMainSwitcher.showPrevious()
|
holder.fileMainSwitcher.showPrevious()
|
||||||
@@ -116,20 +147,22 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
holder.fileDeleteButton.setOnClickListener {
|
holder.fileDeleteButton.setOnClickListener {
|
||||||
fileSelectClearListener?.invoke(fileHistoryEntity)
|
fileSelectClearListener?.invoke(databaseFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isExpanded) {
|
if (isExpanded) {
|
||||||
mPreviousExpandedPosition = position
|
mPreviousExpandedDatabaseFile = databaseFile
|
||||||
}
|
}
|
||||||
|
holder.fileInformationButton.apply {
|
||||||
holder.fileInformation.setOnClickListener {
|
animate().rotation(if (isExpanded) 180F else 0F).start()
|
||||||
mExpandedPosition = if (isExpanded) -1 else position
|
setOnClickListener {
|
||||||
|
mExpandedDatabaseFile = if (isExpanded) null else databaseFile
|
||||||
// Notify change
|
// Notify change
|
||||||
if (mPreviousExpandedPosition < itemCount)
|
val previousExpandedPosition = listDatabaseFiles.indexOf(mPreviousExpandedDatabaseFile)
|
||||||
notifyItemChanged(mPreviousExpandedPosition)
|
notifyItemChanged(previousExpandedPosition)
|
||||||
notifyItemChanged(position)
|
val expandedPosition = listDatabaseFiles.indexOf(mExpandedDatabaseFile)
|
||||||
|
notifyItemChanged(expandedPosition)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh View / Close alias modification if not contains fileAlias
|
// Refresh View / Close alias modification if not contains fileAlias
|
||||||
@@ -142,33 +175,72 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
|||||||
return listDatabaseFiles.size
|
return listDatabaseFiles.size
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<FileDatabaseHistoryEntity>) {
|
fun clearDatabaseFileHistoryList() {
|
||||||
listDatabaseFiles.clear()
|
listDatabaseFiles.clear()
|
||||||
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: FileDatabaseHistoryEntity) {
|
fun addDatabaseFileHistory(fileDatabaseHistoryToAdd: DatabaseFile) {
|
||||||
listDatabaseFiles.remove(fileDatabaseHistoryToDelete)
|
listDatabaseFiles.add(0, fileDatabaseHistoryToAdd)
|
||||||
|
notifyItemInserted(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setOnFileDatabaseHistoryOpenListener(listener : ((FileDatabaseHistoryEntity)->Unit)?) {
|
fun updateDatabaseFileHistory(fileDatabaseHistoryToUpdate: DatabaseFile) {
|
||||||
|
val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToUpdate)
|
||||||
|
if (listDatabaseFiles.remove(fileDatabaseHistoryToUpdate)) {
|
||||||
|
listDatabaseFiles.add(index, fileDatabaseHistoryToUpdate)
|
||||||
|
notifyItemChanged(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: DatabaseFile) {
|
||||||
|
val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToDelete)
|
||||||
|
if (listDatabaseFiles.remove(fileDatabaseHistoryToDelete)) {
|
||||||
|
notifyItemRemoved(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun replaceAllDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List<DatabaseFile>) {
|
||||||
|
if (listDatabaseFiles.isEmpty()) {
|
||||||
|
listFileDatabaseHistoryToAdd.forEach {
|
||||||
|
listDatabaseFiles.add(it)
|
||||||
|
notifyItemInserted(listDatabaseFiles.size)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
listDatabaseFiles.clear()
|
||||||
|
listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDefaultDatabase(databaseUri: Uri?) {
|
||||||
|
val defaultDatabaseFile = listDatabaseFiles.firstOrNull { it.databaseUri == databaseUri }
|
||||||
|
mDefaultDatabaseFile = defaultDatabaseFile
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOnDefaultDatabaseListener(listener: ((DatabaseFile?) -> Unit)?) {
|
||||||
|
this.defaultDatabaseListener = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOnFileDatabaseHistoryOpenListener(listener : ((DatabaseFile)->Unit)?) {
|
||||||
this.fileItemOpenListener = listener
|
this.fileItemOpenListener = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setOnFileDatabaseHistoryDeleteListener(listener : ((FileDatabaseHistoryEntity)->Boolean)?) {
|
fun setOnFileDatabaseHistoryDeleteListener(listener : ((DatabaseFile)->Boolean)?) {
|
||||||
this.fileSelectClearListener = listener
|
this.fileSelectClearListener = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setOnSaveAliasListener(listener : ((FileDatabaseHistoryEntity)->Unit)?) {
|
fun setOnSaveAliasListener(listener : ((DatabaseFile)->Unit)?) {
|
||||||
this.saveAliasListener = listener
|
this.saveAliasListener = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
var fileContainer: ViewGroup = itemView.findViewById(R.id.file_container_basic_info)
|
var fileContainer: ViewGroup = itemView.findViewById(R.id.file_container_basic_info)
|
||||||
|
|
||||||
|
var defaultFileButton: CompoundButton = itemView.findViewById(R.id.default_file_button)
|
||||||
var fileAlias: TextView = itemView.findViewById(R.id.file_alias)
|
var fileAlias: TextView = itemView.findViewById(R.id.file_alias)
|
||||||
var fileInformation: ImageView = itemView.findViewById(R.id.file_information)
|
var fileInformationButton: ImageView = itemView.findViewById(R.id.file_information_button)
|
||||||
|
|
||||||
var fileMainSwitcher: ViewSwitcher = itemView.findViewById(R.id.file_main_switcher)
|
var fileMainSwitcher: ViewSwitcher = itemView.findViewById(R.id.file_main_switcher)
|
||||||
var fileAliasEdit: EditText = itemView.findViewById(R.id.file_alias_edit)
|
var fileAliasEdit: EditText = itemView.findViewById(R.id.file_alias_edit)
|
||||||
@@ -178,7 +250,7 @@ class FileDatabaseHistoryAdapter(private val context: Context)
|
|||||||
var fileModifyButton: ImageView = itemView.findViewById(R.id.file_modify_button)
|
var fileModifyButton: ImageView = itemView.findViewById(R.id.file_modify_button)
|
||||||
var fileDeleteButton: ImageView = itemView.findViewById(R.id.file_delete_button)
|
var fileDeleteButton: ImageView = itemView.findViewById(R.id.file_delete_button)
|
||||||
var filePath: TextView = itemView.findViewById(R.id.file_path)
|
var filePath: TextView = itemView.findViewById(R.id.file_path)
|
||||||
var filePreciseInfoContainer: ViewGroup = itemView.findViewById(R.id.file_precise_info_container)
|
var fileModificationContainer: ViewGroup = itemView.findViewById(R.id.file_modification_container)
|
||||||
var fileModification: TextView = itemView.findViewById(R.id.file_modification)
|
var fileModification: TextView = itemView.findViewById(R.id.file_modification)
|
||||||
var fileSize: TextView = itemView.findViewById(R.id.file_size)
|
var fileSize: TextView = itemView.findViewById(R.id.file_size)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +1,84 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.adapters
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Paint
|
|
||||||
import android.util.Log
|
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import androidx.annotation.ColorInt
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SortedList
|
import androidx.recyclerview.widget.SortedList
|
||||||
import androidx.recyclerview.widget.SortedListAdapterCallback
|
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.SortNodeEnum
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.*
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
|
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||||
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
|
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
||||||
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.view.setTextSize
|
||||||
|
import com.kunzisoft.keepass.view.strikeOut
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class NodeAdapter
|
|
||||||
/**
|
/**
|
||||||
* Create node list adapter with contextMenu or not
|
* Create node list adapter with contextMenu or not
|
||||||
* @param context Context to use
|
* @param context Context to use
|
||||||
*/
|
*/
|
||||||
(private val context: Context)
|
class NodeAdapter (private val context: Context)
|
||||||
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
|
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
|
||||||
|
|
||||||
private val nodeSortedList: SortedList<NodeVersioned>
|
private var nodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
|
||||||
|
private val nodeSortedListCallback: NodeSortedListCallback
|
||||||
|
private val nodeSortedList: SortedList<Node>
|
||||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
|
|
||||||
private var calculateViewTypeTextSize = Array(2) { true} // number of view type
|
private var calculateViewTypeTextSize = Array(2) { true} // number of view type
|
||||||
private var textSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
private var textSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
||||||
private var prefTextSize: Float = 0F
|
private var prefSizeMultiplier: Float = 0F
|
||||||
private var subtextSize: Float = 0F
|
private var subtextDefaultDimension: Float = 0F
|
||||||
private var infoTextSize: Float = 0F
|
private var infoTextDefaultDimension: Float = 0F
|
||||||
private var numberChildrenTextSize: Float = 0F
|
private var numberChildrenTextDefaultDimension: Float = 0F
|
||||||
private var iconSize: Float = 0F
|
private var iconDefaultDimension: Float = 0F
|
||||||
private var listSort: SortNodeEnum = SortNodeEnum.DB
|
|
||||||
private var ascendingSort: Boolean = true
|
|
||||||
private var groupsBeforeSort: Boolean = true
|
|
||||||
private var recycleBinBottomSort: Boolean = true
|
|
||||||
private var showUserNames: Boolean = true
|
private var showUserNames: Boolean = true
|
||||||
private var showNumberEntries: Boolean = true
|
private var showNumberEntries: Boolean = true
|
||||||
|
private var entryFilters = arrayOf<Group.ChildFilter>()
|
||||||
|
|
||||||
private var actionNodesList = LinkedList<NodeVersioned>()
|
private var actionNodesList = LinkedList<Node>()
|
||||||
private var nodeClickCallback: NodeClickCallback? = null
|
private var nodeClickCallback: NodeClickCallback? = null
|
||||||
|
|
||||||
private val mDatabase: Database
|
private val mDatabase: Database
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
private val contentSelectionColor: Int
|
||||||
|
@ColorInt
|
||||||
private val iconGroupColor: Int
|
private val iconGroupColor: Int
|
||||||
|
@ColorInt
|
||||||
private val iconEntryColor: Int
|
private val iconEntryColor: Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,27 +89,23 @@ class NodeAdapter
|
|||||||
get() = nodeSortedList.size() <= 0
|
get() = nodeSortedList.size() <= 0
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
this.infoTextDefaultDimension = context.resources.getDimension(R.dimen.list_medium_size_default)
|
||||||
|
this.subtextDefaultDimension = context.resources.getDimension(R.dimen.list_small_size_default)
|
||||||
|
this.numberChildrenTextDefaultDimension = context.resources.getDimension(R.dimen.list_tiny_size_default)
|
||||||
|
this.iconDefaultDimension = context.resources.getDimension(R.dimen.list_icon_size_default)
|
||||||
|
|
||||||
assignPreferences()
|
assignPreferences()
|
||||||
|
|
||||||
this.nodeSortedList = SortedList(NodeVersioned::class.java, object : SortedListAdapterCallback<NodeVersioned>(this) {
|
this.nodeSortedListCallback = NodeSortedListCallback()
|
||||||
override fun compare(item1: NodeVersioned, item2: NodeVersioned): Int {
|
this.nodeSortedList = SortedList(Node::class.java, nodeSortedListCallback)
|
||||||
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
|
// Database
|
||||||
this.mDatabase = Database.getInstance()
|
this.mDatabase = Database.getInstance()
|
||||||
|
|
||||||
|
// Color of content selection
|
||||||
|
val taContentSelectionColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
||||||
|
this.contentSelectionColor = taContentSelectionColor.getColor(0, Color.WHITE)
|
||||||
|
taContentSelectionColor.recycle()
|
||||||
// Retrieve the color to tint the icon
|
// Retrieve the color to tint the icon
|
||||||
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
||||||
this.iconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
|
this.iconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
|
||||||
@@ -112,20 +116,23 @@ class NodeAdapter
|
|||||||
taTextColor.recycle()
|
taTextColor.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assignPreferences() {
|
fun assignPreferences() {
|
||||||
this.prefTextSize = PreferencesUtil.getListTextSize(context)
|
this.prefSizeMultiplier = PreferencesUtil.getListTextSize(context)
|
||||||
this.infoTextSize = context.resources.getDimension(R.dimen.list_medium_size_default) * prefTextSize
|
|
||||||
this.subtextSize = context.resources.getDimension(R.dimen.list_small_size_default) * prefTextSize
|
notifyChangeSort(
|
||||||
this.numberChildrenTextSize = context.resources.getDimension(R.dimen.list_tiny_size_default) * prefTextSize
|
PreferencesUtil.getListSort(context),
|
||||||
this.iconSize = context.resources.getDimension(R.dimen.list_icon_size_default) * prefTextSize
|
SortNodeEnum.SortNodeParameters(
|
||||||
|
PreferencesUtil.getAscendingSort(context),
|
||||||
|
PreferencesUtil.getGroupsBeforeSort(context),
|
||||||
|
PreferencesUtil.getRecycleBinBottomSort(context)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
this.listSort = PreferencesUtil.getListSort(context)
|
|
||||||
this.ascendingSort = PreferencesUtil.getAscendingSort(context)
|
|
||||||
this.groupsBeforeSort = PreferencesUtil.getGroupsBeforeSort(context)
|
|
||||||
this.recycleBinBottomSort = PreferencesUtil.getRecycleBinBottomSort(context)
|
|
||||||
this.showUserNames = PreferencesUtil.showUsernamesListEntries(context)
|
this.showUserNames = PreferencesUtil.showUsernamesListEntries(context)
|
||||||
this.showNumberEntries = PreferencesUtil.showNumberEntries(context)
|
this.showNumberEntries = PreferencesUtil.showNumberEntries(context)
|
||||||
|
|
||||||
|
this.entryFilters = Group.ChildFilter.getDefaults(context)
|
||||||
|
|
||||||
// Reinit textSize for all view type
|
// Reinit textSize for all view type
|
||||||
calculateViewTypeTextSize.forEachIndexed { index, _ -> calculateViewTypeTextSize[index] = true }
|
calculateViewTypeTextSize.forEachIndexed { index, _ -> calculateViewTypeTextSize[index] = true }
|
||||||
}
|
}
|
||||||
@@ -133,19 +140,28 @@ class NodeAdapter
|
|||||||
/**
|
/**
|
||||||
* Rebuild the list by clear and build children from the group
|
* Rebuild the list by clear and build children from the group
|
||||||
*/
|
*/
|
||||||
fun rebuildList(group: GroupVersioned) {
|
fun rebuildList(group: Group) {
|
||||||
this.nodeSortedList.clear()
|
|
||||||
assignPreferences()
|
assignPreferences()
|
||||||
try {
|
nodeSortedList.replaceAll(group.getFilteredChildren(entryFilters))
|
||||||
this.nodeSortedList.addAll(group.getChildren())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Can't add node elements to the list", e)
|
|
||||||
Toast.makeText(context, "Can't add node elements to the list : " + e.message, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun contains(node: NodeVersioned): Boolean {
|
private inner class NodeSortedListCallback: SortedListAdapterCallback<Node>(this) {
|
||||||
|
override fun compare(item1: Node, item2: Node): Int {
|
||||||
|
return nodeComparator!!.compare(item1, item2)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
|
||||||
|
return oldItem.type == newItem.type
|
||||||
|
&& oldItem.title == newItem.title
|
||||||
|
&& oldItem.icon == newItem.icon
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areItemsTheSame(item1: Node, item2: Node): Boolean {
|
||||||
|
return item1 == item2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun contains(node: Node): Boolean {
|
||||||
return nodeSortedList.indexOf(node) != SortedList.INVALID_POSITION
|
return nodeSortedList.indexOf(node) != SortedList.INVALID_POSITION
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +169,7 @@ class NodeAdapter
|
|||||||
* Add a node to the list
|
* Add a node to the list
|
||||||
* @param node Node to add
|
* @param node Node to add
|
||||||
*/
|
*/
|
||||||
fun addNode(node: NodeVersioned) {
|
fun addNode(node: Node) {
|
||||||
nodeSortedList.add(node)
|
nodeSortedList.add(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +177,7 @@ class NodeAdapter
|
|||||||
* Add nodes to the list
|
* Add nodes to the list
|
||||||
* @param nodes Nodes to add
|
* @param nodes Nodes to add
|
||||||
*/
|
*/
|
||||||
fun addNodes(nodes: List<NodeVersioned>) {
|
fun addNodes(nodes: List<Node>) {
|
||||||
nodeSortedList.addAll(nodes)
|
nodeSortedList.addAll(nodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +185,7 @@ class NodeAdapter
|
|||||||
* Remove a node in the list
|
* Remove a node in the list
|
||||||
* @param node Node to delete
|
* @param node Node to delete
|
||||||
*/
|
*/
|
||||||
fun removeNode(node: NodeVersioned) {
|
fun removeNode(node: Node) {
|
||||||
nodeSortedList.remove(node)
|
nodeSortedList.remove(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +193,7 @@ class NodeAdapter
|
|||||||
* Remove nodes in the list
|
* Remove nodes in the list
|
||||||
* @param nodes Nodes to delete
|
* @param nodes Nodes to delete
|
||||||
*/
|
*/
|
||||||
fun removeNodes(nodes: List<NodeVersioned>) {
|
fun removeNodes(nodes: List<Node>) {
|
||||||
nodes.forEach { node ->
|
nodes.forEach { node ->
|
||||||
nodeSortedList.remove(node)
|
nodeSortedList.remove(node)
|
||||||
}
|
}
|
||||||
@@ -209,7 +225,7 @@ class NodeAdapter
|
|||||||
* @param oldNode Node before the update
|
* @param oldNode Node before the update
|
||||||
* @param newNode Node after the update
|
* @param newNode Node after the update
|
||||||
*/
|
*/
|
||||||
fun updateNode(oldNode: NodeVersioned, newNode: NodeVersioned) {
|
fun updateNode(oldNode: Node, newNode: Node) {
|
||||||
nodeSortedList.beginBatchedUpdates()
|
nodeSortedList.beginBatchedUpdates()
|
||||||
nodeSortedList.remove(oldNode)
|
nodeSortedList.remove(oldNode)
|
||||||
nodeSortedList.add(newNode)
|
nodeSortedList.add(newNode)
|
||||||
@@ -221,7 +237,7 @@ class NodeAdapter
|
|||||||
* @param oldNodes Nodes before the update
|
* @param oldNodes Nodes before the update
|
||||||
* @param newNodes Node after the update
|
* @param newNodes Node after the update
|
||||||
*/
|
*/
|
||||||
fun updateNodes(oldNodes: List<NodeVersioned>, newNodes: List<NodeVersioned>) {
|
fun updateNodes(oldNodes: List<Node>, newNodes: List<Node>) {
|
||||||
nodeSortedList.beginBatchedUpdates()
|
nodeSortedList.beginBatchedUpdates()
|
||||||
oldNodes.forEach { oldNode ->
|
oldNodes.forEach { oldNode ->
|
||||||
nodeSortedList.remove(oldNode)
|
nodeSortedList.remove(oldNode)
|
||||||
@@ -230,11 +246,11 @@ class NodeAdapter
|
|||||||
nodeSortedList.endBatchedUpdates()
|
nodeSortedList.endBatchedUpdates()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun notifyNodeChanged(node: NodeVersioned) {
|
fun notifyNodeChanged(node: Node) {
|
||||||
notifyItemChanged(nodeSortedList.indexOf(node))
|
notifyItemChanged(nodeSortedList.indexOf(node))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setActionNodes(actionNodes: List<NodeVersioned>) {
|
fun setActionNodes(actionNodes: List<Node>) {
|
||||||
this.actionNodesList.apply {
|
this.actionNodesList.apply {
|
||||||
clear()
|
clear()
|
||||||
addAll(actionNodes)
|
addAll(actionNodes)
|
||||||
@@ -253,10 +269,9 @@ class NodeAdapter
|
|||||||
/**
|
/**
|
||||||
* Notify a change sort of the list
|
* Notify a change sort of the list
|
||||||
*/
|
*/
|
||||||
fun notifyChangeSort(sortNodeEnum: SortNodeEnum, ascending: Boolean, groupsBefore: Boolean) {
|
fun notifyChangeSort(sortNodeEnum: SortNodeEnum,
|
||||||
this.listSort = sortNodeEnum
|
sortNodeParameters: SortNodeEnum.SortNodeParameters) {
|
||||||
this.ascendingSort = ascending
|
this.nodeComparator = sortNodeEnum.getNodeComparator(sortNodeParameters)
|
||||||
this.groupsBeforeSort = groupsBefore
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getItemViewType(position: Int): Int {
|
||||||
@@ -274,28 +289,76 @@ class NodeAdapter
|
|||||||
|
|
||||||
override fun onBindViewHolder(holder: NodeViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: NodeViewHolder, position: Int) {
|
||||||
val subNode = nodeSortedList.get(position)
|
val subNode = nodeSortedList.get(position)
|
||||||
|
|
||||||
|
// Node selection
|
||||||
|
holder.container.isSelected = actionNodesList.contains(subNode)
|
||||||
|
|
||||||
// Assign image
|
// Assign image
|
||||||
val iconColor = when (subNode.type) {
|
val iconColor = if (holder.container.isSelected)
|
||||||
|
contentSelectionColor
|
||||||
|
else when (subNode.type) {
|
||||||
Type.GROUP -> iconGroupColor
|
Type.GROUP -> iconGroupColor
|
||||||
Type.ENTRY -> iconEntryColor
|
Type.ENTRY -> iconEntryColor
|
||||||
}
|
}
|
||||||
|
holder.imageIdentifier?.setColorFilter(iconColor)
|
||||||
holder.icon.apply {
|
holder.icon.apply {
|
||||||
assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
|
assignDatabaseIcon(mDatabase.drawFactory, subNode.icon, iconColor)
|
||||||
// Relative size of the icon
|
// Relative size of the icon
|
||||||
layoutParams?.apply {
|
layoutParams?.apply {
|
||||||
height = iconSize.toInt()
|
height = (iconDefaultDimension * prefSizeMultiplier).toInt()
|
||||||
width = iconSize.toInt()
|
width = (iconDefaultDimension * prefSizeMultiplier).toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign text
|
// Assign text
|
||||||
holder.text.apply {
|
holder.text.apply {
|
||||||
text = subNode.title
|
text = subNode.title
|
||||||
setTextSize(textSizeUnit, infoTextSize)
|
setTextSize(textSizeUnit, infoTextDefaultDimension, prefSizeMultiplier)
|
||||||
paintFlags = if (subNode.isCurrentlyExpires)
|
strikeOut(subNode.isCurrentlyExpires)
|
||||||
paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
|
|
||||||
else
|
|
||||||
paintFlags and Paint.STRIKE_THRU_TEXT_FLAG
|
|
||||||
}
|
}
|
||||||
|
// Add subText with username
|
||||||
|
holder.subText.apply {
|
||||||
|
text = ""
|
||||||
|
strikeOut(subNode.isCurrentlyExpires)
|
||||||
|
visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specific elements for entry
|
||||||
|
if (subNode.type == Type.ENTRY) {
|
||||||
|
val entry = subNode as Entry
|
||||||
|
mDatabase.startManageEntry(entry)
|
||||||
|
|
||||||
|
holder.text.text = entry.getVisualTitle()
|
||||||
|
holder.subText.apply {
|
||||||
|
val username = entry.username
|
||||||
|
if (showUserNames && username.isNotEmpty()) {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
text = username
|
||||||
|
setTextSize(textSizeUnit, subtextDefaultDimension, prefSizeMultiplier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.attachmentIcon?.visibility =
|
||||||
|
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
mDatabase.stopManageEntry(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add number of entries in groups
|
||||||
|
if (subNode.type == Type.GROUP) {
|
||||||
|
if (showNumberEntries) {
|
||||||
|
holder.numberChildren?.apply {
|
||||||
|
text = (subNode as Group)
|
||||||
|
.getNumberOfChildEntries(entryFilters)
|
||||||
|
.toString()
|
||||||
|
setTextSize(textSizeUnit, numberChildrenTextDefaultDimension, prefSizeMultiplier)
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
holder.numberChildren?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Assign click
|
// Assign click
|
||||||
holder.container.setOnClickListener {
|
holder.container.setOnClickListener {
|
||||||
nodeClickCallback?.onNodeClick(subNode)
|
nodeClickCallback?.onNodeClick(subNode)
|
||||||
@@ -303,49 +366,8 @@ class NodeAdapter
|
|||||||
holder.container.setOnLongClickListener {
|
holder.container.setOnLongClickListener {
|
||||||
nodeClickCallback?.onNodeLongClick(subNode) ?: false
|
nodeClickCallback?.onNodeLongClick(subNode) ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.container.isSelected = actionNodesList.contains(subNode)
|
|
||||||
|
|
||||||
// Add subText with username
|
|
||||||
holder.subText.apply {
|
|
||||||
text = ""
|
|
||||||
paintFlags = if (subNode.isCurrentlyExpires)
|
|
||||||
paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
|
|
||||||
else
|
|
||||||
paintFlags and Paint.STRIKE_THRU_TEXT_FLAG
|
|
||||||
visibility = View.GONE
|
|
||||||
if (subNode.type == Type.ENTRY) {
|
|
||||||
val entry = subNode as EntryVersioned
|
|
||||||
|
|
||||||
mDatabase.startManageEntry(entry)
|
|
||||||
|
|
||||||
holder.text.text = entry.getVisualTitle()
|
|
||||||
|
|
||||||
val username = entry.username
|
|
||||||
if (showUserNames && username.isNotEmpty()) {
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
text = username
|
|
||||||
setTextSize(textSizeUnit, subtextSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabase.stopManageEntry(entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add number of entries in groups
|
|
||||||
if (subNode.type == Type.GROUP) {
|
|
||||||
if (showNumberEntries) {
|
|
||||||
holder.numberChildren?.apply {
|
|
||||||
text = (subNode as GroupVersioned).getChildEntries(true).size.toString()
|
|
||||||
setTextSize(textSizeUnit, numberChildrenTextSize)
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
holder.numberChildren?.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
return nodeSortedList.size()
|
return nodeSortedList.size()
|
||||||
}
|
}
|
||||||
@@ -361,16 +383,18 @@ class NodeAdapter
|
|||||||
* Callback listener to redefine to do an action when a node is click
|
* Callback listener to redefine to do an action when a node is click
|
||||||
*/
|
*/
|
||||||
interface NodeClickCallback {
|
interface NodeClickCallback {
|
||||||
fun onNodeClick(node: NodeVersioned)
|
fun onNodeClick(node: Node)
|
||||||
fun onNodeLongClick(node: NodeVersioned): Boolean
|
fun onNodeLongClick(node: Node): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
var container: View = itemView.findViewById(R.id.node_container)
|
var container: View = itemView.findViewById(R.id.node_container)
|
||||||
|
var imageIdentifier: ImageView? = itemView.findViewById(R.id.node_image_identifier)
|
||||||
var icon: ImageView = itemView.findViewById(R.id.node_icon)
|
var icon: ImageView = itemView.findViewById(R.id.node_icon)
|
||||||
var text: TextView = itemView.findViewById(R.id.node_text)
|
var text: TextView = itemView.findViewById(R.id.node_text)
|
||||||
var subText: TextView = itemView.findViewById(R.id.node_subtext)
|
var subText: TextView = itemView.findViewById(R.id.node_subtext)
|
||||||
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
||||||
|
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.adapters
|
package com.kunzisoft.keepass.adapters
|
||||||
@@ -22,27 +22,32 @@ package com.kunzisoft.keepass.adapters
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import androidx.cursoradapter.widget.CursorAdapter
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.cursor.EntryCursor
|
import com.kunzisoft.keepass.database.cursor.EntryCursorKDB
|
||||||
|
import com.kunzisoft.keepass.database.cursor.EntryCursorKDBX
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.EntryVersioned
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
import com.kunzisoft.keepass.database.element.PwIcon
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
||||||
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import java.util.*
|
import com.kunzisoft.keepass.view.strikeOut
|
||||||
|
|
||||||
class SearchEntryCursorAdapter(context: Context, private val database: Database)
|
class SearchEntryCursorAdapter(private val context: Context,
|
||||||
|
private val database: Database)
|
||||||
: androidx.cursoradapter.widget.CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
|
: androidx.cursoradapter.widget.CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
|
||||||
|
|
||||||
private val cursorInflater: LayoutInflater = context.getSystemService(
|
private val cursorInflater: LayoutInflater? = context.getSystemService(
|
||||||
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||||
private var displayUsername: Boolean = false
|
private var mDisplayUsername: Boolean = false
|
||||||
|
private var mOmitBackup: Boolean = true
|
||||||
private val iconColor: Int
|
private val iconColor: Int
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -55,12 +60,13 @@ class SearchEntryCursorAdapter(context: Context, private val database: Database)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun reInit(context: Context) {
|
fun reInit(context: Context) {
|
||||||
this.displayUsername = PreferencesUtil.showUsernamesListEntries(context)
|
this.mDisplayUsername = PreferencesUtil.showUsernamesListEntries(context)
|
||||||
|
this.mOmitBackup = PreferencesUtil.omitBackup(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
|
override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
|
||||||
|
|
||||||
val view = cursorInflater.inflate(R.layout.item_search_entry, parent, false)
|
val view = cursorInflater!!.inflate(R.layout.item_search_entry, parent, false)
|
||||||
val viewHolder = ViewHolder()
|
val viewHolder = ViewHolder()
|
||||||
viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon)
|
viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon)
|
||||||
viewHolder.textViewTitle = view.findViewById(R.id.entry_text)
|
viewHolder.textViewTitle = view.findViewById(R.id.entry_text)
|
||||||
@@ -71,56 +77,91 @@ class SearchEntryCursorAdapter(context: Context, private val database: Database)
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun bindView(view: View, context: Context, cursor: Cursor) {
|
override fun bindView(view: View, context: Context, cursor: Cursor) {
|
||||||
|
getEntryFrom(cursor)?.let { currentEntry ->
|
||||||
|
val viewHolder = view.tag as ViewHolder
|
||||||
|
|
||||||
// Retrieve elements from cursor
|
// Assign image
|
||||||
val uuid = UUID(cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_UUID_MOST_SIGNIFICANT_BITS)),
|
viewHolder.imageViewIcon?.assignDatabaseIcon(
|
||||||
cursor.getLong(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_UUID_LEAST_SIGNIFICANT_BITS)))
|
database.drawFactory,
|
||||||
val iconFactory = database.iconFactory
|
currentEntry.icon,
|
||||||
var icon: PwIcon = iconFactory.getIcon(
|
iconColor)
|
||||||
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))))
|
// Assign title
|
||||||
if (icon.isUnknown) {
|
viewHolder.textViewTitle?.apply {
|
||||||
icon = iconFactory.getIcon(cursor.getInt(cursor.getColumnIndex(EntryCursor.COLUMN_INDEX_ICON_STANDARD)))
|
text = currentEntry.getVisualTitle()
|
||||||
if (icon.isUnknown)
|
strikeOut(currentEntry.isCurrentlyExpires)
|
||||||
icon = iconFactory.keyIcon
|
}
|
||||||
|
|
||||||
|
// Assign subtitle
|
||||||
|
viewHolder.textViewSubTitle?.apply {
|
||||||
|
val entryUsername = currentEntry.username
|
||||||
|
text = if (mDisplayUsername && entryUsername.isNotEmpty()) {
|
||||||
|
String.format("(%s)", entryUsername)
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
strikeOut(currentEntry.isCurrentlyExpires)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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
|
private fun getEntryFrom(cursor: Cursor): Entry? {
|
||||||
|
return database.createEntry()?.apply {
|
||||||
// Assign image
|
database.startManageEntry(this)
|
||||||
viewHolder.imageViewIcon?.assignDatabaseIcon(database.drawFactory, icon, iconColor)
|
entryKDB?.let { entryKDB ->
|
||||||
|
(cursor as EntryCursorKDB).populateEntry(entryKDB, database.iconFactory)
|
||||||
// Assign title
|
}
|
||||||
val showTitle = EntryVersioned.getVisualTitle(false, title, username, url, uuid.toString())
|
entryKDBX?.let { entryKDBX ->
|
||||||
viewHolder.textViewTitle?.text = showTitle
|
(cursor as EntryCursorKDBX).populateEntry(entryKDBX, database.iconFactory)
|
||||||
if (displayUsername && username.isNotEmpty()) {
|
}
|
||||||
viewHolder.textViewSubTitle?.text = String.format("(%s)", username)
|
database.stopManageEntry(this)
|
||||||
} else {
|
|
||||||
viewHolder.textViewSubTitle?.text = ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
|
||||||
|
return searchEntries(context, constraint.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchEntries(context: Context, query: String): Cursor? {
|
||||||
|
var cursorKDB: EntryCursorKDB? = null
|
||||||
|
var cursorKDBX: EntryCursorKDBX? = null
|
||||||
|
|
||||||
|
if (database.type == DatabaseKDB.TYPE)
|
||||||
|
cursorKDB = EntryCursorKDB()
|
||||||
|
if (database.type == DatabaseKDBX.TYPE)
|
||||||
|
cursorKDBX = EntryCursorKDBX()
|
||||||
|
|
||||||
|
val searchGroup = database.createVirtualGroupFromSearch(query,
|
||||||
|
mOmitBackup,
|
||||||
|
SearchHelper.MAX_SEARCH_ENTRY)
|
||||||
|
if (searchGroup != null) {
|
||||||
|
// Search in hide entries but not meta-stream
|
||||||
|
for (entry in searchGroup.getFilteredChildEntries(Group.ChildFilter.getDefaults(context))) {
|
||||||
|
entry.entryKDB?.let {
|
||||||
|
cursorKDB?.addEntry(it)
|
||||||
|
}
|
||||||
|
entry.entryKDBX?.let {
|
||||||
|
cursorKDBX?.addEntry(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cursorKDB ?: cursorKDBX
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEntryFromPosition(position: Int): Entry? {
|
||||||
|
var pwEntry: Entry? = null
|
||||||
|
|
||||||
|
val cursor = this.cursor
|
||||||
|
if (cursor.moveToFirst() && cursor.move(position)) {
|
||||||
|
pwEntry = getEntryFrom(cursor)
|
||||||
|
}
|
||||||
|
return pwEntry
|
||||||
|
}
|
||||||
|
|
||||||
private class ViewHolder {
|
private class ViewHolder {
|
||||||
internal var imageViewIcon: ImageView? = null
|
internal var imageViewIcon: ImageView? = null
|
||||||
internal var textViewTitle: TextView? = null
|
internal var textViewTitle: TextView? = null
|
||||||
internal var textViewSubTitle: 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,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.app
|
package com.kunzisoft.keepass.app
|
||||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.app
|
|||||||
import androidx.multidex.MultiDexApplication
|
import androidx.multidex.MultiDexApplication
|
||||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
class App : MultiDexApplication() {
|
class App : MultiDexApplication() {
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ class App : MultiDexApplication() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onTerminate() {
|
override fun onTerminate() {
|
||||||
Database.getInstance().closeAndClear(applicationContext.filesDir)
|
Database.getInstance().closeAndClear(UriUtil.getBinaryDir(this))
|
||||||
super.onTerminate()
|
super.onTerminate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ package com.kunzisoft.keepass.app;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Process;
|
import android.os.Process;
|
||||||
|
|
||||||
import com.kunzisoft.keepass.utils.StringUtil;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
@@ -66,7 +64,7 @@ public final class PRNGFixes {
|
|||||||
|
|
||||||
private static boolean supportedOnThisDevice() {
|
private static boolean supportedOnThisDevice() {
|
||||||
// Blacklist on samsung devices
|
// Blacklist on samsung devices
|
||||||
if (StringUtil.INSTANCE.indexOfIgnoreCase(Build.MANUFACTURER, "samsung", Locale.ENGLISH) >= 0) {
|
if (Build.MANUFACTURER.toLowerCase(Locale.ENGLISH).contains("samsung")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.app.database
|
|
||||||
|
|
||||||
import android.os.AsyncTask
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Private class to invoke each method in a separate thread
|
|
||||||
*/
|
|
||||||
class ActionDatabaseAsyncTask<T>(
|
|
||||||
private val action: () -> T ,
|
|
||||||
private val afterActionDatabaseListener: ((T?) -> Unit)? = null
|
|
||||||
) : AsyncTask<Void, Void, T>() {
|
|
||||||
|
|
||||||
override fun doInBackground(vararg args: Void?): T? {
|
|
||||||
return action.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPostExecute(result: T?) {
|
|
||||||
afterActionDatabaseListener?.invoke(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
package com.kunzisoft.keepass.app.database
|
package com.kunzisoft.keepass.app.database
|
||||||
|
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
package com.kunzisoft.keepass.app.database
|
package com.kunzisoft.keepass.app.database
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -13,7 +32,7 @@ class CipherDatabaseAction(applicationContext: Context) {
|
|||||||
|
|
||||||
fun getCipherDatabase(databaseUri: Uri,
|
fun getCipherDatabase(databaseUri: Uri,
|
||||||
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
|
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
|
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
|
||||||
},
|
},
|
||||||
@@ -32,7 +51,7 @@ class CipherDatabaseAction(applicationContext: Context) {
|
|||||||
|
|
||||||
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
|
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
|
||||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
|
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
|
||||||
|
|
||||||
@@ -51,7 +70,7 @@ class CipherDatabaseAction(applicationContext: Context) {
|
|||||||
|
|
||||||
fun deleteByDatabaseUri(databaseUri: Uri,
|
fun deleteByDatabaseUri(databaseUri: Uri,
|
||||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
|
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
|
||||||
},
|
},
|
||||||
@@ -62,7 +81,7 @@ class CipherDatabaseAction(applicationContext: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAll() {
|
fun deleteAll() {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
cipherDatabaseDao.deleteAll()
|
cipherDatabaseDao.deleteAll()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
package com.kunzisoft.keepass.app.database
|
package com.kunzisoft.keepass.app.database
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
package com.kunzisoft.keepass.app.database
|
package com.kunzisoft.keepass.app.database
|
||||||
|
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
|
|||||||
@@ -1,51 +1,64 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.app.database
|
package com.kunzisoft.keepass.app.database
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import com.kunzisoft.keepass.model.DatabaseFile
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
|
||||||
|
|
||||||
class FileDatabaseHistoryAction(applicationContext: Context) {
|
class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||||
|
|
||||||
private val databaseFileHistoryDao =
|
private val databaseFileHistoryDao =
|
||||||
AppDatabase
|
AppDatabase
|
||||||
.getDatabase(applicationContext)
|
.getDatabase(applicationContext)
|
||||||
.fileDatabaseHistoryDao()
|
.fileDatabaseHistoryDao()
|
||||||
|
|
||||||
fun getFileDatabaseHistory(databaseUri: Uri,
|
fun getDatabaseFile(databaseUri: Uri,
|
||||||
fileHistoryResultListener: (fileDatabaseHistoryResult: FileDatabaseHistoryEntity?) -> Unit) {
|
databaseFileResult: (DatabaseFile?) -> Unit) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
val fileDatabaseHistoryEntity = databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||||
|
val fileDatabaseInfo = FileDatabaseInfo(applicationContext, databaseUri)
|
||||||
|
DatabaseFile(
|
||||||
|
databaseUri,
|
||||||
|
UriUtil.parse(fileDatabaseHistoryEntity?.keyFileUri),
|
||||||
|
UriUtil.decode(fileDatabaseHistoryEntity?.databaseUri),
|
||||||
|
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias ?: ""),
|
||||||
|
fileDatabaseInfo.exists,
|
||||||
|
fileDatabaseInfo.getModificationString(),
|
||||||
|
fileDatabaseInfo.getSizeString()
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fileHistoryResultListener.invoke(it)
|
databaseFileResult.invoke(it)
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getKeyFileUriByDatabaseUri(databaseUri: Uri,
|
fun getKeyFileUriByDatabaseUri(databaseUri: Uri,
|
||||||
keyFileUriResultListener: (Uri?) -> Unit) {
|
keyFileUriResultListener: (Uri?) -> Unit) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||||
},
|
},
|
||||||
@@ -59,61 +72,128 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
|
|||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAllFileDatabaseHistories(fileHistoryResultListener: (fileDatabaseHistoryResult: List<FileDatabaseHistoryEntity>?) -> Unit) {
|
fun getDatabaseFileList(databaseFileListResult: (List<DatabaseFile>) -> Unit) {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.getAll()
|
val hideBrokenLocations = PreferencesUtil.hideBrokenLocations(applicationContext)
|
||||||
|
// Show only uri accessible
|
||||||
|
val databaseFileListLoaded = ArrayList<DatabaseFile>()
|
||||||
|
databaseFileHistoryDao.getAll().forEach { fileDatabaseHistoryEntity ->
|
||||||
|
val fileDatabaseInfo = FileDatabaseInfo(applicationContext, fileDatabaseHistoryEntity.databaseUri)
|
||||||
|
if (hideBrokenLocations && fileDatabaseInfo.exists
|
||||||
|
|| !hideBrokenLocations) {
|
||||||
|
databaseFileListLoaded.add(
|
||||||
|
DatabaseFile(
|
||||||
|
UriUtil.parse(fileDatabaseHistoryEntity.databaseUri),
|
||||||
|
UriUtil.parse(fileDatabaseHistoryEntity.keyFileUri),
|
||||||
|
UriUtil.decode(fileDatabaseHistoryEntity.databaseUri),
|
||||||
|
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
|
||||||
|
fileDatabaseInfo.exists,
|
||||||
|
fileDatabaseInfo.getModificationString(),
|
||||||
|
fileDatabaseInfo.getSizeString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
databaseFileListLoaded
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fileHistoryResultListener.invoke(it)
|
databaseFileList ->
|
||||||
}
|
databaseFileList?.let {
|
||||||
).execute()
|
databaseFileListResult.invoke(it)
|
||||||
}
|
|
||||||
|
|
||||||
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null) {
|
|
||||||
addOrUpdateFileDatabaseHistory(FileDatabaseHistoryEntity(
|
|
||||||
databaseUri.toString(),
|
|
||||||
"",
|
|
||||||
keyFileUri?.toString(),
|
|
||||||
System.currentTimeMillis()
|
|
||||||
), true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addOrUpdateFileDatabaseHistory(fileDatabaseHistory: FileDatabaseHistoryEntity, unmodifiedAlias: Boolean = false) {
|
|
||||||
ActionDatabaseAsyncTask(
|
|
||||||
{
|
|
||||||
val fileDatabaseHistoryRetrieve = databaseFileHistoryDao.getByDatabaseUri(fileDatabaseHistory.databaseUri)
|
|
||||||
|
|
||||||
if (unmodifiedAlias) {
|
|
||||||
fileDatabaseHistory.databaseAlias = fileDatabaseHistoryRetrieve?.databaseAlias ?: ""
|
|
||||||
}
|
|
||||||
// Update values if history element not yet in the database
|
|
||||||
if (fileDatabaseHistoryRetrieve == null) {
|
|
||||||
databaseFileHistoryDao.add(fileDatabaseHistory)
|
|
||||||
} else {
|
|
||||||
databaseFileHistoryDao.update(fileDatabaseHistory)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteFileDatabaseHistory(fileDatabaseHistory: FileDatabaseHistoryEntity,
|
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null,
|
||||||
fileHistoryDeletedResult: (FileDatabaseHistoryEntity?) -> Unit) {
|
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
|
||||||
ActionDatabaseAsyncTask(
|
addOrUpdateDatabaseFile(DatabaseFile(
|
||||||
|
databaseUri,
|
||||||
|
keyFileUri
|
||||||
|
), databaseFileAddedOrUpdatedResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addOrUpdateDatabaseFile(databaseFileToAddOrUpdate: DatabaseFile,
|
||||||
|
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
|
||||||
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.delete(fileDatabaseHistory)
|
databaseFileToAddOrUpdate.databaseUri?.let { databaseUri ->
|
||||||
|
// Try to get info in database first
|
||||||
|
val fileDatabaseHistoryRetrieve = databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||||
|
|
||||||
|
// Complete alias if not exists
|
||||||
|
val fileDatabaseHistory = FileDatabaseHistoryEntity(
|
||||||
|
databaseUri.toString(),
|
||||||
|
databaseFileToAddOrUpdate.databaseAlias
|
||||||
|
?: fileDatabaseHistoryRetrieve?.databaseAlias
|
||||||
|
?: "",
|
||||||
|
databaseFileToAddOrUpdate.keyFileUri?.toString(),
|
||||||
|
System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update values if history element not yet in the database
|
||||||
|
if (fileDatabaseHistoryRetrieve == null) {
|
||||||
|
databaseFileHistoryDao.add(fileDatabaseHistory)
|
||||||
|
} else {
|
||||||
|
databaseFileHistoryDao.update(fileDatabaseHistory)
|
||||||
|
}
|
||||||
|
|
||||||
|
val fileDatabaseInfo = FileDatabaseInfo(applicationContext,
|
||||||
|
fileDatabaseHistory.databaseUri)
|
||||||
|
DatabaseFile(
|
||||||
|
UriUtil.parse(fileDatabaseHistory.databaseUri),
|
||||||
|
UriUtil.parse(fileDatabaseHistory.keyFileUri),
|
||||||
|
UriUtil.decode(fileDatabaseHistory.databaseUri),
|
||||||
|
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
|
||||||
|
fileDatabaseInfo.exists,
|
||||||
|
fileDatabaseInfo.getModificationString(),
|
||||||
|
fileDatabaseInfo.getSizeString()
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
if (it != null && it > 0)
|
databaseFileAddedOrUpdatedResult?.invoke(it)
|
||||||
fileHistoryDeletedResult.invoke(fileDatabaseHistory)
|
}
|
||||||
else
|
).execute()
|
||||||
fileHistoryDeletedResult.invoke(null)
|
}
|
||||||
|
|
||||||
|
fun deleteDatabaseFile(databaseFileToDelete: DatabaseFile,
|
||||||
|
databaseFileDeletedResult: (DatabaseFile?) -> Unit) {
|
||||||
|
IOActionTask(
|
||||||
|
{
|
||||||
|
databaseFileToDelete.databaseUri?.let { databaseUri ->
|
||||||
|
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())?.let { fileDatabaseHistory ->
|
||||||
|
val returnValue = databaseFileHistoryDao.delete(fileDatabaseHistory)
|
||||||
|
if (returnValue > 0) {
|
||||||
|
DatabaseFile(
|
||||||
|
UriUtil.parse(fileDatabaseHistory.databaseUri),
|
||||||
|
UriUtil.parse(fileDatabaseHistory.keyFileUri),
|
||||||
|
UriUtil.decode(fileDatabaseHistory.databaseUri),
|
||||||
|
databaseFileToDelete.databaseAlias
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
databaseFileDeletedResult.invoke(it)
|
||||||
|
}
|
||||||
|
).execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteKeyFileByDatabaseUri(databaseUri: Uri) {
|
||||||
|
IOActionTask(
|
||||||
|
{
|
||||||
|
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAllKeyFiles() {
|
fun deleteAllKeyFiles() {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.deleteAllKeyFiles()
|
databaseFileHistoryDao.deleteAllKeyFiles()
|
||||||
}
|
}
|
||||||
@@ -121,7 +201,7 @@ class FileDatabaseHistoryAction(applicationContext: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAll() {
|
fun deleteAll() {
|
||||||
ActionDatabaseAsyncTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.deleteAll()
|
databaseFileHistoryDao.deleteAll()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
package com.kunzisoft.keepass.app.database
|
package com.kunzisoft.keepass.app.database
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
@@ -19,6 +38,9 @@ interface FileDatabaseHistoryDao {
|
|||||||
@Delete
|
@Delete
|
||||||
fun delete(fileDatabaseHistory: FileDatabaseHistoryEntity): Int
|
fun delete(fileDatabaseHistory: FileDatabaseHistoryEntity): Int
|
||||||
|
|
||||||
|
@Query("UPDATE file_database_history SET keyfile_uri=null WHERE database_uri = :databaseUriString")
|
||||||
|
fun deleteKeyFileByDatabaseUri(databaseUriString: String)
|
||||||
|
|
||||||
@Query("UPDATE file_database_history SET keyfile_uri=null")
|
@Query("UPDATE file_database_history SET keyfile_uri=null")
|
||||||
fun deleteAllKeyFiles()
|
fun deleteAllKeyFiles()
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
package com.kunzisoft.keepass.app.database
|
package com.kunzisoft.keepass.app.database
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.app.database
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to invoke action in a separate IO thread
|
||||||
|
*/
|
||||||
|
class IOActionTask<T>(
|
||||||
|
private val action: () -> T ,
|
||||||
|
private val afterActionDatabaseListener: ((T?) -> Unit)? = null) {
|
||||||
|
|
||||||
|
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
|
fun execute() {
|
||||||
|
mainScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val asyncResult: Deferred<T?> = async {
|
||||||
|
action.invoke()
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
afterActionDatabaseListener?.invoke(asyncResult.await())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.autofill
|
||||||
@@ -26,15 +26,20 @@ import android.content.Intent
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.service.autofill.Dataset
|
import android.service.autofill.Dataset
|
||||||
import android.service.autofill.FillResponse
|
import android.service.autofill.FillResponse
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.autofill.AutofillManager
|
import android.view.autofill.AutofillManager
|
||||||
import android.view.autofill.AutofillValue
|
import android.view.autofill.AutofillValue
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
|
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import java.util.*
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
@@ -56,27 +61,44 @@ object AutofillHelper {
|
|||||||
return String.format("%s (%s)", entryInfo.title, entryInfo.username)
|
return String.format("%s (%s)", entryInfo.title, entryInfo.username)
|
||||||
if (entryInfo.title.isNotEmpty())
|
if (entryInfo.title.isNotEmpty())
|
||||||
return entryInfo.title
|
return entryInfo.title
|
||||||
if (entryInfo.username.isNotEmpty())
|
|
||||||
return entryInfo.username
|
|
||||||
if (entryInfo.url.isNotEmpty())
|
if (entryInfo.url.isNotEmpty())
|
||||||
return entryInfo.url
|
return entryInfo.url
|
||||||
|
if (entryInfo.username.isNotEmpty())
|
||||||
|
return entryInfo.username
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildDataset(context: Context,
|
internal fun addHeader(responseBuilder: FillResponse.Builder,
|
||||||
entryInfo: EntryInfo,
|
packageName: String,
|
||||||
struct: StructureParser.Result): Dataset? {
|
webDomain: String?,
|
||||||
|
applicationId: String?) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
if (webDomain != null) {
|
||||||
|
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_web_domain).apply {
|
||||||
|
setTextViewText(R.id.autofill_web_domain_text, webDomain)
|
||||||
|
})
|
||||||
|
} else if (applicationId != null) {
|
||||||
|
responseBuilder.setHeader(RemoteViews(packageName, R.layout.item_autofill_app_id).apply {
|
||||||
|
setTextViewText(R.id.autofill_app_id_text, applicationId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun buildDataset(context: Context,
|
||||||
|
entryInfo: EntryInfo,
|
||||||
|
struct: StructureParser.Result): Dataset? {
|
||||||
val title = makeEntryTitle(entryInfo)
|
val title = makeEntryTitle(entryInfo)
|
||||||
val views = newRemoteViews(context.packageName, title)
|
val views = newRemoteViews(context, title, entryInfo.icon)
|
||||||
val builder = Dataset.Builder(views)
|
val builder = Dataset.Builder(views)
|
||||||
builder.setId(entryInfo.id)
|
builder.setId(entryInfo.id)
|
||||||
|
|
||||||
struct.password.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.password)) }
|
struct.usernameId?.let { usernameId ->
|
||||||
|
builder.setValue(usernameId, AutofillValue.forText(entryInfo.username))
|
||||||
val ids = ArrayList(struct.username)
|
}
|
||||||
if (entryInfo.username.contains("@") || struct.username.isEmpty())
|
struct.passwordId?.let { password ->
|
||||||
ids.addAll(struct.email)
|
builder.setValue(password, AutofillValue.forText(entryInfo.password))
|
||||||
ids.forEach { id -> builder.setValue(id, AutofillValue.forText(entryInfo.username)) }
|
}
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
builder.build()
|
builder.build()
|
||||||
@@ -87,31 +109,43 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to hit when right key is selected
|
* Build the Autofill response for one entry
|
||||||
*/
|
*/
|
||||||
fun buildResponseWhenEntrySelected(activity: Activity, entryInfo: EntryInfo) {
|
fun buildResponse(activity: Activity, entryInfo: EntryInfo) {
|
||||||
var setResultOk = false
|
buildResponse(activity, ArrayList<EntryInfo>().apply { add(entryInfo) })
|
||||||
activity.intent?.extras?.let { extras ->
|
}
|
||||||
if (extras.containsKey(ASSIST_STRUCTURE)) {
|
|
||||||
activity.intent?.getParcelableExtra<AssistStructure>(ASSIST_STRUCTURE)?.let { structure ->
|
/**
|
||||||
StructureParser(structure).parse()?.let { result ->
|
* Build the Autofill response for many entry
|
||||||
// New Response
|
*/
|
||||||
val responseBuilder = FillResponse.Builder()
|
fun buildResponse(activity: Activity, entriesInfo: List<EntryInfo>) {
|
||||||
val dataset = buildDataset(activity, entryInfo, result)
|
if (entriesInfo.isEmpty()) {
|
||||||
responseBuilder.addDataset(dataset)
|
activity.setResult(Activity.RESULT_CANCELED)
|
||||||
val mReplyIntent = Intent()
|
} else {
|
||||||
Log.d(activity.javaClass.name, "Successed Autofill auth.")
|
var setResultOk = false
|
||||||
mReplyIntent.putExtra(
|
activity.intent?.extras?.let { extras ->
|
||||||
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
if (extras.containsKey(ASSIST_STRUCTURE)) {
|
||||||
responseBuilder.build())
|
activity.intent?.getParcelableExtra<AssistStructure>(ASSIST_STRUCTURE)?.let { structure ->
|
||||||
setResultOk = true
|
StructureParser(structure).parse()?.let { result ->
|
||||||
activity.setResult(Activity.RESULT_OK, mReplyIntent)
|
// New Response
|
||||||
|
val responseBuilder = FillResponse.Builder()
|
||||||
|
entriesInfo.forEach {
|
||||||
|
responseBuilder.addDataset(buildDataset(activity, it, result))
|
||||||
|
}
|
||||||
|
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) {
|
||||||
if (!setResultOk) {
|
Log.w(activity.javaClass.name, "Failed Autofill auth.")
|
||||||
Log.w(activity.javaClass.name, "Failed Autofill auth.")
|
activity.setResult(Activity.RESULT_CANCELED)
|
||||||
activity.setResult(Activity.RESULT_CANCELED)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,9 +153,15 @@ object AutofillHelper {
|
|||||||
/**
|
/**
|
||||||
* Utility method to start an activity with an Autofill for result
|
* Utility method to start an activity with an Autofill for result
|
||||||
*/
|
*/
|
||||||
fun startActivityForAutofillResult(activity: Activity, intent: Intent, assistStructure: AssistStructure) {
|
fun startActivityForAutofillResult(activity: Activity,
|
||||||
|
intent: Intent,
|
||||||
|
assistStructure: AssistStructure,
|
||||||
|
searchInfo: SearchInfo?) {
|
||||||
EntrySelectionHelper.addEntrySelectionModeExtraInIntent(intent)
|
EntrySelectionHelper.addEntrySelectionModeExtraInIntent(intent)
|
||||||
intent.putExtra(ASSIST_STRUCTURE, assistStructure)
|
intent.putExtra(ASSIST_STRUCTURE, assistStructure)
|
||||||
|
searchInfo?.let {
|
||||||
|
intent.putExtra(KEY_SEARCH_INFO, it)
|
||||||
|
}
|
||||||
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
|
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,9 +180,18 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun newRemoteViews(packageName: String, remoteViewsText: String): RemoteViews {
|
private fun newRemoteViews(context: Context,
|
||||||
val presentation = RemoteViews(packageName, R.layout.item_autofill_service)
|
remoteViewsText: String,
|
||||||
presentation.setTextViewText(R.id.text, remoteViewsText)
|
remoteViewsIcon: IconImage? = null): RemoteViews {
|
||||||
|
val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry)
|
||||||
|
presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
|
||||||
|
if (remoteViewsIcon != null) {
|
||||||
|
presentation.assignDatabaseIcon(context,
|
||||||
|
R.id.autofill_entry_icon,
|
||||||
|
Database.getInstance().drawFactory,
|
||||||
|
remoteViewsIcon,
|
||||||
|
ContextCompat.getColor(context, R.color.green))
|
||||||
|
}
|
||||||
return presentation
|
return presentation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePass DX.
|
|
||||||
*
|
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePass DX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePass DX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.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 androidx.annotation.RequiresApi
|
|
||||||
import androidx.appcompat.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)
|
|
||||||
super.onActivityResult(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,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.autofill
|
||||||
@@ -22,31 +22,95 @@ package com.kunzisoft.keepass.autofill
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.CancellationSignal
|
import android.os.CancellationSignal
|
||||||
import android.service.autofill.*
|
import android.service.autofill.*
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class KeeAutofillService : AutofillService() {
|
class KeeAutofillService : AutofillService() {
|
||||||
|
|
||||||
override fun onFillRequest(request: FillRequest, cancellationSignal: CancellationSignal,
|
var applicationIdBlocklist: Set<String>? = null
|
||||||
|
var webDomainBlocklist: Set<String>? = null
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(this)
|
||||||
|
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFillRequest(request: FillRequest,
|
||||||
|
cancellationSignal: CancellationSignal,
|
||||||
callback: FillCallback) {
|
callback: FillCallback) {
|
||||||
val fillContexts = request.fillContexts
|
val fillContexts = request.fillContexts
|
||||||
val latestStructure = fillContexts[fillContexts.size - 1].structure
|
val latestStructure = fillContexts[fillContexts.size - 1].structure
|
||||||
|
|
||||||
cancellationSignal.setOnCancelListener { Log.e(TAG, "Cancel autofill not implemented in this sample.") }
|
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
|
||||||
|
|
||||||
val responseBuilder = FillResponse.Builder()
|
|
||||||
// Check user's settings for authenticating Responses and Datasets.
|
// Check user's settings for authenticating Responses and Datasets.
|
||||||
val parseResult = StructureParser(latestStructure).parse()
|
StructureParser(latestStructure).parse()?.let { parseResult ->
|
||||||
parseResult?.allAutofillIds()?.let { autofillIds ->
|
|
||||||
if (listOf(*autofillIds).isNotEmpty()) {
|
// Build search info only if applicationId or webDomain are not blocked
|
||||||
|
if (searchAllowedFor(parseResult.applicationId, applicationIdBlocklist)
|
||||||
|
&& searchAllowedFor(parseResult.domain, webDomainBlocklist)) {
|
||||||
|
val searchInfo = SearchInfo().apply {
|
||||||
|
applicationId = parseResult.applicationId
|
||||||
|
webDomain = parseResult.domain
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
|
Database.getInstance(),
|
||||||
|
searchInfo,
|
||||||
|
{ items ->
|
||||||
|
val responseBuilder = FillResponse.Builder()
|
||||||
|
AutofillHelper.addHeader(responseBuilder, packageName,
|
||||||
|
parseResult.domain, parseResult.applicationId)
|
||||||
|
items.forEach {
|
||||||
|
responseBuilder.addDataset(AutofillHelper.buildDataset(this, it, parseResult))
|
||||||
|
}
|
||||||
|
callback.onSuccess(responseBuilder.build())
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Show UI if no search result
|
||||||
|
showUIForEntrySelection(parseResult, searchInfo, callback)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Show UI if database not open
|
||||||
|
showUIForEntrySelection(parseResult, searchInfo, callback)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
||||||
|
searchInfo: SearchInfo,
|
||||||
|
callback: FillCallback) {
|
||||||
|
parseResult.allAutofillIds().let { autofillIds ->
|
||||||
|
if (autofillIds.isNotEmpty()) {
|
||||||
// If the entire Autofill Response is authenticated, AuthActivity is used
|
// If the entire Autofill Response is authenticated, AuthActivity is used
|
||||||
// to generate Response.
|
// to generate Response.
|
||||||
val sender = AutofillLauncherActivity.getAuthIntentSenderForResponse(this)
|
val sender = AutofillLauncherActivity.getAuthIntentSenderForResponse(this,
|
||||||
val presentation = RemoteViews(packageName, R.layout.item_autofill_service_unlock)
|
searchInfo)
|
||||||
responseBuilder.setAuthentication(autofillIds, sender, presentation)
|
val responseBuilder = FillResponse.Builder()
|
||||||
|
val remoteViewsUnlock: RemoteViews = if (!parseResult.domain.isNullOrEmpty()) {
|
||||||
|
RemoteViews(packageName, R.layout.item_autofill_unlock_web_domain).apply {
|
||||||
|
setTextViewText(R.id.autofill_web_domain_text, parseResult.domain)
|
||||||
|
}
|
||||||
|
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
||||||
|
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
|
||||||
|
setTextViewText(R.id.autofill_app_id_text, parseResult.applicationId)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RemoteViews(packageName, R.layout.item_autofill_unlock)
|
||||||
|
}
|
||||||
|
responseBuilder.setAuthentication(autofillIds, sender, remoteViewsUnlock)
|
||||||
callback.onSuccess(responseBuilder.build())
|
callback.onSuccess(responseBuilder.build())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,5 +131,18 @@ class KeeAutofillService : AutofillService() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = KeeAutofillService::class.java.name
|
private val TAG = KeeAutofillService::class.java.name
|
||||||
|
|
||||||
|
fun searchAllowedFor(element: String?, blockList: Set<String>?): Boolean {
|
||||||
|
element?.let { elementNotNull ->
|
||||||
|
if (blockList?.any { appIdBlocked ->
|
||||||
|
elementNotNull.contains(appIdBlocked)
|
||||||
|
} == true
|
||||||
|
) {
|
||||||
|
Log.d(TAG, "Autofill not allowed for $elementNotNull")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.autofill
|
||||||
|
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.autofill.AutofillId
|
import android.view.autofill.AutofillId
|
||||||
@@ -35,74 +35,238 @@ import java.util.*
|
|||||||
internal class StructureParser(private val structure: AssistStructure) {
|
internal class StructureParser(private val structure: AssistStructure) {
|
||||||
private var result: Result? = null
|
private var result: Result? = null
|
||||||
private var usernameCandidate: AutofillId? = null
|
private var usernameCandidate: AutofillId? = null
|
||||||
|
private var usernameNeeded = true
|
||||||
|
|
||||||
fun parse(): Result? {
|
fun parse(): Result? {
|
||||||
result = Result()
|
try {
|
||||||
result?.apply {
|
result = Result()
|
||||||
usernameCandidate = null
|
result?.apply {
|
||||||
for (i in 0 until structure.windowNodeCount) {
|
usernameCandidate = null
|
||||||
val windowNode = structure.getWindowNodeAt(i)
|
mainLoop@ for (i in 0 until structure.windowNodeCount) {
|
||||||
title.add(windowNode.title)
|
val windowNode = structure.getWindowNodeAt(i)
|
||||||
windowNode.rootViewNode.webDomain?.let {
|
applicationId = windowNode.title.toString().split("/")[0]
|
||||||
webDomain.add(it)
|
Log.d(TAG, "Autofill applicationId: $applicationId")
|
||||||
}
|
|
||||||
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
|
if (parseViewNode(windowNode.rootViewNode))
|
||||||
|
break@mainLoop
|
||||||
|
}
|
||||||
|
// If not explicit username field found, add the field just before password field.
|
||||||
|
if (usernameId == null && passwordId != null && usernameCandidate != null)
|
||||||
|
usernameId = usernameCandidate
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the result only if password field is retrieved
|
||||||
|
return if ((!usernameNeeded || result?.usernameId != null)
|
||||||
|
&& result?.passwordId != null)
|
||||||
|
result
|
||||||
|
else
|
||||||
|
null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseViewNode(node: AssistStructure.ViewNode) {
|
private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
|
||||||
val hints = node.autofillHints
|
// Get the domain of a web app
|
||||||
val autofillId = node.autofillId
|
node.webDomain?.let { webDomain ->
|
||||||
if (autofillId != null) {
|
if (webDomain.isNotEmpty()) {
|
||||||
if (hints != null && hints.isNotEmpty()) {
|
result?.domain = webDomain
|
||||||
when {
|
Log.d(TAG, "Autofill domain: $webDomain")
|
||||||
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)
|
// Only parse visible nodes
|
||||||
parseViewNode(node.getChildAt(i))
|
if (node.visibility == View.VISIBLE) {
|
||||||
|
if (node.autofillId != null
|
||||||
|
&& node.autofillType == View.AUTOFILL_TYPE_TEXT) {
|
||||||
|
// Parse methods
|
||||||
|
val hints = node.autofillHints
|
||||||
|
if (hints != null && hints.isNotEmpty()) {
|
||||||
|
if (parseNodeByAutofillHint(node))
|
||||||
|
return true
|
||||||
|
} else if (parseNodeByHtmlAttributes(node))
|
||||||
|
return true
|
||||||
|
else if (parseNodeByAndroidInput(node))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Recursive method to process each node
|
||||||
|
for (i in 0 until node.childCount) {
|
||||||
|
if (parseViewNode(node.getChildAt(i)))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseNodeByAutofillHint(node: AssistStructure.ViewNode): Boolean {
|
||||||
|
val autofillId = node.autofillId
|
||||||
|
node.autofillHints?.forEach {
|
||||||
|
when {
|
||||||
|
it.contains(View.AUTOFILL_HINT_USERNAME, true)
|
||||||
|
|| it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|
||||||
|
|| it.contains("email", true)
|
||||||
|
|| it.contains(View.AUTOFILL_HINT_PHONE, true)-> {
|
||||||
|
result?.usernameId = autofillId
|
||||||
|
Log.d(TAG, "Autofill username hint")
|
||||||
|
}
|
||||||
|
it.contains(View.AUTOFILL_HINT_PASSWORD, true) -> {
|
||||||
|
result?.passwordId = autofillId
|
||||||
|
Log.d(TAG, "Autofill password hint")
|
||||||
|
// Username not needed in this case
|
||||||
|
usernameNeeded = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Ignore autocomplete="off"
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
|
||||||
|
it.equals("off", true) ||
|
||||||
|
it.equals("on", true) -> {
|
||||||
|
Log.d(TAG, "Autofill web hint")
|
||||||
|
return parseNodeByHtmlAttributes(node)
|
||||||
|
}
|
||||||
|
else -> Log.d(TAG, "Autofill unsupported hint $it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean {
|
||||||
|
val autofillId = node.autofillId
|
||||||
|
val nodHtml = node.htmlInfo
|
||||||
|
when (nodHtml?.tag?.toLowerCase(Locale.ENGLISH)) {
|
||||||
|
"input" -> {
|
||||||
|
nodHtml.attributes?.forEach { pairAttribute ->
|
||||||
|
when (pairAttribute.first.toLowerCase(Locale.ENGLISH)) {
|
||||||
|
"type" -> {
|
||||||
|
when (pairAttribute.second.toLowerCase(Locale.ENGLISH)) {
|
||||||
|
"tel", "email" -> {
|
||||||
|
result?.usernameId = autofillId
|
||||||
|
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||||
|
}
|
||||||
|
"text" -> {
|
||||||
|
usernameCandidate = autofillId
|
||||||
|
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||||
|
}
|
||||||
|
"password" -> {
|
||||||
|
result?.passwordId = autofillId
|
||||||
|
Log.d(TAG, "Autofill password web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun inputIsVariationType(inputType: Int, vararg type: Int): Boolean {
|
||||||
|
type.forEach {
|
||||||
|
if (inputType and InputType.TYPE_MASK_VARIATION == it)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showHexInputType(inputType: Int): String {
|
||||||
|
return "0x${"%08x".format(inputType)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseNodeByAndroidInput(node: AssistStructure.ViewNode): Boolean {
|
||||||
|
val autofillId = node.autofillId
|
||||||
|
val inputType = node.inputType
|
||||||
|
when (inputType and InputType.TYPE_MASK_CLASS) {
|
||||||
|
InputType.TYPE_CLASS_TEXT -> {
|
||||||
|
when {
|
||||||
|
inputIsVariationType(inputType,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS) -> {
|
||||||
|
result?.usernameId = autofillId
|
||||||
|
Log.d(TAG, "Autofill username android text type: ${showHexInputType(inputType)}")
|
||||||
|
}
|
||||||
|
inputIsVariationType(inputType,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_NORMAL,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> {
|
||||||
|
usernameCandidate = autofillId
|
||||||
|
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
|
||||||
|
}
|
||||||
|
inputIsVariationType(inputType,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_PASSWORD,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) -> {
|
||||||
|
result?.passwordId = autofillId
|
||||||
|
Log.d(TAG, "Autofill password android text type: ${showHexInputType(inputType)}")
|
||||||
|
usernameNeeded = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
inputIsVariationType(inputType,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_FILTER,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_PHONETIC,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_URI) -> {
|
||||||
|
// Type not used
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.d(TAG, "Autofill unknown android text type: ${showHexInputType(inputType)}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputType.TYPE_CLASS_NUMBER -> {
|
||||||
|
when {
|
||||||
|
inputIsVariationType(inputType,
|
||||||
|
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
|
||||||
|
usernameCandidate = autofillId
|
||||||
|
Log.d(TAG, "Autofill usernale candidate android number type: ${showHexInputType(inputType)}")
|
||||||
|
}
|
||||||
|
inputIsVariationType(inputType,
|
||||||
|
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {
|
||||||
|
result?.passwordId = autofillId
|
||||||
|
Log.d(TAG, "Autofill password android number type: ${showHexInputType(inputType)}")
|
||||||
|
usernameNeeded = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.d(TAG, "Autofill unknown android number type: ${showHexInputType(inputType)}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
internal class Result {
|
internal class Result {
|
||||||
val title: MutableList<CharSequence>
|
var applicationId: String? = null
|
||||||
val webDomain: MutableList<String>
|
var domain: String? = null
|
||||||
val username: MutableList<AutofillId>
|
set(value) {
|
||||||
val email: MutableList<AutofillId>
|
if (field == null)
|
||||||
val password: MutableList<AutofillId>
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
var usernameId: AutofillId? = null
|
||||||
title = ArrayList()
|
set(value) {
|
||||||
webDomain = ArrayList()
|
if (field == null)
|
||||||
username = ArrayList()
|
field = value
|
||||||
email = ArrayList()
|
}
|
||||||
password = ArrayList()
|
|
||||||
}
|
var passwordId: AutofillId? = null
|
||||||
|
set(value) {
|
||||||
|
if (field == null)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
fun allAutofillIds(): Array<AutofillId> {
|
fun allAutofillIds(): Array<AutofillId> {
|
||||||
val all = ArrayList<AutofillId>()
|
val all = ArrayList<AutofillId>()
|
||||||
all.addAll(username)
|
usernameId?.let {
|
||||||
all.addAll(email)
|
all.add(it)
|
||||||
all.addAll(password)
|
}
|
||||||
|
passwordId?.let {
|
||||||
|
all.add(it)
|
||||||
|
}
|
||||||
return all.toTypedArray()
|
return all.toTypedArray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,29 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.backup
|
package com.kunzisoft.keepass.backup
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.backup.BackupAgentHelper
|
import android.app.backup.BackupAgentHelper
|
||||||
import android.app.backup.SharedPreferencesBackupHelper
|
import android.app.backup.SharedPreferencesBackupHelper
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
class SettingsBackupAgent : BackupAgentHelper() {
|
class SettingsBackupAgent : BackupAgentHelper() {
|
||||||
|
|
||||||
//TODO Backup
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
val defaultPrefs = this.packageName + "_preferences"
|
val defaultPrefs = this.packageName + "_preferences"
|
||||||
val prefHelper = SharedPreferencesBackupHelper(this, defaultPrefs)
|
val prefHelper = SharedPreferencesBackupHelper(this, defaultPrefs)
|
||||||
|
|||||||
@@ -1,13 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
package com.kunzisoft.keepass.biometric
|
package com.kunzisoft.keepass.biometric
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.provider.Settings
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
|
import android.view.View
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.biometric.BiometricConstants
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.biometric.BiometricPrompt
|
import androidx.biometric.BiometricPrompt
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
@@ -19,80 +42,86 @@ import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
|||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
class AdvancedUnlockedManager(var context: FragmentActivity,
|
class AdvancedUnlockedManager(var context: FragmentActivity,
|
||||||
var databaseFileUri: Uri,
|
var databaseFileUri: Uri,
|
||||||
var advancedUnlockInfoView: AdvancedUnlockInfoView?,
|
private var advancedUnlockInfoView: AdvancedUnlockInfoView?,
|
||||||
var checkboxPasswordView: CompoundButton?,
|
private var checkboxPasswordView: CompoundButton?,
|
||||||
var onCheckedPasswordChangeListener: CompoundButton.OnCheckedChangeListener? = null,
|
private var onCheckedPasswordChangeListener: CompoundButton.OnCheckedChangeListener? = null,
|
||||||
var passwordView: TextView?,
|
var passwordView: TextView?,
|
||||||
var loadDatabaseAfterRegisterCredentials: (encryptedPassword: String?, ivSpec: String?) -> Unit,
|
private var loadDatabaseAfterRegisterCredentials: (encryptedPassword: String?, ivSpec: String?) -> Unit,
|
||||||
var loadDatabaseAfterRetrieveCredentials: (decryptedPassword: String?) -> Unit)
|
private var loadDatabaseAfterRetrieveCredentials: (decryptedPassword: String?) -> Unit)
|
||||||
: BiometricUnlockDatabaseHelper.BiometricUnlockCallback {
|
: BiometricUnlockDatabaseHelper.BiometricUnlockCallback {
|
||||||
|
|
||||||
private var biometricUnlockDatabaseHelper: BiometricUnlockDatabaseHelper? = null
|
private var biometricUnlockDatabaseHelper: BiometricUnlockDatabaseHelper? = null
|
||||||
private var biometricMode: Mode = Mode.UNAVAILABLE
|
private var biometricMode: Mode = Mode.UNAVAILABLE
|
||||||
|
|
||||||
private var isBiometricPromptAutoOpenEnable = PreferencesUtil.isBiometricPromptAutoOpenEnable(context)
|
/**
|
||||||
|
* Manage setting to auto open biometric prompt
|
||||||
|
*/
|
||||||
|
private var biometricPromptAutoOpenPreference = PreferencesUtil.isBiometricPromptAutoOpenEnable(context)
|
||||||
|
var isBiometricPromptAutoOpenEnable: Boolean = true
|
||||||
|
get() {
|
||||||
|
return field && biometricPromptAutoOpenPreference
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variable to check if the prompt can be open (if the right activity is currently shown)
|
||||||
|
// checkBiometricAvailability() allows open biometric prompt and onDestroy() removes the authorization
|
||||||
|
private var allowOpenBiometricPrompt = false
|
||||||
|
|
||||||
private var cipherDatabaseAction = CipherDatabaseAction.getInstance(context.applicationContext)
|
private var cipherDatabaseAction = CipherDatabaseAction.getInstance(context.applicationContext)
|
||||||
|
|
||||||
// fingerprint related code here
|
init {
|
||||||
fun initBiometric() {
|
|
||||||
|
|
||||||
// Check if fingerprint well init (be called the first time the fingerprint is configured
|
|
||||||
// and the activity still active)
|
|
||||||
if (biometricUnlockDatabaseHelper == null || !biometricUnlockDatabaseHelper!!.isBiometricInitialized) {
|
|
||||||
|
|
||||||
biometricUnlockDatabaseHelper = BiometricUnlockDatabaseHelper(context, this)
|
|
||||||
// callback for fingerprint findings
|
|
||||||
biometricUnlockDatabaseHelper?.setAuthenticationCallback(biometricCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a check listener to change fingerprint mode
|
// Add a check listener to change fingerprint mode
|
||||||
checkboxPasswordView?.setOnCheckedChangeListener { compoundButton, checked ->
|
checkboxPasswordView?.setOnCheckedChangeListener { compoundButton, checked ->
|
||||||
|
|
||||||
checkBiometricAvailability()
|
checkBiometricAvailability()
|
||||||
|
|
||||||
// Add old listener to enable the button, only be call here because of onCheckedChange bug
|
// Add old listener to enable the button, only be call here because of onCheckedChange bug
|
||||||
onCheckedPasswordChangeListener?.onCheckedChanged(compoundButton, checked)
|
onCheckedPasswordChangeListener?.onCheckedChanged(compoundButton, checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
checkBiometricAvailability()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
/**
|
||||||
|
* Check biometric availability and change the current mode depending of device's state
|
||||||
|
*/
|
||||||
fun checkBiometricAvailability() {
|
fun checkBiometricAvailability() {
|
||||||
|
|
||||||
// fingerprint not supported (by API level or hardware) so keep option hidden
|
// biometric not supported (by API level or hardware) so keep option hidden
|
||||||
// or manually disable
|
// or manually disable
|
||||||
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate()
|
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate()
|
||||||
|
allowOpenBiometricPrompt = true
|
||||||
|
|
||||||
if (!PreferencesUtil.isBiometricUnlockEnable(context)
|
if (!PreferencesUtil.isBiometricUnlockEnable(context)
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
|
||||||
|
|
||||||
toggleMode(Mode.UNAVAILABLE)
|
toggleMode(Mode.UNAVAILABLE)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
// biometric is available but not configured, show icon but in disabled state with some information
|
||||||
// fingerprint is available but not configured, show icon but in disabled state with some information
|
|
||||||
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
|
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
|
||||||
|
toggleMode(Mode.BIOMETRIC_NOT_CONFIGURED)
|
||||||
toggleMode(Mode.NOT_CONFIGURED)
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (checkboxPasswordView?.isChecked == true) {
|
// Check if fingerprint well init (be called the first time the fingerprint is configured
|
||||||
// listen for encryption
|
// and the activity still active)
|
||||||
toggleMode(Mode.STORE)
|
if (biometricUnlockDatabaseHelper?.isKeyManagerInitialized != true) {
|
||||||
|
biometricUnlockDatabaseHelper = BiometricUnlockDatabaseHelper(context)
|
||||||
|
// callback for fingerprint findings
|
||||||
|
biometricUnlockDatabaseHelper?.biometricUnlockCallback = this
|
||||||
|
biometricUnlockDatabaseHelper?.authenticationCallback = biometricAuthenticationCallback
|
||||||
|
}
|
||||||
|
// Recheck to change the mode
|
||||||
|
if (biometricUnlockDatabaseHelper?.isKeyManagerInitialized != true) {
|
||||||
|
toggleMode(Mode.KEY_MANAGER_UNAVAILABLE)
|
||||||
} else {
|
} else {
|
||||||
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) {
|
if (checkboxPasswordView?.isChecked == true) {
|
||||||
|
// listen for encryption
|
||||||
// fingerprint available but no stored password found yet for this DB so show info don't listen
|
toggleMode(Mode.STORE_CREDENTIAL)
|
||||||
toggleMode( if (it) {
|
} else {
|
||||||
// listen for decryption
|
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) { containsCipher ->
|
||||||
Mode.OPEN
|
// biometric available but no stored password found yet for this DB so show info don't listen
|
||||||
} else {
|
toggleMode( if (containsCipher) {
|
||||||
// wait for typing
|
// listen for decryption
|
||||||
Mode.WAIT_CREDENTIAL
|
Mode.EXTRACT_CREDENTIAL
|
||||||
})
|
} else {
|
||||||
|
// wait for typing
|
||||||
|
Mode.WAIT_CREDENTIAL
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,7 +135,7 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val biometricCallback = object : BiometricPrompt.AuthenticationCallback () {
|
private val biometricAuthenticationCallback = object : BiometricPrompt.AuthenticationCallback () {
|
||||||
|
|
||||||
override fun onAuthenticationError(
|
override fun onAuthenticationError(
|
||||||
errorCode: Int,
|
errorCode: Int,
|
||||||
@@ -127,19 +156,15 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||||
context.runOnUiThread {
|
context.runOnUiThread {
|
||||||
when (biometricMode) {
|
when (biometricMode) {
|
||||||
Mode.UNAVAILABLE -> {
|
Mode.UNAVAILABLE -> {}
|
||||||
}
|
Mode.BIOMETRIC_NOT_CONFIGURED -> {}
|
||||||
Mode.PAUSE -> {
|
Mode.KEY_MANAGER_UNAVAILABLE -> {}
|
||||||
}
|
Mode.WAIT_CREDENTIAL -> {}
|
||||||
Mode.NOT_CONFIGURED -> {
|
Mode.STORE_CREDENTIAL -> {
|
||||||
}
|
|
||||||
Mode.WAIT_CREDENTIAL -> {
|
|
||||||
}
|
|
||||||
Mode.STORE -> {
|
|
||||||
// newly store the entered password in encrypted way
|
// newly store the entered password in encrypted way
|
||||||
biometricUnlockDatabaseHelper?.encryptData(passwordView?.text.toString())
|
biometricUnlockDatabaseHelper?.encryptData(passwordView?.text.toString())
|
||||||
}
|
}
|
||||||
Mode.OPEN -> {
|
Mode.EXTRACT_CREDENTIAL -> {
|
||||||
// retrieve the encrypted value from preferences
|
// retrieve the encrypted value from preferences
|
||||||
cipherDatabaseAction.getCipherDatabase(databaseFileUri) {
|
cipherDatabaseAction.getCipherDatabase(databaseFileUri) {
|
||||||
it?.encryptedValue?.let { value ->
|
it?.encryptedValue?.let { value ->
|
||||||
@@ -155,11 +180,7 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
private fun initNotAvailable() {
|
private fun initNotAvailable() {
|
||||||
showFingerPrintViews(false)
|
showFingerPrintViews(false)
|
||||||
|
|
||||||
advancedUnlockInfoView?.setIconViewClickListener(null)
|
advancedUnlockInfoView?.setIconViewClickListener(false, null)
|
||||||
}
|
|
||||||
|
|
||||||
private fun initPause() {
|
|
||||||
advancedUnlockInfoView?.setIconViewClickListener(null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initNotConfigured() {
|
private fun initNotConfigured() {
|
||||||
@@ -167,7 +188,19 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
setAdvancedUnlockedTitleView(R.string.configure_biometric)
|
setAdvancedUnlockedTitleView(R.string.configure_biometric)
|
||||||
setAdvancedUnlockedMessageView("")
|
setAdvancedUnlockedMessageView("")
|
||||||
|
|
||||||
advancedUnlockInfoView?.setIconViewClickListener(null)
|
advancedUnlockInfoView?.setIconViewClickListener(false) {
|
||||||
|
context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initKeyManagerNotAvailable() {
|
||||||
|
showFingerPrintViews(true)
|
||||||
|
setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
|
||||||
|
setAdvancedUnlockedMessageView("")
|
||||||
|
|
||||||
|
advancedUnlockInfoView?.setIconViewClickListener(false) {
|
||||||
|
context.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initWaitData() {
|
private fun initWaitData() {
|
||||||
@@ -175,7 +208,20 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
setAdvancedUnlockedTitleView(R.string.no_credentials_stored)
|
setAdvancedUnlockedTitleView(R.string.no_credentials_stored)
|
||||||
setAdvancedUnlockedMessageView("")
|
setAdvancedUnlockedMessageView("")
|
||||||
|
|
||||||
advancedUnlockInfoView?.setIconViewClickListener(null)
|
advancedUnlockInfoView?.setIconViewClickListener(false) {
|
||||||
|
biometricAuthenticationCallback.onAuthenticationError(
|
||||||
|
BiometricConstants.ERROR_UNABLE_TO_PROCESS
|
||||||
|
, context.getString(R.string.credential_before_click_biometric_button))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openBiometricPrompt(biometricPrompt: BiometricPrompt?,
|
||||||
|
cryptoObject: BiometricPrompt.CryptoObject,
|
||||||
|
promptInfo: BiometricPrompt.PromptInfo) {
|
||||||
|
context.runOnUiThread {
|
||||||
|
if (allowOpenBiometricPrompt)
|
||||||
|
biometricPrompt?.authenticate(promptInfo, cryptoObject)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initEncryptData() {
|
private fun initEncryptData() {
|
||||||
@@ -188,9 +234,7 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
cryptoObject?.let { crypto ->
|
cryptoObject?.let { crypto ->
|
||||||
// Set listener to open the biometric dialog and save credential
|
// Set listener to open the biometric dialog and save credential
|
||||||
advancedUnlockInfoView?.setIconViewClickListener { _ ->
|
advancedUnlockInfoView?.setIconViewClickListener { _ ->
|
||||||
context.runOnUiThread {
|
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
|
||||||
biometricPrompt?.authenticate(promptInfo, crypto)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,17 +255,13 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
cryptoObject?.let { crypto ->
|
cryptoObject?.let { crypto ->
|
||||||
// Set listener to open the biometric dialog and check credential
|
// Set listener to open the biometric dialog and check credential
|
||||||
advancedUnlockInfoView?.setIconViewClickListener { _ ->
|
advancedUnlockInfoView?.setIconViewClickListener { _ ->
|
||||||
context.runOnUiThread {
|
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
|
||||||
biometricPrompt?.authenticate(promptInfo, crypto)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto open the biometric prompt
|
// Auto open the biometric prompt
|
||||||
if (isBiometricPromptAutoOpenEnable) {
|
if (isBiometricPromptAutoOpenEnable) {
|
||||||
isBiometricPromptAutoOpenEnable = false
|
isBiometricPromptAutoOpenEnable = false
|
||||||
context.runOnUiThread {
|
openBiometricPrompt(biometricPrompt, crypto, promptInfo)
|
||||||
biometricPrompt?.authenticate(promptInfo, crypto)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,28 +275,22 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
fun initBiometricMode() {
|
fun initBiometricMode() {
|
||||||
when (biometricMode) {
|
when (biometricMode) {
|
||||||
Mode.UNAVAILABLE -> initNotAvailable()
|
Mode.UNAVAILABLE -> initNotAvailable()
|
||||||
Mode.PAUSE -> initPause()
|
Mode.BIOMETRIC_NOT_CONFIGURED -> initNotConfigured()
|
||||||
Mode.NOT_CONFIGURED -> initNotConfigured()
|
Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable()
|
||||||
Mode.WAIT_CREDENTIAL -> initWaitData()
|
Mode.WAIT_CREDENTIAL -> initWaitData()
|
||||||
Mode.STORE -> initEncryptData()
|
Mode.STORE_CREDENTIAL -> initEncryptData()
|
||||||
Mode.OPEN -> initDecryptData()
|
Mode.EXTRACT_CREDENTIAL -> initDecryptData()
|
||||||
}
|
}
|
||||||
// Show fingerprint key deletion
|
// Show fingerprint key deletion
|
||||||
context.invalidateOptionsMenu()
|
context.invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pause() {
|
|
||||||
biometricMode = Mode.PAUSE
|
|
||||||
initBiometricMode()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
|
// Close the biometric prompt
|
||||||
|
allowOpenBiometricPrompt = false
|
||||||
|
biometricUnlockDatabaseHelper?.closeBiometricPrompt()
|
||||||
// Restore the checked listener
|
// Restore the checked listener
|
||||||
checkboxPasswordView?.setOnCheckedChangeListener(onCheckedPasswordChangeListener)
|
checkboxPasswordView?.setOnCheckedChangeListener(onCheckedPasswordChangeListener)
|
||||||
|
|
||||||
biometricMode = Mode.UNAVAILABLE
|
|
||||||
initBiometricMode()
|
|
||||||
biometricUnlockDatabaseHelper = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only to fix multiple fingerprint menu #332
|
// Only to fix multiple fingerprint menu #332
|
||||||
@@ -265,7 +299,7 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
if (!addBiometricMenuInProgress) {
|
if (!addBiometricMenuInProgress) {
|
||||||
addBiometricMenuInProgress = true
|
addBiometricMenuInProgress = true
|
||||||
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) {
|
cipherDatabaseAction.containsCipherDatabase(databaseFileUri) {
|
||||||
if ((biometricMode != Mode.UNAVAILABLE && biometricMode != Mode.NOT_CONFIGURED)
|
if ((biometricMode != Mode.UNAVAILABLE && biometricMode != Mode.BIOMETRIC_NOT_CONFIGURED)
|
||||||
&& it) {
|
&& it) {
|
||||||
menuInflater.inflate(R.menu.advanced_unlock, menu)
|
menuInflater.inflate(R.menu.advanced_unlock, menu)
|
||||||
addBiometricMenuInProgress = false
|
addBiometricMenuInProgress = false
|
||||||
@@ -277,13 +311,12 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
fun deleteEntryKey() {
|
fun deleteEntryKey() {
|
||||||
biometricUnlockDatabaseHelper?.deleteEntryKey()
|
biometricUnlockDatabaseHelper?.deleteEntryKey()
|
||||||
cipherDatabaseAction.deleteByDatabaseUri(databaseFileUri)
|
cipherDatabaseAction.deleteByDatabaseUri(databaseFileUri)
|
||||||
biometricMode = Mode.NOT_CONFIGURED
|
biometricMode = Mode.BIOMETRIC_NOT_CONFIGURED
|
||||||
checkBiometricAvailability()
|
checkBiometricAvailability()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {
|
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {
|
||||||
loadDatabaseAfterRegisterCredentials.invoke(encryptedValue, ivSpec)
|
loadDatabaseAfterRegisterCredentials.invoke(encryptedValue, ivSpec)
|
||||||
// TODO setAdvancedUnlockedMessageView(R.string.encrypted_value_stored)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleDecryptedResult(decryptedValue: String) {
|
override fun handleDecryptedResult(decryptedValue: String) {
|
||||||
@@ -296,12 +329,15 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBiometricException(e: Exception) {
|
override fun onBiometricException(e: Exception) {
|
||||||
if (e.localizedMessage != null)
|
e.localizedMessage?.let {
|
||||||
setAdvancedUnlockedMessageView(e.localizedMessage)
|
setAdvancedUnlockedMessageView(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showFingerPrintViews(show: Boolean) {
|
private fun showFingerPrintViews(show: Boolean) {
|
||||||
context.runOnUiThread { advancedUnlockInfoView?.hide = !show }
|
context.runOnUiThread {
|
||||||
|
advancedUnlockInfoView?.visibility = if (show) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setAdvancedUnlockedTitleView(textId: Int) {
|
private fun setAdvancedUnlockedTitleView(textId: Int) {
|
||||||
@@ -323,7 +359,7 @@ class AdvancedUnlockedManager(var context: FragmentActivity,
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum class Mode {
|
enum class Mode {
|
||||||
UNAVAILABLE, PAUSE, NOT_CONFIGURED, WAIT_CREDENTIAL, STORE, OPEN
|
UNAVAILABLE, BIOMETRIC_NOT_CONFIGURED, KEY_MANAGER_UNAVAILABLE, WAIT_CREDENTIAL, STORE_CREDENTIAL, EXTRACT_CREDENTIAL
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.biometric
|
package com.kunzisoft.keepass.biometric
|
||||||
@@ -42,8 +42,7 @@ import javax.crypto.SecretKey
|
|||||||
import javax.crypto.spec.IvParameterSpec
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
|
class BiometricUnlockDatabaseHelper(private val context: FragmentActivity) {
|
||||||
private val biometricUnlockCallback: BiometricUnlockCallback) {
|
|
||||||
|
|
||||||
private var biometricPrompt: BiometricPrompt? = null
|
private var biometricPrompt: BiometricPrompt? = null
|
||||||
|
|
||||||
@@ -53,13 +52,15 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
|
|||||||
private var keyguardManager: KeyguardManager? = null
|
private var keyguardManager: KeyguardManager? = null
|
||||||
private var cryptoObject: BiometricPrompt.CryptoObject? = null
|
private var cryptoObject: BiometricPrompt.CryptoObject? = null
|
||||||
|
|
||||||
private var isBiometricInit = false
|
private var isKeyManagerInit = false
|
||||||
private var authenticationCallback: BiometricPrompt.AuthenticationCallback? = null
|
var authenticationCallback: BiometricPrompt.AuthenticationCallback? = null
|
||||||
|
var biometricUnlockCallback: BiometricUnlockCallback? = null
|
||||||
|
|
||||||
private val promptInfoStoreCredential = BiometricPrompt.PromptInfo.Builder().apply {
|
private val promptInfoStoreCredential = BiometricPrompt.PromptInfo.Builder().apply {
|
||||||
setTitle(context.getString(R.string.biometric_prompt_store_credential_title))
|
setTitle(context.getString(R.string.biometric_prompt_store_credential_title))
|
||||||
setDescription(context.getString(R.string.biometric_prompt_store_credential_message))
|
setDescription(context.getString(R.string.biometric_prompt_store_credential_message))
|
||||||
// TODO device credential
|
setConfirmationRequired(true)
|
||||||
|
// TODO device credential #102 #152
|
||||||
/*
|
/*
|
||||||
if (keyguardManager?.isDeviceSecure == true)
|
if (keyguardManager?.isDeviceSecure == true)
|
||||||
setDeviceCredentialAllowed(true)
|
setDeviceCredentialAllowed(true)
|
||||||
@@ -70,8 +71,9 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
|
|||||||
|
|
||||||
private val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply {
|
private val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply {
|
||||||
setTitle(context.getString(R.string.biometric_prompt_extract_credential_title))
|
setTitle(context.getString(R.string.biometric_prompt_extract_credential_title))
|
||||||
setDescription(context.getString(R.string.biometric_prompt_extract_credential_message))
|
//setDescription(context.getString(R.string.biometric_prompt_extract_credential_message))
|
||||||
// TODO device credential
|
setConfirmationRequired(false)
|
||||||
|
// TODO device credential #102 #152
|
||||||
/*
|
/*
|
||||||
if (keyguardManager?.isDeviceSecure == true)
|
if (keyguardManager?.isDeviceSecure == true)
|
||||||
setDeviceCredentialAllowed(true)
|
setDeviceCredentialAllowed(true)
|
||||||
@@ -80,20 +82,20 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
|
|||||||
setNegativeButtonText(context.getString(android.R.string.cancel))
|
setNegativeButtonText(context.getString(android.R.string.cancel))
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
val isBiometricInitialized: Boolean
|
val isKeyManagerInitialized: Boolean
|
||||||
get() {
|
get() {
|
||||||
if (!isBiometricInit) {
|
if (!isKeyManagerInit) {
|
||||||
biometricUnlockCallback.onBiometricException(Exception("Biometric not initialized"))
|
biometricUnlockCallback?.onBiometricException(Exception("Biometric not initialized"))
|
||||||
}
|
}
|
||||||
return isBiometricInit
|
return isKeyManagerInit
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (BiometricManager.from(context).canAuthenticate() != BiometricManager.BIOMETRIC_SUCCESS) {
|
if (BiometricManager.from(context).canAuthenticate() != BiometricManager.BIOMETRIC_SUCCESS) {
|
||||||
// really not much to do when no fingerprint support found
|
// really not much to do when no fingerprint support found
|
||||||
isBiometricInit = false
|
isKeyManagerInit = false
|
||||||
} else {
|
} else {
|
||||||
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
this.keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager?
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.keyStore = KeyStore.getInstance(BIOMETRIC_KEYSTORE)
|
this.keyStore = KeyStore.getInstance(BIOMETRIC_KEYSTORE)
|
||||||
@@ -103,17 +105,19 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
|
|||||||
+ BIOMETRIC_BLOCKS_MODES + "/"
|
+ BIOMETRIC_BLOCKS_MODES + "/"
|
||||||
+ BIOMETRIC_ENCRYPTION_PADDING)
|
+ BIOMETRIC_ENCRYPTION_PADDING)
|
||||||
this.cryptoObject = BiometricPrompt.CryptoObject(cipher!!)
|
this.cryptoObject = BiometricPrompt.CryptoObject(cipher!!)
|
||||||
isBiometricInit = true
|
isKeyManagerInit = (keyStore != null
|
||||||
|
&& keyGenerator != null
|
||||||
|
&& cipher != null)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to initialize the keystore", e)
|
Log.e(TAG, "Unable to initialize the keystore", e)
|
||||||
isBiometricInit = false
|
isKeyManagerInit = false
|
||||||
biometricUnlockCallback.onBiometricException(e)
|
biometricUnlockCallback?.onBiometricException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSecretKey(): SecretKey? {
|
private fun getSecretKey(): SecretKey? {
|
||||||
if (!isBiometricInitialized) {
|
if (!isKeyManagerInitialized) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -139,14 +143,14 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
|
|||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to create a key in keystore", e)
|
Log.e(TAG, "Unable to create a key in keystore", e)
|
||||||
biometricUnlockCallback.onBiometricException(e)
|
biometricUnlockCallback?.onBiometricException(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
return keyStore.getKey(BIOMETRIC_KEYSTORE_KEY, null) as SecretKey?
|
return keyStore.getKey(BIOMETRIC_KEYSTORE_KEY, null) as SecretKey?
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to retrieve the key in keystore", e)
|
Log.e(TAG, "Unable to retrieve the key in keystore", e)
|
||||||
biometricUnlockCallback.onBiometricException(e)
|
biometricUnlockCallback?.onBiometricException(e)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -155,7 +159,7 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
|
|||||||
: (biometricPrompt: BiometricPrompt?,
|
: (biometricPrompt: BiometricPrompt?,
|
||||||
cryptoObject: BiometricPrompt.CryptoObject?,
|
cryptoObject: BiometricPrompt.CryptoObject?,
|
||||||
promptInfo: BiometricPrompt.PromptInfo)->Unit) {
|
promptInfo: BiometricPrompt.PromptInfo)->Unit) {
|
||||||
if (!isBiometricInitialized) {
|
if (!isKeyManagerInitialized) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -168,19 +172,18 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
|
|||||||
|
|
||||||
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
||||||
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
|
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
|
||||||
biometricUnlockCallback.onInvalidKeyException(unrecoverableKeyException)
|
biometricUnlockCallback?.onInvalidKeyException(unrecoverableKeyException)
|
||||||
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
||||||
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
|
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
|
||||||
biometricUnlockCallback.onInvalidKeyException(invalidKeyException)
|
biometricUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to initialize encrypt data", e)
|
Log.e(TAG, "Unable to initialize encrypt data", e)
|
||||||
biometricUnlockCallback.onBiometricException(e)
|
biometricUnlockCallback?.onBiometricException(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun encryptData(value: String) {
|
fun encryptData(value: String) {
|
||||||
if (!isBiometricInitialized) {
|
if (!isKeyManagerInitialized) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -190,21 +193,20 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
|
|||||||
// passes updated iv spec on to callback so this can be stored for decryption
|
// passes updated iv spec on to callback so this can be stored for decryption
|
||||||
cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec ->
|
cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec ->
|
||||||
val ivSpecValue = Base64.encodeToString(spec.iv, Base64.NO_WRAP)
|
val ivSpecValue = Base64.encodeToString(spec.iv, Base64.NO_WRAP)
|
||||||
biometricUnlockCallback.handleEncryptedResult(encryptedBase64, ivSpecValue)
|
biometricUnlockCallback?.handleEncryptedResult(encryptedBase64, ivSpecValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to encrypt data", e)
|
Log.e(TAG, "Unable to encrypt data", e)
|
||||||
biometricUnlockCallback.onBiometricException(e)
|
biometricUnlockCallback?.onBiometricException(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initDecryptData(ivSpecValue: String, actionIfCypherInit
|
fun initDecryptData(ivSpecValue: String, actionIfCypherInit
|
||||||
: (biometricPrompt: BiometricPrompt?,
|
: (biometricPrompt: BiometricPrompt?,
|
||||||
cryptoObject: BiometricPrompt.CryptoObject?,
|
cryptoObject: BiometricPrompt.CryptoObject?,
|
||||||
promptInfo: BiometricPrompt.PromptInfo)->Unit) {
|
promptInfo: BiometricPrompt.PromptInfo)->Unit) {
|
||||||
if (!isBiometricInitialized) {
|
if (!isKeyManagerInitialized) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -224,32 +226,30 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
|
|||||||
deleteEntryKey()
|
deleteEntryKey()
|
||||||
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
||||||
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
|
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
|
||||||
biometricUnlockCallback.onInvalidKeyException(invalidKeyException)
|
biometricUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to initialize decrypt data", e)
|
Log.e(TAG, "Unable to initialize decrypt data", e)
|
||||||
biometricUnlockCallback.onBiometricException(e)
|
biometricUnlockCallback?.onBiometricException(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decryptData(encryptedValue: String) {
|
fun decryptData(encryptedValue: String) {
|
||||||
if (!isBiometricInitialized) {
|
if (!isKeyManagerInitialized) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// actual decryption here
|
// actual decryption here
|
||||||
val encrypted = Base64.decode(encryptedValue, Base64.NO_WRAP)
|
val encrypted = Base64.decode(encryptedValue, Base64.NO_WRAP)
|
||||||
cipher?.doFinal(encrypted)?.let { decrypted ->
|
cipher?.doFinal(encrypted)?.let { decrypted ->
|
||||||
biometricUnlockCallback.handleDecryptedResult(String(decrypted))
|
biometricUnlockCallback?.handleDecryptedResult(String(decrypted))
|
||||||
}
|
}
|
||||||
} catch (badPaddingException: BadPaddingException) {
|
} catch (badPaddingException: BadPaddingException) {
|
||||||
Log.e(TAG, "Unable to decrypt data", badPaddingException)
|
Log.e(TAG, "Unable to decrypt data", badPaddingException)
|
||||||
biometricUnlockCallback.onInvalidKeyException(badPaddingException)
|
biometricUnlockCallback?.onInvalidKeyException(badPaddingException)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to decrypt data", e)
|
Log.e(TAG, "Unable to decrypt data", e)
|
||||||
biometricUnlockCallback.onBiometricException(e)
|
biometricUnlockCallback?.onBiometricException(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteEntryKey() {
|
fun deleteEntryKey() {
|
||||||
@@ -258,14 +258,10 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
|
|||||||
keyStore?.deleteEntry(BIOMETRIC_KEYSTORE_KEY)
|
keyStore?.deleteEntry(BIOMETRIC_KEYSTORE_KEY)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to delete entry key in keystore", e)
|
Log.e(TAG, "Unable to delete entry key in keystore", e)
|
||||||
biometricUnlockCallback.onBiometricException(e)
|
biometricUnlockCallback?.onBiometricException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAuthenticationCallback(authenticationCallback: BiometricPrompt.AuthenticationCallback) {
|
|
||||||
this.authenticationCallback = authenticationCallback
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun initBiometricPrompt() {
|
fun initBiometricPrompt() {
|
||||||
if (biometricPrompt == null) {
|
if (biometricPrompt == null) {
|
||||||
@@ -275,6 +271,10 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun closeBiometricPrompt() {
|
||||||
|
biometricPrompt?.cancelAuthentication()
|
||||||
|
}
|
||||||
|
|
||||||
interface BiometricUnlockErrorCallback {
|
interface BiometricUnlockErrorCallback {
|
||||||
fun onInvalidKeyException(e: Exception)
|
fun onInvalidKeyException(e: Exception)
|
||||||
fun onBiometricException(e: Exception)
|
fun onBiometricException(e: Exception)
|
||||||
@@ -299,22 +299,24 @@ class BiometricUnlockDatabaseHelper(private val context: FragmentActivity,
|
|||||||
* Remove entry key in keystore
|
* Remove entry key in keystore
|
||||||
*/
|
*/
|
||||||
fun deleteEntryKeyInKeystoreForBiometric(context: FragmentActivity,
|
fun deleteEntryKeyInKeystoreForBiometric(context: FragmentActivity,
|
||||||
biometricUnlockCallback: BiometricUnlockErrorCallback) {
|
biometricCallback: BiometricUnlockErrorCallback) {
|
||||||
val fingerPrintHelper = BiometricUnlockDatabaseHelper(context, object : BiometricUnlockCallback {
|
BiometricUnlockDatabaseHelper(context).apply {
|
||||||
|
biometricUnlockCallback = object : BiometricUnlockCallback {
|
||||||
|
|
||||||
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {}
|
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {}
|
||||||
|
|
||||||
override fun handleDecryptedResult(decryptedValue: String) {}
|
override fun handleDecryptedResult(decryptedValue: String) {}
|
||||||
|
|
||||||
override fun onInvalidKeyException(e: Exception) {
|
override fun onInvalidKeyException(e: Exception) {
|
||||||
biometricUnlockCallback.onInvalidKeyException(e)
|
biometricCallback.onInvalidKeyException(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBiometricException(e: Exception) {
|
||||||
|
biometricCallback.onBiometricException(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
deleteEntryKey()
|
||||||
override fun onBiometricException(e: Exception) {
|
}
|
||||||
biometricUnlockCallback.onBiometricException(e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
fingerPrintHelper.deleteEntryKey()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,62 +1,63 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.biometric
|
package com.kunzisoft.keepass.biometric
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Animatable2
|
|
||||||
import android.graphics.drawable.AnimatedVectorDrawable
|
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
||||||
|
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
class FingerPrintAnimatedVector(context: Context, imageView: ImageView) {
|
class FingerPrintAnimatedVector(context: Context, imageView: ImageView) {
|
||||||
|
|
||||||
private val scanFingerprint: AnimatedVectorDrawable =
|
private val scanFingerprint: AnimatedVectorDrawableCompat? =
|
||||||
context.getDrawable(R.drawable.scan_fingerprint) as AnimatedVectorDrawable
|
AnimatedVectorDrawableCompat.create(context, R.drawable.scan_fingerprint)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
imageView.setImageDrawable(scanFingerprint)
|
imageView.setImageDrawable(scanFingerprint)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var animationCallback = object : Animatable2.AnimationCallback() {
|
private var animationCallback = object : Animatable2Compat.AnimationCallback() {
|
||||||
override fun onAnimationEnd(drawable: Drawable) {
|
override fun onAnimationEnd(drawable: Drawable) {
|
||||||
if (!scanFingerprint.isRunning)
|
imageView.post {
|
||||||
scanFingerprint.start()
|
scanFingerprint?.start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startScan() {
|
fun startScan() {
|
||||||
scanFingerprint.registerAnimationCallback(animationCallback)
|
scanFingerprint?.registerAnimationCallback(animationCallback)
|
||||||
|
|
||||||
if (!scanFingerprint.isRunning)
|
if (scanFingerprint?.isRunning != true)
|
||||||
scanFingerprint.start()
|
scanFingerprint?.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopScan() {
|
fun stopScan() {
|
||||||
scanFingerprint.unregisterAnimationCallback(animationCallback)
|
scanFingerprint?.unregisterAnimationCallback(animationCallback)
|
||||||
|
|
||||||
if (scanFingerprint.isRunning)
|
if (scanFingerprint?.isRunning == true)
|
||||||
scanFingerprint.stop()
|
scanFingerprint.stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto
|
package com.kunzisoft.keepass.crypto
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto
|
package com.kunzisoft.keepass.crypto
|
||||||
@@ -24,7 +24,7 @@ import com.kunzisoft.keepass.crypto.engine.AesEngine
|
|||||||
import com.kunzisoft.keepass.crypto.engine.ChaCha20Engine
|
import com.kunzisoft.keepass.crypto.engine.ChaCha20Engine
|
||||||
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
import com.kunzisoft.keepass.crypto.engine.CipherEngine
|
||||||
import com.kunzisoft.keepass.crypto.engine.TwofishEngine
|
import com.kunzisoft.keepass.crypto.engine.TwofishEngine
|
||||||
import org.spongycastle.jce.provider.BouncyCastleProvider
|
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
import java.security.Security
|
import java.security.Security
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -37,20 +37,10 @@ object CipherFactory {
|
|||||||
private var blacklisted: Boolean = false
|
private var blacklisted: Boolean = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||||
Security.addProvider(BouncyCastleProvider())
|
Security.addProvider(BouncyCastleProvider())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class)
|
|
||||||
@JvmOverloads
|
|
||||||
fun getInstance(transformation: String, androidOverride: Boolean = false): Cipher {
|
|
||||||
// Return the native AES if it is possible
|
|
||||||
return if (!deviceBlacklisted() && !androidOverride && hasNativeImplementation(transformation) && NativeLib.loaded()) {
|
|
||||||
Cipher.getInstance(transformation, AESProvider())
|
|
||||||
} else {
|
|
||||||
Cipher.getInstance(transformation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deviceBlacklisted(): Boolean {
|
fun deviceBlacklisted(): Boolean {
|
||||||
if (!blacklistInit) {
|
if (!blacklistInit) {
|
||||||
blacklistInit = true
|
blacklistInit = true
|
||||||
@@ -64,6 +54,16 @@ object CipherFactory {
|
|||||||
return transformation == "AES/CBC/PKCS5Padding"
|
return transformation == "AES/CBC/PKCS5Padding"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class)
|
||||||
|
fun getInstance(transformation: String, androidOverride: Boolean = false): Cipher {
|
||||||
|
// Return the native AES if it is possible
|
||||||
|
return if (!deviceBlacklisted() && !androidOverride && hasNativeImplementation(transformation) && NativeLib.loaded()) {
|
||||||
|
Cipher.getInstance(transformation, AESProvider())
|
||||||
|
} else {
|
||||||
|
Cipher.getInstance(transformation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate appropriate cipher based on KeePass 2.x UUID's
|
* Generate appropriate cipher based on KeePass 2.x UUID's
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,34 +1,36 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto
|
package com.kunzisoft.keepass.crypto
|
||||||
|
|
||||||
enum class CrsAlgorithm constructor(val id: Int) {
|
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||||
|
|
||||||
Null(0),
|
enum class CrsAlgorithm constructor(val id: UnsignedInt) {
|
||||||
ArcFourVariant(1),
|
|
||||||
Salsa20(2),
|
Null(UnsignedInt(0)),
|
||||||
ChaCha20(3);
|
ArcFourVariant(UnsignedInt(1)),
|
||||||
|
Salsa20(UnsignedInt(2)),
|
||||||
|
ChaCha20(UnsignedInt(3));
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun fromId(num: Int): CrsAlgorithm? {
|
fun fromId(num: UnsignedInt): CrsAlgorithm? {
|
||||||
for (e in values()) {
|
for (e in values()) {
|
||||||
if (e.id == num) {
|
if (e.id == num) {
|
||||||
return e
|
return e
|
||||||
|
|||||||
@@ -1,33 +1,31 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto
|
package com.kunzisoft.keepass.crypto
|
||||||
|
|
||||||
import com.kunzisoft.keepass.stream.LEDataOutputStream
|
|
||||||
import com.kunzisoft.keepass.stream.NullOutputStream
|
import com.kunzisoft.keepass.stream.NullOutputStream
|
||||||
|
import com.kunzisoft.keepass.stream.longTo8Bytes
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.security.DigestOutputStream
|
import java.security.DigestOutputStream
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
import java.util.Arrays
|
import java.util.*
|
||||||
|
|
||||||
import javax.crypto.Mac
|
import javax.crypto.Mac
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@@ -60,7 +58,7 @@ object CryptoUtil {
|
|||||||
throw RuntimeException(e)
|
throw RuntimeException(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
val pbR = LEDataOutputStream.writeLongBuf(r)
|
val pbR = longTo8Bytes(r)
|
||||||
val part = hmac.doFinal(pbR)
|
val part = hmac.doFinal(pbR)
|
||||||
|
|
||||||
val copy = min(cbOut - pos, part.size)
|
val copy = min(cbOut - pos, part.size)
|
||||||
@@ -76,12 +74,10 @@ object CryptoUtil {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmOverloads
|
|
||||||
fun hashSha256(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
|
fun hashSha256(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
|
||||||
return hashGen("SHA-256", data, offset, count)
|
return hashGen("SHA-256", data, offset, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmOverloads
|
|
||||||
fun hashSha512(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
|
fun hashSha512(data: ByteArray, offset: Int = 0, count: Int = data.size): ByteArray {
|
||||||
return hashGen("SHA-512", data, offset, count)
|
return hashGen("SHA-512", data, offset, count)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto;
|
package com.kunzisoft.keepass.crypto;
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto
|
package com.kunzisoft.keepass.crypto
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto
|
package com.kunzisoft.keepass.crypto
|
||||||
|
|
||||||
import org.spongycastle.crypto.StreamCipher
|
import org.bouncycastle.crypto.StreamCipher
|
||||||
import org.spongycastle.crypto.engines.ChaCha7539Engine
|
import org.bouncycastle.crypto.engines.ChaCha7539Engine
|
||||||
import org.spongycastle.crypto.engines.Salsa20Engine
|
import org.bouncycastle.crypto.engines.Salsa20Engine
|
||||||
import org.spongycastle.crypto.params.KeyParameter
|
import org.bouncycastle.crypto.params.KeyParameter
|
||||||
import org.spongycastle.crypto.params.ParametersWithIV
|
import org.bouncycastle.crypto.params.ParametersWithIV
|
||||||
|
|
||||||
object StreamCipherFactory {
|
object StreamCipherFactory {
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto.engine
|
package com.kunzisoft.keepass.crypto.engine
|
||||||
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||||
import com.kunzisoft.keepass.database.element.PwEncryptionAlgorithm
|
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.utils.Types
|
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
||||||
import java.security.InvalidAlgorithmParameterException
|
import java.security.InvalidAlgorithmParameterException
|
||||||
import java.security.InvalidKeyException
|
import java.security.InvalidKeyException
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
@@ -41,13 +41,28 @@ class AesEngine : CipherEngine() {
|
|||||||
return cipher
|
return cipher
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPwEncryptionAlgorithm(): PwEncryptionAlgorithm {
|
override fun getPwEncryptionAlgorithm(): EncryptionAlgorithm {
|
||||||
return PwEncryptionAlgorithm.AESRijndael
|
return EncryptionAlgorithm.AESRijndael
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
val CIPHER_UUID: UUID = Types.bytestoUUID(
|
val CIPHER_UUID: UUID = bytes16ToUuid(
|
||||||
byteArrayOf(0x31.toByte(), 0xC1.toByte(), 0xF2.toByte(), 0xE6.toByte(), 0xBF.toByte(), 0x71.toByte(), 0x43.toByte(), 0x50.toByte(), 0xBE.toByte(), 0x58.toByte(), 0x05.toByte(), 0x21.toByte(), 0x6A.toByte(), 0xFC.toByte(), 0x5A.toByte(), 0xFF.toByte()))
|
byteArrayOf(0x31.toByte(),
|
||||||
|
0xC1.toByte(),
|
||||||
|
0xF2.toByte(),
|
||||||
|
0xE6.toByte(),
|
||||||
|
0xBF.toByte(),
|
||||||
|
0x71.toByte(),
|
||||||
|
0x43.toByte(),
|
||||||
|
0x50.toByte(),
|
||||||
|
0xBE.toByte(),
|
||||||
|
0x58.toByte(),
|
||||||
|
0x05.toByte(),
|
||||||
|
0x21.toByte(),
|
||||||
|
0x6A.toByte(),
|
||||||
|
0xFC.toByte(),
|
||||||
|
0x5A.toByte(),
|
||||||
|
0xFF.toByte()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto.engine
|
package com.kunzisoft.keepass.crypto.engine
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.PwEncryptionAlgorithm
|
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.utils.Types
|
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
||||||
import org.spongycastle.jce.provider.BouncyCastleProvider
|
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||||
import java.security.InvalidAlgorithmParameterException
|
import java.security.InvalidAlgorithmParameterException
|
||||||
import java.security.InvalidKeyException
|
import java.security.InvalidKeyException
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
@@ -44,13 +44,28 @@ class ChaCha20Engine : CipherEngine() {
|
|||||||
return cipher
|
return cipher
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPwEncryptionAlgorithm(): PwEncryptionAlgorithm {
|
override fun getPwEncryptionAlgorithm(): EncryptionAlgorithm {
|
||||||
return PwEncryptionAlgorithm.ChaCha20
|
return EncryptionAlgorithm.ChaCha20
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
val CIPHER_UUID: UUID = Types.bytestoUUID(
|
val CIPHER_UUID: UUID = bytes16ToUuid(
|
||||||
byteArrayOf(0xD6.toByte(), 0x03.toByte(), 0x8A.toByte(), 0x2B.toByte(), 0x8B.toByte(), 0x6F.toByte(), 0x4C.toByte(), 0xB5.toByte(), 0xA5.toByte(), 0x24.toByte(), 0x33.toByte(), 0x9A.toByte(), 0x31.toByte(), 0xDB.toByte(), 0xB5.toByte(), 0x9A.toByte()))
|
byteArrayOf(0xD6.toByte(),
|
||||||
|
0x03.toByte(),
|
||||||
|
0x8A.toByte(),
|
||||||
|
0x2B.toByte(),
|
||||||
|
0x8B.toByte(),
|
||||||
|
0x6F.toByte(),
|
||||||
|
0x4C.toByte(),
|
||||||
|
0xB5.toByte(),
|
||||||
|
0xA5.toByte(),
|
||||||
|
0x24.toByte(),
|
||||||
|
0x33.toByte(),
|
||||||
|
0x9A.toByte(),
|
||||||
|
0x31.toByte(),
|
||||||
|
0xDB.toByte(),
|
||||||
|
0xB5.toByte(),
|
||||||
|
0x9A.toByte()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto.engine
|
package com.kunzisoft.keepass.crypto.engine
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.PwEncryptionAlgorithm
|
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||||
|
|
||||||
import java.security.InvalidAlgorithmParameterException
|
import java.security.InvalidAlgorithmParameterException
|
||||||
import java.security.InvalidKeyException
|
import java.security.InvalidKeyException
|
||||||
@@ -46,6 +46,6 @@ abstract class CipherEngine {
|
|||||||
return getCipher(opmode, key, IV, false)
|
return getCipher(opmode, key, IV, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun getPwEncryptionAlgorithm(): PwEncryptionAlgorithm
|
abstract fun getPwEncryptionAlgorithm(): EncryptionAlgorithm
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,31 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePass DX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePass DX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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,
|
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.crypto.engine
|
package com.kunzisoft.keepass.crypto.engine
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
import com.kunzisoft.keepass.crypto.CipherFactory
|
||||||
import com.kunzisoft.keepass.database.element.PwEncryptionAlgorithm
|
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.utils.Types
|
import com.kunzisoft.keepass.stream.bytes16ToUuid
|
||||||
|
|
||||||
import java.security.InvalidAlgorithmParameterException
|
import java.security.InvalidAlgorithmParameterException
|
||||||
import java.security.InvalidKeyException
|
import java.security.InvalidKeyException
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
import java.util.UUID
|
import java.util.*
|
||||||
|
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.NoSuchPaddingException
|
import javax.crypto.NoSuchPaddingException
|
||||||
import javax.crypto.spec.IvParameterSpec
|
import javax.crypto.spec.IvParameterSpec
|
||||||
@@ -47,13 +45,28 @@ class TwofishEngine : CipherEngine() {
|
|||||||
return cipher
|
return cipher
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPwEncryptionAlgorithm(): PwEncryptionAlgorithm {
|
override fun getPwEncryptionAlgorithm(): EncryptionAlgorithm {
|
||||||
return PwEncryptionAlgorithm.Twofish
|
return EncryptionAlgorithm.Twofish
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
val CIPHER_UUID: UUID = Types.bytestoUUID(
|
val CIPHER_UUID: UUID = bytes16ToUuid(
|
||||||
byteArrayOf(0xAD.toByte(), 0x68.toByte(), 0xF2.toByte(), 0x9F.toByte(), 0x57.toByte(), 0x6F.toByte(), 0x4B.toByte(), 0xB9.toByte(), 0xA3.toByte(), 0x6A.toByte(), 0xD4.toByte(), 0x7A.toByte(), 0xF9.toByte(), 0x65.toByte(), 0x34.toByte(), 0x6C.toByte()))
|
byteArrayOf(0xAD.toByte(),
|
||||||
|
0x68.toByte(),
|
||||||
|
0xF2.toByte(),
|
||||||
|
0x9F.toByte(),
|
||||||
|
0x57.toByte(),
|
||||||
|
0x6F.toByte(),
|
||||||
|
0x4B.toByte(),
|
||||||
|
0xB9.toByte(),
|
||||||
|
0xA3.toByte(),
|
||||||
|
0x6A.toByte(),
|
||||||
|
0xD4.toByte(),
|
||||||
|
0x7A.toByte(),
|
||||||
|
0xF9.toByte(),
|
||||||
|
0x65.toByte(),
|
||||||
|
0x34.toByte(),
|
||||||
|
0x6C.toByte()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.crypto.finalkey
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.crypto.CipherFactory.deviceBlacklisted
|
||||||
|
|
||||||
|
object AESKeyTransformerFactory : KeyTransformer() {
|
||||||
|
override fun transformMasterKey(seed: ByteArray?, key: ByteArray?, rounds: Long?): ByteArray? {
|
||||||
|
// Prefer the native final key implementation
|
||||||
|
val keyTransformer = if (!deviceBlacklisted()
|
||||||
|
&& NativeAESKeyTransformer.available()) {
|
||||||
|
NativeAESKeyTransformer()
|
||||||
|
} else {
|
||||||
|
// Fall back on the android crypto implementation
|
||||||
|
AndroidAESKeyTransformer()
|
||||||
|
}
|
||||||
|
return keyTransformer.transformMasterKey(seed, key, rounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.crypto.finalkey
|
||||||
|
|
||||||
|
import java.io.IOException
|
||||||
|
import java.security.InvalidKeyException
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.ShortBufferException
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
class AndroidAESKeyTransformer : KeyTransformer() {
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun transformMasterKey(seed: ByteArray?, key: ByteArray?, rounds: Long?): ByteArray? {
|
||||||
|
val cipher: Cipher = try {
|
||||||
|
Cipher.getInstance("AES/ECB/NoPadding")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IOException("Unable to get the cipher", e)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(seed, "AES"))
|
||||||
|
} catch (e: InvalidKeyException) {
|
||||||
|
throw IOException("Unable to init the cipher", e)
|
||||||
|
}
|
||||||
|
if (key == null) {
|
||||||
|
throw IOException("Invalid key")
|
||||||
|
}
|
||||||
|
if (rounds == null) {
|
||||||
|
throw IOException("Invalid rounds")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt key rounds times
|
||||||
|
val keyLength = key.size
|
||||||
|
val newKey = ByteArray(keyLength)
|
||||||
|
System.arraycopy(key, 0, newKey, 0, keyLength)
|
||||||
|
val destKey = ByteArray(keyLength)
|
||||||
|
for (i in 0 until rounds) {
|
||||||
|
try {
|
||||||
|
cipher.update(newKey, 0, newKey.size, destKey, 0)
|
||||||
|
System.arraycopy(destKey, 0, newKey, 0, newKey.size)
|
||||||
|
} catch (e: ShortBufferException) {
|
||||||
|
throw IOException("Short buffer", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash the key
|
||||||
|
val messageDigest: MessageDigest = try {
|
||||||
|
MessageDigest.getInstance("SHA-256")
|
||||||
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
|
throw IOException("SHA-256 not implemented here: " + e.message)
|
||||||
|
}
|
||||||
|
messageDigest.update(newKey)
|
||||||
|
return messageDigest.digest()
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user