Compare commits
900 Commits
3.0.0_beta
...
3.4.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c0f1a036b | ||
|
|
831b649cbb | ||
|
|
ded3c204b9 | ||
|
|
23eec5f066 | ||
|
|
6c167090e1 | ||
|
|
7d9eca0d46 | ||
|
|
c551aff474 | ||
|
|
e627745358 | ||
|
|
5a30d9d2b5 | ||
|
|
0a46817bbc | ||
|
|
a4134fa8c8 | ||
|
|
683535a5a6 | ||
|
|
edb53112c2 | ||
|
|
83a77af520 | ||
|
|
df3ae17c7b | ||
|
|
4a1624a443 | ||
|
|
a8de9f9f9f | ||
|
|
3aa5b40acd | ||
|
|
8400f3e874 | ||
|
|
b40bca1913 | ||
|
|
7100257f31 | ||
|
|
17df1a4d8a | ||
|
|
d7a5209c68 | ||
|
|
076220eacd | ||
|
|
99a50f271a | ||
|
|
63d265da06 | ||
|
|
30e3624eb1 | ||
|
|
88f3713e28 | ||
|
|
90f0c22545 | ||
|
|
8deed8468d | ||
|
|
923ad26b1b | ||
|
|
3bc858e4c2 | ||
|
|
f5a7fa41a7 | ||
|
|
bf71d5508b | ||
|
|
aa5adc28cb | ||
|
|
2dad013cc0 | ||
|
|
7ade66f3ac | ||
|
|
ed75a64b46 | ||
|
|
e156b80d91 | ||
|
|
90e4862280 | ||
|
|
438080d3d6 | ||
|
|
3c17605764 | ||
|
|
3f68bc0eda | ||
|
|
3e4452da00 | ||
|
|
549c690b56 | ||
|
|
aabe06f29b | ||
|
|
82693c5cd3 | ||
|
|
37a4f26d2f | ||
|
|
ca94063c7b | ||
|
|
eadc4bf6c2 | ||
|
|
b1c307c86b | ||
|
|
48331f9552 | ||
|
|
f907aa578a | ||
|
|
41e2620cc1 | ||
|
|
e7a82b167a | ||
|
|
088c556b00 | ||
|
|
c80343b6d4 | ||
|
|
4e52a8cf60 | ||
|
|
1ed1d4233f | ||
|
|
6e4626bc02 | ||
|
|
2608ae247f | ||
|
|
785586bfe9 | ||
|
|
bdcbb177ae | ||
|
|
15ac365d79 | ||
|
|
debbcb753b | ||
|
|
69d73aeaa4 | ||
|
|
dffe53370f | ||
|
|
4334e6dcdf | ||
|
|
c2c6c093d5 | ||
|
|
77e539eec2 | ||
|
|
a57970210e | ||
|
|
1b31a46fb7 | ||
|
|
87f19c74fc | ||
|
|
bd157a9724 | ||
|
|
5a327eb0db | ||
|
|
4b9c0b0109 | ||
|
|
df6b75cdbb | ||
|
|
0b4f8c122b | ||
|
|
2a87eaf3e5 | ||
|
|
c52266f5cf | ||
|
|
3b21f8add2 | ||
|
|
6574bd10a0 | ||
|
|
23f3335988 | ||
|
|
a5d7f33c82 | ||
|
|
3782c4dac0 | ||
|
|
1fc02fd2fe | ||
|
|
cc347c1dbe | ||
|
|
79ff20eb18 | ||
|
|
e6e8a447da | ||
|
|
233f0c5bdb | ||
|
|
9ed4271a14 | ||
|
|
470c0b6b43 | ||
|
|
afa8ae42b9 | ||
|
|
63d426503f | ||
|
|
ffb7f80b26 | ||
|
|
63f8826fd8 | ||
|
|
ef836e8b84 | ||
|
|
abc1c43a51 | ||
|
|
6b54dd9e0d | ||
|
|
1f54e7752d | ||
|
|
6ac941f276 | ||
|
|
a4fe92562f | ||
|
|
b9bd1d9d4b | ||
|
|
3b6c28488a | ||
|
|
875eb3500d | ||
|
|
3a88a2451c | ||
|
|
6800b73a4f | ||
|
|
983404e6d8 | ||
|
|
b95c0a18a7 | ||
|
|
36b317cad8 | ||
|
|
35d74888fb | ||
|
|
6c308483f7 | ||
|
|
9d25fb74ec | ||
|
|
d217b52744 | ||
|
|
319da4b174 | ||
|
|
9bee467942 | ||
|
|
44ac70fc97 | ||
|
|
d897611d62 | ||
|
|
c2ae251e73 | ||
|
|
35ad285864 | ||
|
|
97bdae21eb | ||
|
|
d6dc6e43c7 | ||
|
|
01e6e530d5 | ||
|
|
9ec0178beb | ||
|
|
d66f2f6d24 | ||
|
|
79cd4004cc | ||
|
|
991243e2df | ||
|
|
b91cf11d86 | ||
|
|
d182ec09fa | ||
|
|
8641822358 | ||
|
|
9665cbb428 | ||
|
|
a280dfaf3b | ||
|
|
3e56521ea8 | ||
|
|
b205230ea9 | ||
|
|
51645ab126 | ||
|
|
5d04897e75 | ||
|
|
1ac0ea5cc6 | ||
|
|
a07e8b51e5 | ||
|
|
a81f0238f4 | ||
|
|
2b81eb8ec7 | ||
|
|
e5eb642781 | ||
|
|
a4cbe25733 | ||
|
|
2042c85b22 | ||
|
|
3149f8745c | ||
|
|
15b9f1616f | ||
|
|
94c02b7288 | ||
|
|
7e70b59a59 | ||
|
|
2c7f5e41ed | ||
|
|
108b8df280 | ||
|
|
553098f9be | ||
|
|
131eb78407 | ||
|
|
f956a279a5 | ||
|
|
7150686b92 | ||
|
|
4b1fb2c173 | ||
|
|
94464bf608 | ||
|
|
2faa88784a | ||
|
|
e6607b53d8 | ||
|
|
3f6a6c864a | ||
|
|
30a578257d | ||
|
|
8411134adf | ||
|
|
f86a5d1a19 | ||
|
|
be72492537 | ||
|
|
76f9e8ec6e | ||
|
|
8fb1c44e58 | ||
|
|
f607b35cf3 | ||
|
|
0e56bec35a | ||
|
|
c890d10114 | ||
|
|
dee2fe5ce7 | ||
|
|
4a4d767bce | ||
|
|
d57e0cf601 | ||
|
|
7fa141dd1b | ||
|
|
c261a0cbca | ||
|
|
66661cbd49 | ||
|
|
100c126c3d | ||
|
|
d466e3077d | ||
|
|
24587dc34e | ||
|
|
32cc57dd03 | ||
|
|
a55488846b | ||
|
|
dcf61fd4e2 | ||
|
|
5bf998468a | ||
|
|
01c9625c59 | ||
|
|
772c378922 | ||
|
|
ee50a91379 | ||
|
|
9cfda3bad8 | ||
|
|
aa19b08bd9 | ||
|
|
87f69bb7e2 | ||
|
|
41c0aeedbe | ||
|
|
3cbe53d76f | ||
|
|
aed60d6c1e | ||
|
|
be7d35490d | ||
|
|
d0ea997c63 | ||
|
|
1fecffeba2 | ||
|
|
76319a56a2 | ||
|
|
1d71de7031 | ||
|
|
e9a1cfea11 | ||
|
|
9115856d19 | ||
|
|
8c45266c18 | ||
|
|
6b4130df89 | ||
|
|
e3e10e7dfa | ||
|
|
cbf900004d | ||
|
|
51b36dc460 | ||
|
|
9f8016afe2 | ||
|
|
d5a36db50a | ||
|
|
ecd458d8d0 | ||
|
|
9088297c41 | ||
|
|
5282deb088 | ||
|
|
75d661f12b | ||
|
|
83bc769d9e | ||
|
|
8324acadc8 | ||
|
|
6e42db41be | ||
|
|
3917bfc9e6 | ||
|
|
d11febb1ce | ||
|
|
360eb6f9cc | ||
|
|
e4ac5d01d0 | ||
|
|
6a51fc0668 | ||
|
|
ba03f07fbe | ||
|
|
7bf7d63f64 | ||
|
|
d3efaabc24 | ||
|
|
b4283ed98b | ||
|
|
de407e4cf9 | ||
|
|
60ed3a9836 | ||
|
|
7948358d85 | ||
|
|
96b82bb9b2 | ||
|
|
699ccf13f0 | ||
|
|
ae88aa4e42 | ||
|
|
bcce13b12f | ||
|
|
4fd4f660a7 | ||
|
|
518e59b33c | ||
|
|
165cdcc00d | ||
|
|
48c2115fdf | ||
|
|
d7d68ccdeb | ||
|
|
5cf6362db4 | ||
|
|
4efcc48160 | ||
|
|
383274ce0f | ||
|
|
c9dec3a2f7 | ||
|
|
2d4bf2903b | ||
|
|
2b88cfbda0 | ||
|
|
eb6ab7a156 | ||
|
|
9bed7bf213 | ||
|
|
99c2796014 | ||
|
|
9ee9bf12ae | ||
|
|
688cbe50f2 | ||
|
|
e0577d1628 | ||
|
|
de70925f8a | ||
|
|
0f8dd17fde | ||
|
|
4bc8a08606 | ||
|
|
cf34433186 | ||
|
|
21113d6fc8 | ||
|
|
d7decba3f5 | ||
|
|
671364f395 | ||
|
|
d0072237e7 | ||
|
|
d11c001e0d | ||
|
|
a197453ce0 | ||
|
|
d1586a8d80 | ||
|
|
6e959e415f | ||
|
|
8a8e14701b | ||
|
|
60bb144020 | ||
|
|
b561db3a67 | ||
|
|
b0e722acce | ||
|
|
f79d32b22b | ||
|
|
3579606d1e | ||
|
|
b93c6266a6 | ||
|
|
ea9c530667 | ||
|
|
6ea95c050a | ||
|
|
5a99a28195 | ||
|
|
0cd5351c07 | ||
|
|
148bed801b | ||
|
|
87a1a3ba1b | ||
|
|
20d2c10bba | ||
|
|
3eed5e395e | ||
|
|
870fbaa05c | ||
|
|
55868f68da | ||
|
|
e6f0fbeab5 | ||
|
|
8a554349b5 | ||
|
|
ab87d4e564 | ||
|
|
8400a83b70 | ||
|
|
5d2caa37a9 | ||
|
|
a6b7cfc2d3 | ||
|
|
74107b90bb | ||
|
|
9f4a915d43 | ||
|
|
3f1f22e1c3 | ||
|
|
1d98eb2b95 | ||
|
|
f1b56fed16 | ||
|
|
23e75c56ed | ||
|
|
eafc9cc1fa | ||
|
|
3e3dbb22df | ||
|
|
127b8ba0bd | ||
|
|
8a8e588a01 | ||
|
|
3c1b010821 | ||
|
|
739c938576 | ||
|
|
69fed0c347 | ||
|
|
d040258296 | ||
|
|
abee18839b | ||
|
|
9c0eb4e27e | ||
|
|
10598ef5e5 | ||
|
|
fa09e2d21d | ||
|
|
11ddfe3445 | ||
|
|
9abbc8876b | ||
|
|
c5cf99e13d | ||
|
|
82ac1c8f5e | ||
|
|
837f8146e4 | ||
|
|
9698c3f2ee | ||
|
|
85290b8f9d | ||
|
|
1ccf48d7a5 | ||
|
|
2cbec9f2b6 | ||
|
|
692be415fa | ||
|
|
83d5218f92 | ||
|
|
324b016324 | ||
|
|
005c9673f7 | ||
|
|
b8762dc6e0 | ||
|
|
93abff0768 | ||
|
|
ac1cfb43d8 | ||
|
|
30ec712782 | ||
|
|
bbd93fbc5b | ||
|
|
c46cbaff4b | ||
|
|
1e979b256a | ||
|
|
50429b419f | ||
|
|
2c540be5a3 | ||
|
|
155d3d222f | ||
|
|
3fc3bf1302 | ||
|
|
f0fafbbc6e | ||
|
|
5f7d980d89 | ||
|
|
c3799ed7fe | ||
|
|
f7533af5d8 | ||
|
|
2c9504fb93 | ||
|
|
c22b3f5335 | ||
|
|
2aea7fd46d | ||
|
|
bb9dda0cf1 | ||
|
|
ced6f05e59 | ||
|
|
cc14b5d4d4 | ||
|
|
2922f7110d | ||
|
|
762a877c5e | ||
|
|
aace7a8260 | ||
|
|
ac91abcb45 | ||
|
|
5d6ccd84c9 | ||
|
|
bd2f980f8e | ||
|
|
69af330ba1 | ||
|
|
b7bc3bfa8f | ||
|
|
678663ad66 | ||
|
|
ec3aa14112 | ||
|
|
3cdd98dc1a | ||
|
|
fc3270efd2 | ||
|
|
0dd871d3b5 | ||
|
|
41108ff407 | ||
|
|
99de8cf220 | ||
|
|
a60e887eb3 | ||
|
|
5133cb5c8f | ||
|
|
1607d84c21 | ||
|
|
cc0a990af8 | ||
|
|
fec5bdcfc8 | ||
|
|
648514d150 | ||
|
|
37b9745b6b | ||
|
|
f858b1144c | ||
|
|
c7cd7b83fa | ||
|
|
3cef086b69 | ||
|
|
ca51f591de | ||
|
|
da63404a84 | ||
|
|
de2160f992 | ||
|
|
8b7bb36e66 | ||
|
|
6ade91dafa | ||
|
|
2ef66f3011 | ||
|
|
5b2fd0bfbb | ||
|
|
ab392e2cf3 | ||
|
|
8eb922e93f | ||
|
|
20900dce07 | ||
|
|
c2705ff5d2 | ||
|
|
8af70fa7b5 | ||
|
|
e7eb8099ac | ||
|
|
cedf2eafbb | ||
|
|
e7553c68a0 | ||
|
|
cea7f1c2d1 | ||
|
|
45937f9c9c | ||
|
|
ee2a1ea924 | ||
|
|
1851c205e9 | ||
|
|
c4a3947cb3 | ||
|
|
d3e76bcf21 | ||
|
|
cf4e64d6c5 | ||
|
|
4cac571f1b | ||
|
|
445ed92ff7 | ||
|
|
6686ce15c1 | ||
|
|
59f134e0cd | ||
|
|
aab3f8c56f | ||
|
|
17c26e2a96 | ||
|
|
aec9124ef5 | ||
|
|
2df1e9bc2e | ||
|
|
6ceecbfd0f | ||
|
|
460726cadb | ||
|
|
32baa7b9f1 | ||
|
|
f44e648ccc | ||
|
|
2ad385acb6 | ||
|
|
ebaec2eaf0 | ||
|
|
dac8c2c15d | ||
|
|
60965491b0 | ||
|
|
19a3faa85a | ||
|
|
3779346eed | ||
|
|
185e263ddd | ||
|
|
8c0d984955 | ||
|
|
8d1012eda0 | ||
|
|
03f147e3d0 | ||
|
|
13ef5d640c | ||
|
|
2e8eed5afe | ||
|
|
54653a5bea | ||
|
|
e29082eba3 | ||
|
|
b635e9bb0d | ||
|
|
179bfdc3d2 | ||
|
|
631cb104cd | ||
|
|
1aeaf6855e | ||
|
|
6bac1ace30 | ||
|
|
488bab7c4d | ||
|
|
69fbfd7723 | ||
|
|
7697713c44 | ||
|
|
ee9b072f45 | ||
|
|
741defd31e | ||
|
|
e259b37f74 | ||
|
|
9a87de797c | ||
|
|
5d44ba658e | ||
|
|
00518b8231 | ||
|
|
6a3a99d2ac | ||
|
|
819434968c | ||
|
|
9f55ca2fdb | ||
|
|
7f69563edb | ||
|
|
b5cf0f987e | ||
|
|
28ad0b39c3 | ||
|
|
b4e9040d5c | ||
|
|
18a6ff0aa5 | ||
|
|
66e8c25265 | ||
|
|
aba1f2d35b | ||
|
|
5f29bcea8f | ||
|
|
fac6fd7926 | ||
|
|
5f4ab201af | ||
|
|
9591d36f9d | ||
|
|
a38f34995d | ||
|
|
f72564e48d | ||
|
|
39ae743c64 | ||
|
|
9cdb355878 | ||
|
|
6bdabbc96b | ||
|
|
d9e7d5ff6f | ||
|
|
9f41da7868 | ||
|
|
d4f7258ed1 | ||
|
|
aa34d78052 | ||
|
|
72712e8e0e | ||
|
|
bf25054ef2 | ||
|
|
32efafb404 | ||
|
|
3748ba1afa | ||
|
|
5baf91dc77 | ||
|
|
a152a48402 | ||
|
|
5fb4c4c20c | ||
|
|
80cf4f05f8 | ||
|
|
ce8e532f61 | ||
|
|
7e04fcbb6c | ||
|
|
71c37d0b8b | ||
|
|
518375e29a | ||
|
|
25c845c1c7 | ||
|
|
6a468b339f | ||
|
|
05a52dd482 | ||
|
|
5f1413ea1f | ||
|
|
84e45482a4 | ||
|
|
32bfdca562 | ||
|
|
547971545e | ||
|
|
c12b16faf9 | ||
|
|
858d6c8723 | ||
|
|
0f021fae9f | ||
|
|
5cb42264c5 | ||
|
|
c17af4098f | ||
|
|
e6c094b433 | ||
|
|
b15a86618b | ||
|
|
2e66cee551 | ||
|
|
c6e3d0125a | ||
|
|
f60f7d32dc | ||
|
|
dd7175d0c6 | ||
|
|
ba0e87f32a | ||
|
|
cebba5a09d | ||
|
|
7b0c31c641 | ||
|
|
487be74e83 | ||
|
|
3c7c88e0f5 | ||
|
|
e876e89b41 | ||
|
|
065fd9632c | ||
|
|
f7d8641e31 | ||
|
|
ed6456599b | ||
|
|
649538846a | ||
|
|
7468db5269 | ||
|
|
8291e94de4 | ||
|
|
94d30fc7ec | ||
|
|
36c6f371c5 | ||
|
|
94b3f66e14 | ||
|
|
c9527ddacd | ||
|
|
805c728e92 | ||
|
|
7812a86adb | ||
|
|
d22defcd83 | ||
|
|
d06829ff7b | ||
|
|
9110a1aca6 | ||
|
|
d7c90601d0 | ||
|
|
7b5fc600f5 | ||
|
|
0e699a1918 | ||
|
|
49f9964bc7 | ||
|
|
ee3ec4b14d | ||
|
|
cdcc9f0aff | ||
|
|
d56a01998f | ||
|
|
4e7f7f7fa0 | ||
|
|
201d8f8aee | ||
|
|
3847cb4d2d | ||
|
|
89a338ac33 | ||
|
|
0084d113d3 | ||
|
|
6d49cc3577 | ||
|
|
910c45a6be | ||
|
|
1d16ad764f | ||
|
|
142021c849 | ||
|
|
c87b9768b1 | ||
|
|
fbc3c1a9a4 | ||
|
|
05d1656a9e | ||
|
|
98804db478 | ||
|
|
c90f18c45a | ||
|
|
553783bfce | ||
|
|
fbf67f28f5 | ||
|
|
b8005466cd | ||
|
|
82177a386e | ||
|
|
176b276835 | ||
|
|
a4732194ef | ||
|
|
4a815daf4d | ||
|
|
8aa7804e8d | ||
|
|
3b7086627d | ||
|
|
20fb4036d1 | ||
|
|
3679ad3d70 | ||
|
|
63f23c571f | ||
|
|
7e96303c7d | ||
|
|
4f5879179e | ||
|
|
5a81f54a1b | ||
|
|
d8ccaf3578 | ||
|
|
7d0848f22b | ||
|
|
b02cffab56 | ||
|
|
59b6deb7be | ||
|
|
1b734051d5 | ||
|
|
037fe41961 | ||
|
|
7bf1263c04 | ||
|
|
0e4afd4681 | ||
|
|
21930021ed | ||
|
|
748b5c54c9 | ||
|
|
82d305008b | ||
|
|
1f24955533 | ||
|
|
ccfa28b895 | ||
|
|
2e3562d87e | ||
|
|
f1be109c15 | ||
|
|
b09bd39ad4 | ||
|
|
d812c3d61b | ||
|
|
0d7772a4c6 | ||
|
|
a5abf4d186 | ||
|
|
c938a13482 | ||
|
|
0bfc395986 | ||
|
|
60c536f444 | ||
|
|
8129e6e0c1 | ||
|
|
223a8e9a5e | ||
|
|
0e21c75007 | ||
|
|
6ca031e8fd | ||
|
|
f02e86fb50 | ||
|
|
b17f8244da | ||
|
|
4c02ce138b | ||
|
|
5eba208b00 | ||
|
|
2a4fbbfb35 | ||
|
|
5dd8bdcae6 | ||
|
|
c3e82eea5d | ||
|
|
2cfb96be33 | ||
|
|
715eedfab1 | ||
|
|
7f3a1c9a7d | ||
|
|
8413d2b31a | ||
|
|
04a03da382 | ||
|
|
e3274657ea | ||
|
|
f3b25cb792 | ||
|
|
d181f886fe | ||
|
|
616d073395 | ||
|
|
d36fc19585 | ||
|
|
95d9e07e2f | ||
|
|
25077a7b9a | ||
|
|
6971cd1a6b | ||
|
|
1e43a65743 | ||
|
|
2f2cbf343e | ||
|
|
3426b3cdeb | ||
|
|
e1d4c172f4 | ||
|
|
4c4a67afaf | ||
|
|
d33f210940 | ||
|
|
ead59bd410 | ||
|
|
f9445de71f | ||
|
|
c26995779a | ||
|
|
c6277453a3 | ||
|
|
91ebf2ba6f | ||
|
|
0f87bdd5a3 | ||
|
|
9e97042dd1 | ||
|
|
b7c4a99e71 | ||
|
|
48e315453e | ||
|
|
a8a6d14ca3 | ||
|
|
e895dd3430 | ||
|
|
f59859137a | ||
|
|
dee92e9e40 | ||
|
|
6701f4f95e | ||
|
|
e20f769854 | ||
|
|
4f762a9432 | ||
|
|
3c49eb1635 | ||
|
|
bdc6a282e2 | ||
|
|
8392ab2cc4 | ||
|
|
93c7c09f8c | ||
|
|
120116414f | ||
|
|
11794e5819 | ||
|
|
edce3d7bec | ||
|
|
e133e32e7c | ||
|
|
471859e448 | ||
|
|
d8de66eb14 | ||
|
|
cfdc0237d7 | ||
|
|
05fad24eda | ||
|
|
d4818c5567 | ||
|
|
8e8e6a7b93 | ||
|
|
6547f0ffad | ||
|
|
6f172fffa8 | ||
|
|
ed16e06676 | ||
|
|
1874f06f42 | ||
|
|
e9db24429a | ||
|
|
a59f4d45ca | ||
|
|
c7b3e0926c | ||
|
|
f0f5258bc9 | ||
|
|
12c07cf793 | ||
|
|
d2b8c85015 | ||
|
|
b9652291bd | ||
|
|
b0d1f93bfc | ||
|
|
a6d6c247a8 | ||
|
|
553416c927 | ||
|
|
b83696bc60 | ||
|
|
23ce320d75 | ||
|
|
dd170aafee | ||
|
|
27d5733dbc | ||
|
|
9ba769c53e | ||
|
|
8ff57e3004 | ||
|
|
5c75c6c7d3 | ||
|
|
56efb20ffa | ||
|
|
699578bb59 | ||
|
|
8d7d01bf88 | ||
|
|
0bc37d2fc2 | ||
|
|
aaa1655af1 | ||
|
|
f1bd4e1bba | ||
|
|
f74147070a | ||
|
|
20f4ea93e4 | ||
|
|
15a28e7c83 | ||
|
|
c550e1de54 | ||
|
|
ab26e561fd | ||
|
|
66968a28a3 | ||
|
|
37d1f91224 | ||
|
|
9e69068d42 | ||
|
|
8e2a9fcd01 | ||
|
|
1753887916 | ||
|
|
21bcffcc87 | ||
|
|
1caed49c75 | ||
|
|
619ea35168 | ||
|
|
877f913e8f | ||
|
|
25c47390c0 | ||
|
|
b3c46348a1 | ||
|
|
6f154194f1 | ||
|
|
c3ab08ce17 | ||
|
|
004fffa992 | ||
|
|
d6bd80c9c0 | ||
|
|
318bcdd011 | ||
|
|
3076f2af68 | ||
|
|
3dd9ef5564 | ||
|
|
367e5fa84e | ||
|
|
97cd61fd13 | ||
|
|
869bf7a345 | ||
|
|
9e15ac242d | ||
|
|
84943e58f1 | ||
|
|
2289bf0a27 | ||
|
|
7fda40c983 | ||
|
|
7eeed8f670 | ||
|
|
4d3f4ed5c2 | ||
|
|
145a4f5c20 | ||
|
|
9afe3d26e9 | ||
|
|
b73a7f1ed8 | ||
|
|
91a2bc3862 | ||
|
|
78a8a840b0 | ||
|
|
f4d54b6ca3 | ||
|
|
bc7a1c332c | ||
|
|
0e75cb9095 | ||
|
|
41b6fb6dcd | ||
|
|
2ca3cbc88f | ||
|
|
d05641a3d6 | ||
|
|
28bf84e05c | ||
|
|
ff51b53660 | ||
|
|
8b8e034b18 | ||
|
|
39927b06e3 | ||
|
|
66db2e7d16 | ||
|
|
a927c33ef1 | ||
|
|
17bc18b881 | ||
|
|
aa643c4a82 | ||
|
|
836fbea676 | ||
|
|
045049243c | ||
|
|
b9813a3494 | ||
|
|
9b42a93ce1 | ||
|
|
8502bceef1 | ||
|
|
663387476f | ||
|
|
daafd83df9 | ||
|
|
f780f2725b | ||
|
|
483aca871a | ||
|
|
352e709c3b | ||
|
|
629057b2c1 | ||
|
|
0e5f53596d | ||
|
|
0d91f07646 | ||
|
|
db882a26ab | ||
|
|
35c8ea22b1 | ||
|
|
7f01619358 | ||
|
|
ee109b4ceb | ||
|
|
7a398e5453 | ||
|
|
23a548f9b4 | ||
|
|
d4655d7034 | ||
|
|
9feb96b541 | ||
|
|
e939278193 | ||
|
|
d4ef1a2617 | ||
|
|
5f8746ced3 | ||
|
|
40a063e94f | ||
|
|
8f5439b958 | ||
|
|
e347f57d8b | ||
|
|
7169b15fd8 | ||
|
|
f9def8c96f | ||
|
|
ef43837af1 | ||
|
|
2efb8e8b8c | ||
|
|
e5bb69ea5f | ||
|
|
0979ca607d | ||
|
|
6ae186b2af | ||
|
|
98fb27f77d | ||
|
|
d68510bbaa | ||
|
|
71fdd2d92d | ||
|
|
3656689ff3 | ||
|
|
7d78406db6 | ||
|
|
ac47748e41 | ||
|
|
80f9b46479 | ||
|
|
999f1bf47a | ||
|
|
9e114eb2b8 | ||
|
|
4177d34b00 | ||
|
|
3ec5c04bf6 | ||
|
|
a877c068b6 | ||
|
|
6a3db90c1e | ||
|
|
a079e0d864 | ||
|
|
719776d66e | ||
|
|
c5af1241e9 | ||
|
|
27e4d7b563 | ||
|
|
450ab34721 | ||
|
|
3e2d4eae2c | ||
|
|
d89b6529ef | ||
|
|
5caf11556a | ||
|
|
78cc6f0f40 | ||
|
|
0007cd4668 | ||
|
|
05195e41de | ||
|
|
66f44ef87d | ||
|
|
a0585d9b11 | ||
|
|
5067946b13 | ||
|
|
f52241d5a8 | ||
|
|
04ccb25fa3 | ||
|
|
5a3f4b60b8 | ||
|
|
4408b2e488 | ||
|
|
9a26acee35 | ||
|
|
6ac377348b | ||
|
|
daeb88d4f4 | ||
|
|
47bf199f52 | ||
|
|
91540b022d | ||
|
|
505a51b6b5 | ||
|
|
28400488aa | ||
|
|
45ae600289 | ||
|
|
8be6874651 | ||
|
|
f694a500e0 | ||
|
|
c415fa01fc | ||
|
|
5225a9459c | ||
|
|
2974b150af | ||
|
|
cf353c8067 | ||
|
|
3aacb5d8b3 | ||
|
|
114fbdbe01 | ||
|
|
f1f83cbec4 | ||
|
|
335c28c2c9 | ||
|
|
daf12cbcce | ||
|
|
bf56eca003 | ||
|
|
c3542224ae | ||
|
|
a12b7fd58a | ||
|
|
98d004edbf | ||
|
|
67586a98b3 | ||
|
|
429faf44db | ||
|
|
c6d8911883 | ||
|
|
69ad7979ae | ||
|
|
12e398ce9b | ||
|
|
5c4a202616 | ||
|
|
adc1ec8444 | ||
|
|
0227d2fcb4 | ||
|
|
e87caac723 | ||
|
|
95abe3b5ac | ||
|
|
133a902c54 | ||
|
|
494544a4c2 | ||
|
|
aacb03d9ef | ||
|
|
fbeaa1781f | ||
|
|
ca73aad538 | ||
|
|
94b6118bf7 | ||
|
|
4a4bfefd17 | ||
|
|
7d6d86c6ff | ||
|
|
6796b0cd2a | ||
|
|
29e1f824b0 | ||
|
|
51263a2911 | ||
|
|
dd7f857475 | ||
|
|
bedc327e65 | ||
|
|
f0fdd4a537 | ||
|
|
9b847a0561 | ||
|
|
469e76b80a | ||
|
|
9c6994b476 | ||
|
|
52ba487617 | ||
|
|
21c57c9484 | ||
|
|
9b1ea6a07a | ||
|
|
f7d7bb0ea3 | ||
|
|
dd96b9ef53 | ||
|
|
b6b01893ba | ||
|
|
ad531d793d | ||
|
|
a9dd11e24a | ||
|
|
115983830b | ||
|
|
442cece081 | ||
|
|
14e08457b9 | ||
|
|
fabcc08cd5 | ||
|
|
7750843b04 | ||
|
|
40a893a8c9 | ||
|
|
37ebd30a4d | ||
|
|
c03188e976 | ||
|
|
d7da1ce333 | ||
|
|
dd9ee8c3f8 | ||
|
|
34bbd8f439 | ||
|
|
053f57cff5 | ||
|
|
365d2e2844 | ||
|
|
c0ac01a34a | ||
|
|
22c0bc0adb | ||
|
|
1b88f2ddf0 | ||
|
|
b4f2a1eb89 | ||
|
|
e9fc9cbc2a | ||
|
|
b809180a1b | ||
|
|
ecc75df3a1 | ||
|
|
d1b6863143 | ||
|
|
faf27143aa | ||
|
|
aee58a4475 | ||
|
|
c4cbf07d78 | ||
|
|
c5e07f643f | ||
|
|
818f5820d5 | ||
|
|
77c6e28876 | ||
|
|
fb7f66012d | ||
|
|
0f7f7bbe6c | ||
|
|
8dedb8deb4 | ||
|
|
dbb2c10bba | ||
|
|
67a5eef7d6 | ||
|
|
979f651251 | ||
|
|
3b93cbb009 | ||
|
|
30c63bfc4b | ||
|
|
bed40324a1 | ||
|
|
bc035de377 | ||
|
|
4db3cb6936 | ||
|
|
ed4b91f4bd | ||
|
|
24c7151276 | ||
|
|
804a9c07b8 | ||
|
|
528ea56821 | ||
|
|
7ba9c69ff8 | ||
|
|
0fc34da08a | ||
|
|
3fbf8cdbc8 | ||
|
|
e21f20d818 | ||
|
|
9fd9a60ca3 | ||
|
|
78b683d724 | ||
|
|
ad2f5036e1 | ||
|
|
4afbad8faa | ||
|
|
d284db4d3c | ||
|
|
82450c0ae8 | ||
|
|
8dd6c33901 | ||
|
|
f920d40db5 | ||
|
|
19be6c1acc | ||
|
|
7d9d8ad0e4 | ||
|
|
85f8237d5f | ||
|
|
c542894734 | ||
|
|
d348987077 | ||
|
|
3718610595 | ||
|
|
9c36ec0623 | ||
|
|
c6917b5d74 | ||
|
|
ae8b1c0c29 | ||
|
|
27978c459c | ||
|
|
1dc7f5c666 | ||
|
|
12ac870d3a | ||
|
|
dd1baa0224 | ||
|
|
4eaa179789 | ||
|
|
9008cd4549 | ||
|
|
bb27ef41cc | ||
|
|
2d35ac1df8 | ||
|
|
589ffc0c06 | ||
|
|
1f7f38c7d3 | ||
|
|
83817a2dc0 | ||
|
|
11af9da66f | ||
|
|
af3926acf3 | ||
|
|
ab40c2b3fd | ||
|
|
fd05670dbc | ||
|
|
1ac094bfae | ||
|
|
fdf052cddb | ||
|
|
9a8d50ba6f | ||
|
|
136c97c312 | ||
|
|
bf00b88ef3 | ||
|
|
bafd1ea549 | ||
|
|
982618511b | ||
|
|
a4ad7ca3b1 | ||
|
|
99d71b57a4 | ||
|
|
1b2d8502e0 | ||
|
|
53e4ea9334 | ||
|
|
3ce704155c |
13
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
#github: [J-Jamet] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
|
#patreon: # Replace with a single Patreon username
|
||||||
|
#open_collective: # Replace with a single Open Collective username
|
||||||
|
#ko_fi: # Replace with a single Ko-fi username
|
||||||
|
#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: Kunzisoft # Replace with a single Liberapay username
|
||||||
|
issuehunt: Kunzisoft/KeePassDX # Replace with a single IssueHunt username
|
||||||
|
#otechie: # Replace with a single Otechie username
|
||||||
|
#lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
custom: ['https://www.keepassdx.com/#donation'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
12
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -8,9 +8,11 @@ assignees: ''
|
|||||||
---
|
---
|
||||||
|
|
||||||
**Describe the bug**
|
**Describe the bug**
|
||||||
|
|
||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
|
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
@@ -18,24 +20,30 @@ Steps to reproduce the behavior:
|
|||||||
4. See error
|
4. See error
|
||||||
|
|
||||||
**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.
|
||||||
|
|
||||||
**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]
|
||||||
- Location: [e.g. Remote file retrieved with GDrive app]
|
- Location: [e.g. Remote file retrieved with GDrive app]
|
||||||
|
- File provider (`content://` URI): [e.g. `content://com.google.android.apps.docs.storage/5`]
|
||||||
- Size: [e.g. 150Mo]
|
- Size: [e.g. 150Mo]
|
||||||
- Contains attachment: [e.g. Yes]
|
- Contains attachment: [e.g. Yes]
|
||||||
|
|
||||||
**KeePassDX (please complete the following information):**
|
**KeePassDX:**
|
||||||
|
|
||||||
- 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]
|
||||||
|
|
||||||
**Android (please complete the following information):**
|
**Android:**
|
||||||
|
|
||||||
- Device: [e.g. GalaxyS8]
|
- Device: [e.g. GalaxyS8]
|
||||||
- Version: [e.g. 8.1]
|
- Version: [e.g. 8.1]
|
||||||
|
|
||||||
**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]
|
- Browser for Autofill: [e.g. Chrome version X]
|
||||||
|
|||||||
3
.gitignore
vendored
@@ -80,6 +80,9 @@ art/screen*.png
|
|||||||
art/logo_512.png
|
art/logo_512.png
|
||||||
art/store_screens/
|
art/store_screens/
|
||||||
|
|
||||||
|
# Release
|
||||||
|
releases/*
|
||||||
|
|
||||||
# Dir linux
|
# Dir linux
|
||||||
.directory
|
.directory
|
||||||
*/.directory
|
*/.directory
|
||||||
|
|||||||
109
CHANGELOG
@@ -1,3 +1,110 @@
|
|||||||
|
KeePassDX(3.4.4)
|
||||||
|
* Fix crash in New Android 13 #1321
|
||||||
|
* Better backstack management for selection mode
|
||||||
|
* Prevent Tapjacking #1318
|
||||||
|
* Small changes #1298
|
||||||
|
|
||||||
|
KeePassDX(3.4.3)
|
||||||
|
* Remove "Select share info" setting for Magikeyboard #1304
|
||||||
|
* Fix quick search and better loadGroup implementation #1302
|
||||||
|
* Fix small bugs
|
||||||
|
|
||||||
|
KeePassDX(3.4.2)
|
||||||
|
* Fix service parameter and workflow to remove notification when service is killed
|
||||||
|
* Fix color
|
||||||
|
|
||||||
|
KeePassDX(3.4.1)
|
||||||
|
* Fix search mode with Magikeyboard #1292
|
||||||
|
* Fix select another entry with Magikeyboard #1293
|
||||||
|
* Fix unexpected lock with Magikeyboard #1294
|
||||||
|
* Small UI changes
|
||||||
|
|
||||||
|
KeePassDX(3.4.0)
|
||||||
|
* Passphrase implementation #218
|
||||||
|
* Show visual password strength indicator with entropy #631 #869 #454 #1270
|
||||||
|
* Dynamically save password generator configuration #618 #696
|
||||||
|
* Add advanced password filters #1052 #448 #983 #271 #539
|
||||||
|
* Better search implementation #175 #1254 #1267
|
||||||
|
* Manage package name from Magikeyboard #1010 #1261
|
||||||
|
* Ask confirmation to lock if changes without save #970
|
||||||
|
* Fix small bugs #1282
|
||||||
|
|
||||||
|
KeePassDX(3.3.3)
|
||||||
|
* Fix shared otpauth link if database not open #1274
|
||||||
|
* Ellipsize attachment name #1253
|
||||||
|
* Add a warning to inform about KeyStore usage #1269
|
||||||
|
* Fingerprint unlock no more by default #1273
|
||||||
|
* Tabs to show main and advanced content separately
|
||||||
|
* Fix URL color
|
||||||
|
|
||||||
|
KeePassDX(3.3.2)
|
||||||
|
* Merge KeePassDX & KeePassDX Pro #1257
|
||||||
|
* Create new Contributor Pro app
|
||||||
|
|
||||||
|
KeePassDX(3.3.1)
|
||||||
|
* Fix Japanese keyboard in search #1248
|
||||||
|
* Better OOM management #256
|
||||||
|
* Fix filters #1249
|
||||||
|
* Fix temp advanced unlocking #1245
|
||||||
|
* Best autofill recognition #1250
|
||||||
|
* Workaround to fill OTP token in multiple fields with Magikeyboard (long press) #1158
|
||||||
|
|
||||||
|
KeePassDX(3.3.0)
|
||||||
|
* Quick search and dynamic filters #163 #462 #521
|
||||||
|
* Keep search context #1141
|
||||||
|
* Add searchable groups #905 #1006
|
||||||
|
* Search with regular expression #175
|
||||||
|
* Merge from file and save as copy #1221 #1204 #840
|
||||||
|
* Fix custom data #1236
|
||||||
|
* Fix education hints #1192
|
||||||
|
* Fix save and app instance in selection mode
|
||||||
|
* New UI and fix styles
|
||||||
|
* Add "Simple" and "Reply" themes
|
||||||
|
|
||||||
|
KeePassDX(3.2.0)
|
||||||
|
* Manage data merge #840 #977
|
||||||
|
* Manage Tags #633
|
||||||
|
* Inherit colors and icon from template #1213 #1130
|
||||||
|
* Entry colors setting #1207
|
||||||
|
* Setting to keep the screen on when watching the entry #1119
|
||||||
|
* Add path in quick search
|
||||||
|
* Small fixes
|
||||||
|
|
||||||
|
KeePassDX(3.1.0)
|
||||||
|
* Add breadcrumb
|
||||||
|
* Add path in search results #1148
|
||||||
|
* Add group info dialog #1177
|
||||||
|
* Manage colors #64 #913
|
||||||
|
* Fix UI in Android 8 #509
|
||||||
|
* Upgrade libs and SDK to 31 #833
|
||||||
|
* Fix parser of database v1 #1201
|
||||||
|
* Stop asking WRITE_EXTERNAL_STORAGE permission
|
||||||
|
|
||||||
|
KeePassDX(3.0.4)
|
||||||
|
* Fix autofill inline bugs #1173 #1165
|
||||||
|
* Small UI change
|
||||||
|
|
||||||
|
KeePassDX(3.0.3)
|
||||||
|
* Change default Argon2 parameters #1098
|
||||||
|
* Add & edit custom icon name #976
|
||||||
|
* Fix templates #1128 #1133 #1138
|
||||||
|
* Update Autofill compatibility list #725 #1154
|
||||||
|
* Improve fingerprint usage #1137 #1145
|
||||||
|
* Change backup configuration #1144
|
||||||
|
* Add lock button in database notification
|
||||||
|
|
||||||
|
KeePassDX(3.0.2)
|
||||||
|
* Samsung DeX mode #1114 #245 (Thx @chenxiaolong)
|
||||||
|
|
||||||
|
KeePassDX(3.0.1)
|
||||||
|
* Fix text size and smallest margin #1085
|
||||||
|
* Fix number of lines during an edition #1073
|
||||||
|
* Fix Magikeyboard URL auto action #1100
|
||||||
|
* Fix exception after group name change and save #1112
|
||||||
|
* Fix timeout reset #1107
|
||||||
|
* Fix search actions #1091 #1092
|
||||||
|
* Small changes #1106 #1085
|
||||||
|
|
||||||
KeePassDX(3.0.0)
|
KeePassDX(3.0.0)
|
||||||
* Add / Manage dynamic templates #191
|
* Add / Manage dynamic templates #191
|
||||||
* Manually select RecycleBin group and Templates group #191
|
* Manually select RecycleBin group and Templates group #191
|
||||||
@@ -5,7 +112,7 @@ KeePassDX(3.0.0)
|
|||||||
* Fix timeout in dialogs #716
|
* Fix timeout in dialogs #716
|
||||||
* Check URI permissions #626
|
* Check URI permissions #626
|
||||||
* Better autofill implementation #943 #946 #984 #1070 (Thx @uduerholz)
|
* Better autofill implementation #943 #946 #984 #1070 (Thx @uduerholz)
|
||||||
* Improvements #680 #1035 #1043 #942 #1021 #1027
|
* Improvements #680 #1035 #1043 #942 #1021 #1027 #1046 #1082 #1083 (Thx @chenxiaolong)
|
||||||
|
|
||||||
KeePassDX(2.10.5)
|
KeePassDX(2.10.5)
|
||||||
* Increase the saving speed of database #1028
|
* Increase the saving speed of database #1028
|
||||||
|
|||||||
10
Gemfile
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Autogenerated by fastlane
|
||||||
|
#
|
||||||
|
# Ensure this file is checked in to source control!
|
||||||
|
|
||||||
|
source "https://rubygems.org"
|
||||||
|
|
||||||
|
gem 'fastlane'
|
||||||
|
|
||||||
|
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
|
||||||
|
eval_gemfile(plugins_path) if File.exist?(plugins_path)
|
||||||
220
Gemfile.lock
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
GEM
|
||||||
|
remote: https://rubygems.org/
|
||||||
|
specs:
|
||||||
|
CFPropertyList (3.0.5)
|
||||||
|
rexml
|
||||||
|
addressable (2.8.0)
|
||||||
|
public_suffix (>= 2.0.2, < 5.0)
|
||||||
|
artifactory (3.0.15)
|
||||||
|
atomos (0.1.3)
|
||||||
|
aws-eventstream (1.2.0)
|
||||||
|
aws-partitions (1.577.0)
|
||||||
|
aws-sdk-core (3.130.1)
|
||||||
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
|
aws-partitions (~> 1, >= 1.525.0)
|
||||||
|
aws-sigv4 (~> 1.1)
|
||||||
|
jmespath (~> 1.0)
|
||||||
|
aws-sdk-kms (1.55.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.127.0)
|
||||||
|
aws-sigv4 (~> 1.1)
|
||||||
|
aws-sdk-s3 (1.113.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.127.0)
|
||||||
|
aws-sdk-kms (~> 1)
|
||||||
|
aws-sigv4 (~> 1.4)
|
||||||
|
aws-sigv4 (1.4.0)
|
||||||
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
|
babosa (1.0.4)
|
||||||
|
claide (1.1.0)
|
||||||
|
colored (1.2)
|
||||||
|
colored2 (3.1.2)
|
||||||
|
commander (4.6.0)
|
||||||
|
highline (~> 2.0.0)
|
||||||
|
declarative (0.0.20)
|
||||||
|
digest-crc (0.6.4)
|
||||||
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
|
domain_name (0.5.20190701)
|
||||||
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
|
dotenv (2.7.6)
|
||||||
|
emoji_regex (3.2.3)
|
||||||
|
excon (0.92.2)
|
||||||
|
faraday (1.10.0)
|
||||||
|
faraday-em_http (~> 1.0)
|
||||||
|
faraday-em_synchrony (~> 1.0)
|
||||||
|
faraday-excon (~> 1.1)
|
||||||
|
faraday-httpclient (~> 1.0)
|
||||||
|
faraday-multipart (~> 1.0)
|
||||||
|
faraday-net_http (~> 1.0)
|
||||||
|
faraday-net_http_persistent (~> 1.0)
|
||||||
|
faraday-patron (~> 1.0)
|
||||||
|
faraday-rack (~> 1.0)
|
||||||
|
faraday-retry (~> 1.0)
|
||||||
|
ruby2_keywords (>= 0.0.4)
|
||||||
|
faraday-cookie_jar (0.0.7)
|
||||||
|
faraday (>= 0.8.0)
|
||||||
|
http-cookie (~> 1.0.0)
|
||||||
|
faraday-em_http (1.0.0)
|
||||||
|
faraday-em_synchrony (1.0.0)
|
||||||
|
faraday-excon (1.1.0)
|
||||||
|
faraday-httpclient (1.0.1)
|
||||||
|
faraday-multipart (1.0.3)
|
||||||
|
multipart-post (>= 1.2, < 3)
|
||||||
|
faraday-net_http (1.0.1)
|
||||||
|
faraday-net_http_persistent (1.2.0)
|
||||||
|
faraday-patron (1.0.0)
|
||||||
|
faraday-rack (1.0.0)
|
||||||
|
faraday-retry (1.0.3)
|
||||||
|
faraday_middleware (1.2.0)
|
||||||
|
faraday (~> 1.0)
|
||||||
|
fastimage (2.2.6)
|
||||||
|
fastlane (2.205.1)
|
||||||
|
CFPropertyList (>= 2.3, < 4.0.0)
|
||||||
|
addressable (>= 2.8, < 3.0.0)
|
||||||
|
artifactory (~> 3.0)
|
||||||
|
aws-sdk-s3 (~> 1.0)
|
||||||
|
babosa (>= 1.0.3, < 2.0.0)
|
||||||
|
bundler (>= 1.12.0, < 3.0.0)
|
||||||
|
colored
|
||||||
|
commander (~> 4.6)
|
||||||
|
dotenv (>= 2.1.1, < 3.0.0)
|
||||||
|
emoji_regex (>= 0.1, < 4.0)
|
||||||
|
excon (>= 0.71.0, < 1.0.0)
|
||||||
|
faraday (~> 1.0)
|
||||||
|
faraday-cookie_jar (~> 0.0.6)
|
||||||
|
faraday_middleware (~> 1.0)
|
||||||
|
fastimage (>= 2.1.0, < 3.0.0)
|
||||||
|
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||||
|
google-apis-androidpublisher_v3 (~> 0.3)
|
||||||
|
google-apis-playcustomapp_v1 (~> 0.1)
|
||||||
|
google-cloud-storage (~> 1.31)
|
||||||
|
highline (~> 2.0)
|
||||||
|
json (< 3.0.0)
|
||||||
|
jwt (>= 2.1.0, < 3)
|
||||||
|
mini_magick (>= 4.9.4, < 5.0.0)
|
||||||
|
multipart-post (~> 2.0.0)
|
||||||
|
naturally (~> 2.2)
|
||||||
|
optparse (~> 0.1.1)
|
||||||
|
plist (>= 3.1.0, < 4.0.0)
|
||||||
|
rubyzip (>= 2.0.0, < 3.0.0)
|
||||||
|
security (= 0.1.3)
|
||||||
|
simctl (~> 1.6.3)
|
||||||
|
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||||
|
terminal-table (>= 1.4.5, < 2.0.0)
|
||||||
|
tty-screen (>= 0.6.3, < 1.0.0)
|
||||||
|
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||||
|
word_wrap (~> 1.0.0)
|
||||||
|
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||||
|
xcpretty (~> 0.3.0)
|
||||||
|
xcpretty-travis-formatter (>= 0.0.3)
|
||||||
|
fastlane-plugin-versioning_android (0.1.0)
|
||||||
|
gh_inspector (1.1.3)
|
||||||
|
google-apis-androidpublisher_v3 (0.19.0)
|
||||||
|
google-apis-core (>= 0.4, < 2.a)
|
||||||
|
google-apis-core (0.4.2)
|
||||||
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
|
httpclient (>= 2.8.1, < 3.a)
|
||||||
|
mini_mime (~> 1.0)
|
||||||
|
representable (~> 3.0)
|
||||||
|
retriable (>= 2.0, < 4.a)
|
||||||
|
rexml
|
||||||
|
webrick
|
||||||
|
google-apis-iamcredentials_v1 (0.10.0)
|
||||||
|
google-apis-core (>= 0.4, < 2.a)
|
||||||
|
google-apis-playcustomapp_v1 (0.7.0)
|
||||||
|
google-apis-core (>= 0.4, < 2.a)
|
||||||
|
google-apis-storage_v1 (0.13.0)
|
||||||
|
google-apis-core (>= 0.4, < 2.a)
|
||||||
|
google-cloud-core (1.6.0)
|
||||||
|
google-cloud-env (~> 1.0)
|
||||||
|
google-cloud-errors (~> 1.0)
|
||||||
|
google-cloud-env (1.6.0)
|
||||||
|
faraday (>= 0.17.3, < 3.0)
|
||||||
|
google-cloud-errors (1.2.0)
|
||||||
|
google-cloud-storage (1.36.1)
|
||||||
|
addressable (~> 2.8)
|
||||||
|
digest-crc (~> 0.4)
|
||||||
|
google-apis-iamcredentials_v1 (~> 0.1)
|
||||||
|
google-apis-storage_v1 (~> 0.1)
|
||||||
|
google-cloud-core (~> 1.6)
|
||||||
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
|
mini_mime (~> 1.0)
|
||||||
|
googleauth (1.1.2)
|
||||||
|
faraday (>= 0.17.3, < 3.a)
|
||||||
|
jwt (>= 1.4, < 3.0)
|
||||||
|
memoist (~> 0.16)
|
||||||
|
multi_json (~> 1.11)
|
||||||
|
os (>= 0.9, < 2.0)
|
||||||
|
signet (>= 0.16, < 2.a)
|
||||||
|
highline (2.0.3)
|
||||||
|
http-cookie (1.0.4)
|
||||||
|
domain_name (~> 0.5)
|
||||||
|
httpclient (2.8.3)
|
||||||
|
jmespath (1.6.1)
|
||||||
|
json (2.6.1)
|
||||||
|
jwt (2.3.0)
|
||||||
|
memoist (0.16.2)
|
||||||
|
mini_magick (4.11.0)
|
||||||
|
mini_mime (1.1.2)
|
||||||
|
multi_json (1.15.0)
|
||||||
|
multipart-post (2.0.0)
|
||||||
|
nanaimo (0.3.0)
|
||||||
|
naturally (2.2.1)
|
||||||
|
optparse (0.1.1)
|
||||||
|
os (1.1.4)
|
||||||
|
plist (3.6.0)
|
||||||
|
public_suffix (4.0.7)
|
||||||
|
rake (13.0.6)
|
||||||
|
representable (3.1.1)
|
||||||
|
declarative (< 0.1.0)
|
||||||
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
|
uber (< 0.2.0)
|
||||||
|
retriable (3.1.2)
|
||||||
|
rexml (3.2.5)
|
||||||
|
rouge (2.0.7)
|
||||||
|
ruby2_keywords (0.0.5)
|
||||||
|
rubyzip (2.3.2)
|
||||||
|
security (0.1.3)
|
||||||
|
signet (0.16.1)
|
||||||
|
addressable (~> 2.8)
|
||||||
|
faraday (>= 0.17.5, < 3.0)
|
||||||
|
jwt (>= 1.5, < 3.0)
|
||||||
|
multi_json (~> 1.10)
|
||||||
|
simctl (1.6.8)
|
||||||
|
CFPropertyList
|
||||||
|
naturally
|
||||||
|
terminal-notifier (2.0.0)
|
||||||
|
terminal-table (1.8.0)
|
||||||
|
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||||
|
trailblazer-option (0.1.2)
|
||||||
|
tty-cursor (0.7.1)
|
||||||
|
tty-screen (0.8.1)
|
||||||
|
tty-spinner (0.9.3)
|
||||||
|
tty-cursor (~> 0.7)
|
||||||
|
uber (0.1.0)
|
||||||
|
unf (0.1.4)
|
||||||
|
unf_ext
|
||||||
|
unf_ext (0.0.8.1)
|
||||||
|
unicode-display_width (1.8.0)
|
||||||
|
webrick (1.7.0)
|
||||||
|
word_wrap (1.0.0)
|
||||||
|
xcodeproj (1.21.0)
|
||||||
|
CFPropertyList (>= 2.3.3, < 4.0)
|
||||||
|
atomos (~> 0.1.3)
|
||||||
|
claide (>= 1.0.2, < 2.0)
|
||||||
|
colored2 (~> 3.1)
|
||||||
|
nanaimo (~> 0.3.0)
|
||||||
|
rexml (~> 3.2.4)
|
||||||
|
xcpretty (0.3.0)
|
||||||
|
rouge (~> 2.0.7)
|
||||||
|
xcpretty-travis-formatter (1.0.1)
|
||||||
|
xcpretty (~> 0.2, >= 0.0.7)
|
||||||
|
|
||||||
|
PLATFORMS
|
||||||
|
ruby
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
fastlane
|
||||||
|
fastlane-plugin-versioning_android
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
2.1.4
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Android KeePassDX
|
# 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/icon.png"> **Lightweight password manager for Android**, KeePassDX allows editing encrypted data in a single file in KeePass format and fill in the forms in a secure way.
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
|
<img src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
- Material design with **themes**.
|
- Material design with **themes**.
|
||||||
- **Auto-Fill** and Integration.
|
- **Auto-Fill** and Integration.
|
||||||
- Field filling **keyboard**.
|
- Field filling **keyboard**.
|
||||||
|
- Dynamic **templates**
|
||||||
- **History** of each entry.
|
- **History** of each entry.
|
||||||
- Precise management of **settings**.
|
- Precise management of **settings**.
|
||||||
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
|
- Code written in **native languages** *(Kotlin / Java / JNI / C)*.
|
||||||
@@ -42,7 +43,7 @@ Optional visual styles are accessible after a contribution (and a congratulatory
|
|||||||
|
|
||||||
* Add features by making a **[pull request](https://help.github.com/articles/about-pull-requests/)**.
|
* 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/)).
|
* 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.
|
* **[Donate](https://www.keepassdx.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.
|
* Buy the **[Pro version](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro)** of KeePassDX.
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
@@ -71,7 +72,7 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright © 2020 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
Copyright © 2022 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||||
|
|
||||||
This file is part of KeePassDX.
|
This file is part of KeePassDX.
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlin-parcelize'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 30
|
compileSdkVersion 31
|
||||||
buildToolsVersion "30.0.3"
|
buildToolsVersion "31.0.0"
|
||||||
ndkVersion "21.4.7075529"
|
ndkVersion "21.4.7075529"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 15
|
minSdkVersion 15
|
||||||
targetSdkVersion 30
|
targetSdkVersion 31
|
||||||
versionCode = 86
|
versionCode = 113
|
||||||
versionName = "3.0.0_beta03"
|
versionName = "3.4.4"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
@@ -42,36 +43,30 @@ android {
|
|||||||
dimension "version"
|
dimension "version"
|
||||||
applicationIdSuffix = ".libre"
|
applicationIdSuffix = ".libre"
|
||||||
buildConfigField "String", "BUILD_VERSION", "\"libre\""
|
buildConfigField "String", "BUILD_VERSION", "\"libre\""
|
||||||
buildConfigField "boolean", "FULL_VERSION", "true"
|
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "false"
|
buildConfigField "boolean", "CLOSED_STORE", "false"
|
||||||
buildConfigField "String[]", "STYLES_DISABLED",
|
buildConfigField "String[]", "STYLES_DISABLED",
|
||||||
"{\"KeepassDXStyle_Red\"," +
|
"{\"KeepassDXStyle_Red\"," +
|
||||||
"\"KeepassDXStyle_Red_Night\"," +
|
"\"KeepassDXStyle_Red_Night\"," +
|
||||||
|
"\"KeepassDXStyle_Reply\"," +
|
||||||
|
"\"KeepassDXStyle_Reply_Night\"," +
|
||||||
"\"KeepassDXStyle_Purple\"," +
|
"\"KeepassDXStyle_Purple\"," +
|
||||||
"\"KeepassDXStyle_Purple_Dark\"}"
|
"\"KeepassDXStyle_Purple_Dark\"}"
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||||
}
|
}
|
||||||
pro {
|
|
||||||
dimension "version"
|
|
||||||
applicationIdSuffix = ".pro"
|
|
||||||
buildConfigField "String", "BUILD_VERSION", "\"pro\""
|
|
||||||
buildConfigField "boolean", "FULL_VERSION", "true"
|
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "true"
|
|
||||||
buildConfigField "String[]", "STYLES_DISABLED", "{}"
|
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
|
||||||
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIZiXvrQCzSV9LNI6-p7cjTKENZLHIrz_zaqZuQQ" ]
|
|
||||||
}
|
|
||||||
free {
|
free {
|
||||||
dimension "version"
|
dimension "version"
|
||||||
applicationIdSuffix = ".free"
|
applicationIdSuffix = ".free"
|
||||||
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
||||||
buildConfigField "boolean", "FULL_VERSION", "false"
|
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "true"
|
buildConfigField "boolean", "CLOSED_STORE", "true"
|
||||||
buildConfigField "String[]", "STYLES_DISABLED",
|
buildConfigField "String[]", "STYLES_DISABLED",
|
||||||
"{\"KeepassDXStyle_Blue\"," +
|
"{\"KeepassDXStyle_Simple\"," +
|
||||||
|
"\"KeepassDXStyle_Simple_Night\"," +
|
||||||
|
"\"KeepassDXStyle_Blue\"," +
|
||||||
"\"KeepassDXStyle_Blue_Night\"," +
|
"\"KeepassDXStyle_Blue_Night\"," +
|
||||||
"\"KeepassDXStyle_Red\"," +
|
"\"KeepassDXStyle_Red\"," +
|
||||||
"\"KeepassDXStyle_Red_Night\"," +
|
"\"KeepassDXStyle_Red_Night\"," +
|
||||||
|
"\"KeepassDXStyle_Reply\"," +
|
||||||
|
"\"KeepassDXStyle_Reply_Night\"," +
|
||||||
"\"KeepassDXStyle_Purple\"," +
|
"\"KeepassDXStyle_Purple\"," +
|
||||||
"\"KeepassDXStyle_Purple_Dark\"}"
|
"\"KeepassDXStyle_Purple_Dark\"}"
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||||
@@ -81,7 +76,6 @@ android {
|
|||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
libre.res.srcDir 'src/libre/res'
|
libre.res.srcDir 'src/libre/res'
|
||||||
pro.res.srcDir 'src/pro/res'
|
|
||||||
free.res.srcDir 'src/free/res'
|
free.res.srcDir 'src/free/res'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,36 +93,42 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def room_version = "2.2.6"
|
def room_version = "2.4.2"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
implementation "com.android.support:multidex:1.0.3"
|
||||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
implementation "androidx.appcompat:appcompat:$android_appcompat_version"
|
||||||
|
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'androidx.biometric:biometric:1.1.0'
|
implementation 'androidx.biometric:biometric:1.1.0'
|
||||||
|
implementation 'androidx.media:media:1.6.0'
|
||||||
// Lifecycle - LiveData - ViewModel - Coroutines
|
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||||
implementation "androidx.core:core-ktx:1.3.2"
|
implementation "androidx.core:core-ktx:$android_core_version"
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
implementation 'androidx.fragment:fragment-ktx:1.4.1'
|
||||||
// WARNING: Don't upgrade because slowdown https://github.com/Kunzisoft/KeePassDX/issues/923
|
implementation "com.google.android.material:material:$android_material_version"
|
||||||
implementation 'com.google.android.material:material:1.1.0'
|
// Token auto complete
|
||||||
|
// From sources until https://github.com/splitwise/TokenAutoComplete/pull/422 fixed
|
||||||
|
// implementation "com.splitwise:tokenautocomplete:4.0.0-beta04"
|
||||||
// Database
|
// Database
|
||||||
implementation "androidx.room:room-runtime:$room_version"
|
implementation "androidx.room:room-runtime:$room_version"
|
||||||
kapt "androidx.room:room-compiler:$room_version"
|
kapt "androidx.room:room-compiler:$room_version"
|
||||||
// Autofill
|
// Autofill
|
||||||
implementation "androidx.autofill:autofill:1.1.0"
|
implementation "androidx.autofill:autofill:1.1.0"
|
||||||
// Time
|
// Time
|
||||||
implementation 'joda-time:joda-time:2.10.6'
|
implementation 'joda-time:joda-time:2.10.13'
|
||||||
// Color
|
// Color
|
||||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.4'
|
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.6'
|
||||||
// Education
|
// Education
|
||||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.0'
|
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3'
|
||||||
// Apache Commons
|
// Apache Commons
|
||||||
implementation 'commons-io:commons-io:2.8.0'
|
implementation 'commons-io:commons-io:2.8.0'
|
||||||
implementation 'commons-codec:commons-codec:1.15'
|
implementation 'commons-codec:commons-codec:1.15'
|
||||||
|
// Password generator
|
||||||
|
implementation 'me.gosimple:nbvcxz:1.5.0'
|
||||||
// Encrypt lib
|
// Encrypt lib
|
||||||
implementation project(path: ':crypto')
|
implementation project(path: ':crypto')
|
||||||
// Icon pack
|
// Icon pack
|
||||||
@@ -136,6 +136,6 @@ dependencies {
|
|||||||
implementation project(path: ':icon-pack-material')
|
implementation project(path: ':icon-pack-material')
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
androidTestImplementation "androidx.test:runner:$android_test_version"
|
||||||
androidTestImplementation 'androidx.test:rules:1.3.0'
|
androidTestImplementation "androidx.test:rules:$android_test_version"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +1,31 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108"
|
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp">
|
android:height="108dp"
|
||||||
|
android:viewportWidth="120"
|
||||||
|
android:viewportHeight="120">
|
||||||
<group
|
<group
|
||||||
android:translateY="-332">
|
android:translateX="6"
|
||||||
<group
|
android:translateY="8">
|
||||||
android:translateY="332">
|
<path
|
||||||
<path
|
android:fillColor="#24000000"
|
||||||
android:pathData="M65.728516 32.791016L58.052734 35.904297 56.173828 48.380859 35.306641 69.267578 35.238281 73.759766 69.478516 108 108 108 108 70.810547 73.09375 35.904297 65.728516 32.791016Z"
|
android:strokeWidth="1.99999297"
|
||||||
android:strokeLineJoin="round"
|
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||||
android:strokeLineCap="round"
|
<path
|
||||||
android:strokeMiterLimit="4" >
|
android:fillColor="#24000000"
|
||||||
<aapt:attr name="android:fillColor">
|
android:strokeWidth="1.99999297"
|
||||||
<gradient
|
android:pathData="M63.9961,34.0059 C61.5643,34.096,59.2564,35.102,57.5352,36.8223 C53.7682,40.589,53.7682,46.6982,57.5352,50.4649 C61.3017,54.232,67.4073,54.232,71.1739,50.4649 C74.9409,46.6982,74.9409,40.589,71.1739,36.8223 C69.2766,34.9258,66.6768,33.9054,63.9962,34.0059 Z M68.1992,40.6954 C69.8278,40.6958,71.148,42.016,71.1484,43.6446 C71.148,45.2732,69.8278,46.5934,68.1992,46.5938 C66.5706,46.5934,65.2504,45.2732,65.25,43.6446 C65.2504,42.016,66.5706,40.6958,68.1992,40.6954 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||||
android:endColor="#0000"
|
</group>
|
||||||
android:endX="80"
|
<group
|
||||||
android:endY="80"
|
android:translateX="6"
|
||||||
android:startColor="#4e000000"
|
android:translateY="6">
|
||||||
android:startX="0"
|
<path
|
||||||
android:startY="0"
|
android:fillColor="#ffa726"
|
||||||
android:type="linear"/>
|
android:strokeWidth="1.99999297"
|
||||||
</aapt:attr>
|
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||||
</path>
|
<path
|
||||||
</group>
|
android:fillColor="#ffffff"
|
||||||
<group
|
android:strokeWidth="1.99999297"
|
||||||
android:scaleX="0.3939503"
|
android:pathData="M63.9961,34.0059 C61.5643,34.096,59.2564,35.102,57.5352,36.8223 C53.7682,40.589,53.7682,46.6982,57.5352,50.4649 C61.3017,54.232,67.4073,54.232,71.1739,50.4649 C74.9409,46.6982,74.9409,40.589,71.1739,36.8223 C69.2766,34.9258,66.6768,33.9054,63.9962,34.0059 Z M68.1992,40.6954 C69.8278,40.6958,71.148,42.016,71.1484,43.6446 C71.148,45.2732,69.8278,46.5934,68.1992,46.5938 C66.5706,46.5934,65.2504,45.2732,65.25,43.6446 C65.2504,42.016,66.5706,40.6958,68.1992,40.6954 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||||
android:scaleY="0.3939503"
|
|
||||||
android:translateX="33.66343"
|
|
||||||
android:translateY="233.998">
|
|
||||||
<path
|
|
||||||
android:pathData="M88.76953 339.91602L4.1718754 424.59766 4.0000004 436 15.400391 435.82813 27.240234 424 40 424l0 -12 12 0 0 -12.73438 34.01172 -33.97656A8 8 0 0 1 84 360a8 8 0 0 1 8 -8 8 8 0 0 1 5.296882 2.01367l2.787098 -2.7832 -11.31445 -11.31445z"
|
|
||||||
android:fillColor="#eaeaea"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="#58000000" />
|
|
||||||
</group>
|
|
||||||
<group
|
|
||||||
android:scaleX="0.3939503"
|
|
||||||
android:scaleY="0.3939503"
|
|
||||||
android:translateX="33.66343"
|
|
||||||
android:translateY="233.998">
|
|
||||||
<path
|
|
||||||
android:pathData="M4.0000004 340L4.1718754 351.40137 88.59863 435.82812 100 436 99.828122 424.59863 15.401367 340.17188Z"
|
|
||||||
android:fillColor="#81c784" />
|
|
||||||
</group>
|
|
||||||
<group
|
|
||||||
android:scaleX="0.3939503"
|
|
||||||
android:scaleY="0.3939503"
|
|
||||||
android:translateX="33.66343"
|
|
||||||
android:translateY="233.998">
|
|
||||||
<path
|
|
||||||
android:pathData="M81.39454 332.00195a27 27 0 0 0 -19.48634 7.90625 27 27 0 0 0 0 38.1836 27 27 0 0 0 38.1836 0 27 27 0 0 0 0 -38.1836 27 27 0 0 0 -18.69726 -7.90625zM92 352a8 8 0 0 1 8 8 8 8 0 0 1 -8 8 8 8 0 0 1 -8 -8 8 8 0 0 1 8 -8z"
|
|
||||||
android:fillColor="#eaeaea"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="#58000000" />
|
|
||||||
</group>
|
|
||||||
</group>
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
19
app/src/free/res/drawable/ic_app_white_24dp.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="84"
|
||||||
|
android:viewportHeight="84">
|
||||||
|
<group
|
||||||
|
android:translateX="-12"
|
||||||
|
android:translateY="-12">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffa726"
|
||||||
|
android:strokeWidth="1.99999297"
|
||||||
|
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeWidth="1.99999297"
|
||||||
|
android:pathData="M63.9961,34.0059 C61.5643,34.096,59.2564,35.102,57.5352,36.8223 C53.7682,40.589,53.7682,46.6982,57.5352,50.4649 C61.3017,54.232,67.4073,54.232,71.1739,50.4649 C74.9409,46.6982,74.9409,40.589,71.1739,36.8223 C69.2766,34.9258,66.6768,33.9054,63.9962,34.0059 Z M68.1992,40.6954 C69.8278,40.6958,71.148,42.016,71.1484,43.6446 C71.148,45.2732,69.8278,46.5934,68.1992,46.5938 C66.5706,46.5934,65.2504,45.2732,65.25,43.6446 C65.2504,42.016,66.5706,40.6958,68.1992,40.6954 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 13 KiB |
@@ -1,61 +1,31 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108"
|
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp">
|
android:height="108dp"
|
||||||
|
android:viewportWidth="120"
|
||||||
|
android:viewportHeight="120">
|
||||||
<group
|
<group
|
||||||
android:translateY="-332">
|
android:translateX="6"
|
||||||
<group
|
android:translateY="8">
|
||||||
android:translateY="332">
|
<path
|
||||||
<path
|
android:fillColor="#24000000"
|
||||||
android:pathData="M65.728516 32.791016L58.052734 35.904297 56.173828 48.380859 35.306641 69.267578 35.238281 73.759766 69.478516 108 108 108 108 70.810547 73.09375 35.904297 65.728516 32.791016Z"
|
android:strokeWidth="1.99999297"
|
||||||
android:strokeLineJoin="round"
|
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||||
android:strokeLineCap="round"
|
<path
|
||||||
android:strokeMiterLimit="4" >
|
android:fillColor="#24000000"
|
||||||
<aapt:attr name="android:fillColor">
|
android:strokeWidth="1.99999297"
|
||||||
<gradient
|
android:pathData="M64.501,35.0576 C63.7095,35.0576,62.918,35.3613,62.3115,35.9678 L55.0127,43.2666 C53.7998,44.4795,53.7998,46.4306,55.0127,47.6436 L62.3115,54.9424 C63.5244,56.1553,65.4775,56.1553,66.6904,54.9424 L73.9873,47.6436 C75.2002,46.4307,75.2002,44.4796,73.9873,43.2666 L66.6904,35.9678 C66.0839,35.3613,65.2924,35.0576,64.5009,35.0576 Z M67.6729,42.6006 C69.3298,42.6006,70.6729,43.9437,70.6729,45.6006 C70.6729,47.2575,69.3298,48.6006,67.6729,48.6006 C66.016,48.6006,64.6729,47.2575,64.6729,45.6006 C64.6729,43.9437,66.016,42.6006,67.6729,42.6006 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||||
android:endColor="#0000"
|
</group>
|
||||||
android:endX="80"
|
<group
|
||||||
android:endY="80"
|
android:translateX="6"
|
||||||
android:startColor="#4e000000"
|
android:translateY="6">
|
||||||
android:startX="0"
|
<path
|
||||||
android:startY="0"
|
android:fillColor="#ffa726"
|
||||||
android:type="linear"/>
|
android:strokeWidth="1.99999297"
|
||||||
</aapt:attr>
|
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||||
</path>
|
<path
|
||||||
</group>
|
android:fillColor="#ffffff"
|
||||||
<group
|
android:strokeWidth="1.99999297"
|
||||||
android:scaleX="0.3939503"
|
android:pathData="M64.501,35.0576 C63.7095,35.0576,62.918,35.3613,62.3115,35.9678 L55.0127,43.2666 C53.7998,44.4795,53.7998,46.4306,55.0127,47.6436 L62.3115,54.9424 C63.5244,56.1553,65.4775,56.1553,66.6904,54.9424 L73.9873,47.6436 C75.2002,46.4307,75.2002,44.4796,73.9873,43.2666 L66.6904,35.9678 C66.0839,35.3613,65.2924,35.0576,64.5009,35.0576 Z M67.6729,42.6006 C69.3298,42.6006,70.6729,43.9437,70.6729,45.6006 C70.6729,47.2575,69.3298,48.6006,67.6729,48.6006 C66.016,48.6006,64.6729,47.2575,64.6729,45.6006 C64.6729,43.9437,66.016,42.6006,67.6729,42.6006 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||||
android:scaleY="0.3939503"
|
|
||||||
android:translateX="33.66343"
|
|
||||||
android:translateY="233.998">
|
|
||||||
<path
|
|
||||||
android:pathData="M88.76953 339.91602L4.1718754 424.59766 4.0000004 436 15.400391 435.82813 27.240234 424 40 424l0 -12 12 0 0 -12.73438 34.01172 -33.97656A8 8 0 0 1 84 360a8 8 0 0 1 8 -8 8 8 0 0 1 5.296882 2.01367l2.787098 -2.7832 -11.31445 -11.31445z"
|
|
||||||
android:fillColor="#eaeaea"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="#58000000"/>
|
|
||||||
</group>
|
|
||||||
<group
|
|
||||||
android:scaleX="0.3939503"
|
|
||||||
android:scaleY="0.3939503"
|
|
||||||
android:translateX="33.66343"
|
|
||||||
android:translateY="233.998">
|
|
||||||
<path
|
|
||||||
android:pathData="M4.0000004 340L4.1718754 351.40137 88.59863 435.82812 100 436 99.828122 424.59863 15.401367 340.17188Z"
|
|
||||||
android:fillColor="#64b5f6" />
|
|
||||||
</group>
|
|
||||||
<group
|
|
||||||
android:scaleX="0.3939503"
|
|
||||||
android:scaleY="0.3939503"
|
|
||||||
android:translateX="33.66343"
|
|
||||||
android:translateY="233.998">
|
|
||||||
<path
|
|
||||||
android:pathData="M81.39454 332.00195a27 27 0 0 0 -19.48634 7.90625 27 27 0 0 0 0 38.1836 27 27 0 0 0 38.1836 0 27 27 0 0 0 0 -38.1836 27 27 0 0 0 -18.69726 -7.90625zM92 352a8 8 0 0 1 8 8 8 8 0 0 1 -8 8 8 8 0 0 1 -8 -8 8 8 0 0 1 8 -8z"
|
|
||||||
android:fillColor="#eaeaea"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="#58000000" />
|
|
||||||
</group>
|
|
||||||
</group>
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
19
app/src/libre/res/drawable/ic_app_white_24dp.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="84"
|
||||||
|
android:viewportHeight="84">
|
||||||
|
<group
|
||||||
|
android:translateX="-12"
|
||||||
|
android:translateY="-12">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffa726"
|
||||||
|
android:strokeWidth="1.99999297"
|
||||||
|
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeWidth="1.99999297"
|
||||||
|
android:pathData="M64.501,35.0576 C63.7095,35.0576,62.918,35.3613,62.3115,35.9678 L55.0127,43.2666 C53.7998,44.4795,53.7998,46.4306,55.0127,47.6436 L62.3115,54.9424 C63.5244,56.1553,65.4775,56.1553,66.6904,54.9424 L73.9873,47.6436 C75.2002,46.4307,75.2002,44.4796,73.9873,43.2666 L66.6904,35.9678 C66.0839,35.3613,65.2924,35.0576,64.5009,35.0576 Z M67.6729,42.6006 C69.3298,42.6006,70.6729,43.9437,70.6729,45.6006 C70.6729,47.2575,69.3298,48.6006,67.6729,48.6006 C66.016,48.6006,64.6729,47.2575,64.6729,45.6006 C64.6729,43.9437,66.016,42.6006,67.6729,42.6006 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/green" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/green" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 12 KiB |
@@ -10,15 +10,12 @@
|
|||||||
android:anyDensity="true" />
|
android:anyDensity="true" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.FOREGROUND_SERVICE" />
|
android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.USE_BIOMETRIC" />
|
android:name="android.permission.USE_BIOMETRIC" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.VIBRATE"/>
|
android:name="android.permission.VIBRATE"/>
|
||||||
<!-- Write permission until Android 10 -->
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
|
||||||
android:maxSdkVersion="28"
|
|
||||||
tools:ignore="ScopedStorage" />
|
|
||||||
<!-- Open apps from links -->
|
<!-- Open apps from links -->
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
@@ -30,20 +27,21 @@
|
|||||||
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_rules"
|
android:fullBackupContent="@xml/old_backup_rules"
|
||||||
|
android:dataExtractionRules="@xml/backup_rules"
|
||||||
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
|
android:backupAgent="com.kunzisoft.keepass.backup.SettingsBackupAgent"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:theme="@style/KeepassDXStyle.Night"
|
android:theme="@style/KeepassDXStyle.Night"
|
||||||
tools:targetApi="n">
|
tools:targetApi="s">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.backup.api_key"
|
android:name="com.google.android.backup.api_key"
|
||||||
android:value="${googleAndroidBackupAPIKey}" />
|
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"
|
||||||
android:label="@string/app_name"
|
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
|
android:exported="true"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:windowSoftInputMode="stateHidden|stateAlwaysHidden" >
|
android:windowSoftInputMode="stateHidden|stateAlwaysHidden" >
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -52,7 +50,8 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.PasswordActivity"
|
android:name="com.kunzisoft.keepass.activities.MainCredentialActivity"
|
||||||
|
android:exported="true"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:windowSoftInputMode="adjustResize|stateUnchanged">
|
android:windowSoftInputMode="adjustResize|stateUnchanged">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -111,6 +110,7 @@
|
|||||||
<!-- Main Activity -->
|
<!-- Main Activity -->
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.GroupActivity"
|
android:name="com.kunzisoft.keepass.activities.GroupActivity"
|
||||||
|
android:exported="false"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:windowSoftInputMode="adjustPan">
|
android:windowSoftInputMode="adjustPan">
|
||||||
<meta-data
|
<meta-data
|
||||||
@@ -131,6 +131,9 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.IconPickerActivity"
|
android:name="com.kunzisoft.keepass.activities.IconPickerActivity"
|
||||||
android:configChanges="keyboardHidden" />
|
android:configChanges="keyboardHidden" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.KeyGeneratorActivity"
|
||||||
|
android:configChanges="keyboardHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.ImageViewerActivity"
|
android:name="com.kunzisoft.keepass.activities.ImageViewerActivity"
|
||||||
android:configChanges="keyboardHidden" />
|
android:configChanges="keyboardHidden" />
|
||||||
@@ -147,14 +150,17 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
|
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
|
||||||
android:theme="@style/Theme.Transparent"
|
android:theme="@style/Theme.Transparent"
|
||||||
android:configChanges="keyboardHidden" />
|
android:configChanges="keyboardHidden"
|
||||||
|
android:excludeFromRecents="true"/>
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
|
android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
|
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
|
||||||
android:theme="@style/Theme.Transparent">
|
android:theme="@style/Theme.Transparent"
|
||||||
|
android:launchMode="singleInstance"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
@@ -168,12 +174,10 @@
|
|||||||
<data android:scheme="otpauth" android:host="hotp" />
|
<data android:scheme="otpauth" android:host="hotp" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name="com.kunzisoft.keepass.activities.MagikeyboardLauncherActivity"
|
|
||||||
android:theme="@style/Theme.Transparent" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
||||||
android:label="@string/keyboard_setting_label">
|
android:label="@string/keyboard_setting_label"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@@ -199,6 +203,7 @@
|
|||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
|
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
|
||||||
android:label="@string/autofill_service_name"
|
android:label="@string/autofill_service_name"
|
||||||
|
android:exported="true"
|
||||||
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.autofill"
|
android:name="android.autofill"
|
||||||
@@ -210,6 +215,7 @@
|
|||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService"
|
android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService"
|
||||||
android:label="@string/keyboard_label"
|
android:label="@string/keyboard_label"
|
||||||
|
android:exported="true"
|
||||||
android:permission="android.permission.BIND_INPUT_METHOD" >
|
android:permission="android.permission.BIND_INPUT_METHOD" >
|
||||||
<meta-data android:name="android.view.im"
|
<meta-data android:name="android.view.im"
|
||||||
android:resource="@xml/keyboard_method"/>
|
android:resource="@xml/keyboard_method"/>
|
||||||
@@ -221,6 +227,14 @@
|
|||||||
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
|
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
<receiver
|
||||||
|
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.app.action.ENTER_KNOX_DESKTOP_MODE" />
|
||||||
|
<action android:name="android.app.action.EXIT_KNOX_DESKTOP_MODE" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
||||||
</application>
|
</application>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ 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 com.kunzisoft.keepass.utils.UriUtil
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
class AboutActivity : StylishActivity() {
|
class AboutActivity : StylishActivity() {
|
||||||
@@ -45,6 +46,12 @@ class AboutActivity : StylishActivity() {
|
|||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
|
val appName = if (UriUtil.contributingUser(this))
|
||||||
|
getString(R.string.app_name) + " " + getString(R.string.app_name_part3)
|
||||||
|
else
|
||||||
|
getString(R.string.app_name)
|
||||||
|
findViewById<TextView>(R.id.activity_about_app_name).text = appName
|
||||||
|
|
||||||
var version: String
|
var version: String
|
||||||
var build: String
|
var build: String
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -23,28 +23,33 @@ import android.app.Activity
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentSender
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.view.inputmethod.InlineSuggestionsRequest
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
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.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper.EXTRA_INLINE_SUGGESTIONS_REQUEST
|
import com.kunzisoft.keepass.autofill.CompatInlineSuggestionsRequest
|
||||||
import com.kunzisoft.keepass.autofill.KeeAutofillService
|
import com.kunzisoft.keepass.autofill.KeeAutofillService
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class AutofillLauncherActivity : DatabaseModeActivity() {
|
class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||||
|
|
||||||
|
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||||
|
AutofillHelper.buildActivityResultLauncher(this, true)
|
||||||
|
else null
|
||||||
|
|
||||||
override fun applyCustomStyle(): Boolean {
|
override fun applyCustomStyle(): Boolean {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -60,17 +65,37 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
|
EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode ->
|
||||||
when (specialMode) {
|
when (specialMode) {
|
||||||
SpecialMode.SELECTION -> {
|
SpecialMode.SELECTION -> {
|
||||||
// Build search param
|
intent.getBundleExtra(KEY_SELECTION_BUNDLE)?.let { bundle ->
|
||||||
val searchInfo = SearchInfo().apply {
|
// To pass extra inline request
|
||||||
applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID)
|
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
|
||||||
webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME)
|
compatInlineSuggestionsRequest = bundle.getParcelable(KEY_INLINE_SUGGESTION)
|
||||||
manualSelection = intent.getBooleanExtra(KEY_MANUAL_SELECTION, false)
|
}
|
||||||
}
|
// Build search param
|
||||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
bundle.getParcelable<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
|
||||||
searchInfo.webDomain = concreteWebDomain
|
SearchInfo.getConcreteWebDomain(
|
||||||
launchSelection(database, searchInfo)
|
this,
|
||||||
|
searchInfo.webDomain
|
||||||
|
) { concreteWebDomain ->
|
||||||
|
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
||||||
|
val assistStructure = AutofillHelper
|
||||||
|
.retrieveAutofillComponent(intent)
|
||||||
|
?.assistStructure
|
||||||
|
val newAutofillComponent = if (assistStructure != null) {
|
||||||
|
AutofillComponent(
|
||||||
|
assistStructure,
|
||||||
|
compatInlineSuggestionsRequest
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
searchInfo.webDomain = concreteWebDomain
|
||||||
|
launchSelection(database, newAutofillComponent, searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Remove bundle
|
||||||
|
intent.removeExtra(KEY_SELECTION_BUNDLE)
|
||||||
}
|
}
|
||||||
SpecialMode.REGISTRATION -> {
|
SpecialMode.REGISTRATION -> {
|
||||||
// To register info
|
// To register info
|
||||||
@@ -91,10 +116,8 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun launchSelection(database: Database?,
|
private fun launchSelection(database: Database?,
|
||||||
|
autofillComponent: AutofillComponent?,
|
||||||
searchInfo: SearchInfo) {
|
searchInfo: SearchInfo) {
|
||||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
|
||||||
val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent)
|
|
||||||
|
|
||||||
if (autofillComponent == null) {
|
if (autofillComponent == null) {
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(Activity.RESULT_CANCELED)
|
||||||
finish()
|
finish()
|
||||||
@@ -119,6 +142,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
// Show the database UI to select the entry
|
// Show the database UI to select the entry
|
||||||
GroupActivity.launchForAutofillResult(this,
|
GroupActivity.launchForAutofillResult(this,
|
||||||
openedDatabase,
|
openedDatabase,
|
||||||
|
mAutofillActivityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
false)
|
false)
|
||||||
@@ -126,6 +150,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
{
|
{
|
||||||
// If database not open
|
// If database not open
|
||||||
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
||||||
|
mAutofillActivityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
@@ -186,55 +211,47 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
Toast.makeText(this.applicationContext, R.string.autofill_read_only_save, Toast.LENGTH_LONG).show()
|
Toast.makeText(this.applicationContext, R.string.autofill_read_only_save, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
|
||||||
|
|
||||||
if (PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
|
|
||||||
// Close the database
|
|
||||||
sendBroadcast(Intent(LOCK_ACTION))
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val KEY_MANUAL_SELECTION = "KEY_MANUAL_SELECTION"
|
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
|
||||||
private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
|
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
||||||
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
|
private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION"
|
||||||
private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME"
|
|
||||||
|
|
||||||
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
|
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
|
||||||
|
|
||||||
fun getPendingIntentForSelection(context: Context,
|
fun getPendingIntentForSelection(context: Context,
|
||||||
searchInfo: SearchInfo? = null,
|
searchInfo: SearchInfo? = null,
|
||||||
inlineSuggestionsRequest: InlineSuggestionsRequest? = null): PendingIntent {
|
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent {
|
||||||
return PendingIntent.getActivity(context, 0,
|
return PendingIntent.getActivity(context, 0,
|
||||||
// Doesn't work with Parcelable (don't know why?)
|
// Doesn't work with direct extra Parcelable (don't know why?)
|
||||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
// Wrap into a bundle to bypass the problem
|
||||||
searchInfo?.let {
|
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||||
putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId)
|
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
|
||||||
putExtra(KEY_SEARCH_DOMAIN, it.webDomain)
|
putParcelable(KEY_SEARCH_INFO, searchInfo)
|
||||||
putExtra(KEY_SEARCH_SCHEME, it.webScheme)
|
|
||||||
putExtra(KEY_MANUAL_SELECTION, it.manualSelection)
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
inlineSuggestionsRequest?.let {
|
putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
|
||||||
putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
},
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
} else {
|
||||||
|
PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPendingIntentForRegistration(context: Context,
|
fun getPendingIntentForRegistration(context: Context,
|
||||||
registerInfo: RegisterInfo): PendingIntent {
|
registerInfo: RegisterInfo): PendingIntent {
|
||||||
return PendingIntent.getActivity(context, 0,
|
return PendingIntent.getActivity(context, 0,
|
||||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||||
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
||||||
putExtra(KEY_REGISTER_INFO, registerInfo)
|
putExtra(KEY_REGISTER_INFO, registerInfo)
|
||||||
},
|
},
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
} else {
|
||||||
|
PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchForRegistration(context: Context,
|
fun launchForRegistration(context: Context,
|
||||||
|
|||||||
@@ -32,15 +32,25 @@ 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.ProgressBar
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||||
|
import androidx.core.graphics.BlendModeCompat
|
||||||
|
import androidx.core.graphics.ColorUtils
|
||||||
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||||
|
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
|
import com.kunzisoft.keepass.adapters.TagsAdapter
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
@@ -57,26 +67,34 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
|||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.*
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
|
import com.kunzisoft.keepass.view.changeControlColor
|
||||||
|
import com.kunzisoft.keepass.view.changeTitleColor
|
||||||
import com.kunzisoft.keepass.view.hideByFading
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.HashMap
|
|
||||||
|
|
||||||
class EntryActivity : DatabaseLockActivity() {
|
class EntryActivity : DatabaseLockActivity() {
|
||||||
|
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
||||||
|
private var appBarLayout: AppBarLayout? = null
|
||||||
private var titleIconView: ImageView? = null
|
private var titleIconView: ImageView? = null
|
||||||
private var historyView: View? = null
|
private var historyView: View? = null
|
||||||
private var entryProgress: ProgressBar? = null
|
private var tagsListView: RecyclerView? = null
|
||||||
|
private var entryContentTab: TabLayout? = null
|
||||||
|
private var tagsAdapter: TagsAdapter? = null
|
||||||
|
private var entryProgress: LinearProgressIndicator? = null
|
||||||
private var lockView: View? = null
|
private var lockView: View? = null
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
private var loadingView: ProgressBar? = null
|
private var loadingView: ProgressBar? = null
|
||||||
|
|
||||||
private val mEntryViewModel: EntryViewModel by viewModels()
|
private val mEntryViewModel: EntryViewModel by viewModels()
|
||||||
|
|
||||||
|
private val mEntryActivityEducation = EntryActivityEducation(this)
|
||||||
|
|
||||||
private var mMainEntryId: NodeId<UUID>? = null
|
private var mMainEntryId: NodeId<UUID>? = null
|
||||||
private var mHistoryPosition: Int = -1
|
private var mHistoryPosition: Int = -1
|
||||||
private var mEntryIsHistory: Boolean = false
|
private var mEntryIsHistory: Boolean = false
|
||||||
@@ -84,11 +102,21 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
private var mEntryLoaded = false
|
private var mEntryLoaded = false
|
||||||
|
|
||||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||||
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
|
|
||||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
private var mAttachmentSelected: Attachment? = null
|
||||||
|
|
||||||
|
private var mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) {
|
||||||
|
// Reload the current id from database
|
||||||
|
mEntryViewModel.loadDatabase(mDatabase)
|
||||||
|
}
|
||||||
|
|
||||||
private var mIcon: IconImage? = null
|
private var mIcon: IconImage? = null
|
||||||
private var mIconColor: Int = 0
|
private var mColorAccent: Int = 0
|
||||||
|
private var mControlColor: Int = 0
|
||||||
|
private var mColorPrimary: Int = 0
|
||||||
|
private var mColorBackground: Int = 0
|
||||||
|
private var mBackgroundColor: Int? = null
|
||||||
|
private var mForegroundColor: Int? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -103,8 +131,11 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
// Get views
|
// Get views
|
||||||
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
|
||||||
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
||||||
|
appBarLayout = findViewById(R.id.app_bar)
|
||||||
titleIconView = findViewById(R.id.entry_icon)
|
titleIconView = findViewById(R.id.entry_icon)
|
||||||
historyView = findViewById(R.id.history_container)
|
historyView = findViewById(R.id.history_container)
|
||||||
|
tagsListView = findViewById(R.id.entry_tags_list_view)
|
||||||
|
entryContentTab = findViewById(R.id.entry_content_tab)
|
||||||
entryProgress = findViewById(R.id.entry_progress)
|
entryProgress = findViewById(R.id.entry_progress)
|
||||||
lockView = findViewById(R.id.lock_button)
|
lockView = findViewById(R.id.lock_button)
|
||||||
loadingView = findViewById(R.id.loading)
|
loadingView = findViewById(R.id.loading)
|
||||||
@@ -113,10 +144,39 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
collapsingToolbarLayout?.title = " "
|
collapsingToolbarLayout?.title = " "
|
||||||
toolbar?.title = " "
|
toolbar?.title = " "
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
// Retrieve the textColor to tint the toolbar
|
||||||
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||||
mIconColor = taIconColor.getColor(0, Color.BLACK)
|
val taControlColor = theme.obtainStyledAttributes(intArrayOf(R.attr.toolbarColorControl))
|
||||||
taIconColor.recycle()
|
val taColorPrimary = theme.obtainStyledAttributes(intArrayOf(R.attr.colorPrimary))
|
||||||
|
val taColorBackground = theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
|
||||||
|
mColorAccent = taColorAccent.getColor(0, Color.BLACK)
|
||||||
|
mControlColor = taControlColor.getColor(0, Color.BLACK)
|
||||||
|
mColorPrimary = taColorPrimary.getColor(0, Color.BLACK)
|
||||||
|
mColorBackground = taColorBackground.getColor(0, Color.BLACK)
|
||||||
|
taColorAccent.recycle()
|
||||||
|
taControlColor.recycle()
|
||||||
|
taColorPrimary.recycle()
|
||||||
|
taColorBackground.recycle()
|
||||||
|
|
||||||
|
// Init Tags adapter
|
||||||
|
tagsAdapter = TagsAdapter(this)
|
||||||
|
tagsListView?.apply {
|
||||||
|
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
adapter = tagsAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init content tab
|
||||||
|
entryContentTab?.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||||
|
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||||
|
mEntryViewModel.selectSection(EntryViewModel.EntrySection.
|
||||||
|
getEntrySectionByPosition(tab?.position ?: 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
||||||
|
|
||||||
|
override fun onTabReselected(tab: TabLayout.Tab?) {}
|
||||||
|
})
|
||||||
|
|
||||||
// Get Entry from UUID
|
// Get Entry from UUID
|
||||||
try {
|
try {
|
||||||
@@ -133,6 +193,15 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
|
|
||||||
// Init SAF manager
|
// Init SAF manager
|
||||||
mExternalFileHelper = ExternalFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
mExternalFileHelper?.buildCreateDocument { createdFileUri ->
|
||||||
|
mAttachmentSelected?.let { attachment ->
|
||||||
|
if (createdFileUri != null) {
|
||||||
|
mAttachmentFileBinderManager
|
||||||
|
?.startDownloadAttachment(createdFileUri, attachment)
|
||||||
|
}
|
||||||
|
mAttachmentSelected = null
|
||||||
|
}
|
||||||
|
}
|
||||||
// Init attachment service binder manager
|
// Init attachment service binder manager
|
||||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||||
|
|
||||||
@@ -140,6 +209,10 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
lockAndExit()
|
lockAndExit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mEntryViewModel.sectionSelected.observe(this) { entrySection ->
|
||||||
|
entryContentTab?.getTabAt(entrySection.position)?.select()
|
||||||
|
}
|
||||||
|
|
||||||
mEntryViewModel.entryInfoHistory.observe(this) { entryInfoHistory ->
|
mEntryViewModel.entryInfoHistory.observe(this) { entryInfoHistory ->
|
||||||
if (entryInfoHistory != null) {
|
if (entryInfoHistory != null) {
|
||||||
this.mMainEntryId = entryInfoHistory.mainEntryId
|
this.mMainEntryId = entryInfoHistory.mainEntryId
|
||||||
@@ -152,10 +225,8 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
// Assign history dedicated view
|
// Assign history dedicated view
|
||||||
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
|
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
|
||||||
if (entryIsHistory) {
|
if (entryIsHistory) {
|
||||||
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
|
||||||
collapsingToolbarLayout?.contentScrim =
|
collapsingToolbarLayout?.contentScrim =
|
||||||
ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
ColorDrawable(mColorAccent)
|
||||||
taColorAccent.recycle()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val entryInfo = entryInfoHistory.entryInfo
|
val entryInfo = entryInfoHistory.entryInfo
|
||||||
@@ -170,15 +241,20 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
// Assign title icon
|
// Assign title icon
|
||||||
mIcon = entryInfo.icon
|
mIcon = entryInfo.icon
|
||||||
titleIconView?.let { iconView ->
|
|
||||||
mIconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, mIconColor)
|
|
||||||
}
|
|
||||||
// Assign title text
|
// Assign title text
|
||||||
val entryTitle =
|
val entryTitle =
|
||||||
if (entryInfo.title.isNotEmpty()) entryInfo.title else entryInfo.id.toString()
|
if (entryInfo.title.isNotEmpty()) entryInfo.title else UuidUtil.toHexString(entryInfo.id)
|
||||||
collapsingToolbarLayout?.title = entryTitle
|
collapsingToolbarLayout?.title = entryTitle
|
||||||
toolbar?.title = entryTitle
|
toolbar?.title = entryTitle
|
||||||
mUrl = entryInfo.url
|
mUrl = entryInfo.url
|
||||||
|
// Assign tags
|
||||||
|
val tags = entryInfo.tags
|
||||||
|
tagsListView?.visibility = if (tags.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
tagsAdapter?.setTags(tags)
|
||||||
|
// Assign colors
|
||||||
|
val showEntryColors = PreferencesUtil.showEntryColors(this)
|
||||||
|
mBackgroundColor = if (showEntryColors) entryInfo.backgroundColor else null
|
||||||
|
mForegroundColor = if (showEntryColors) entryInfo.foregroundColor else null
|
||||||
|
|
||||||
loadingView?.hideByFading()
|
loadingView?.hideByFading()
|
||||||
mEntryLoaded = true
|
mEntryLoaded = true
|
||||||
@@ -190,9 +266,9 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement ->
|
mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement ->
|
||||||
if (otpElement == null)
|
if (otpElement == null) {
|
||||||
entryProgress?.visibility = View.GONE
|
entryProgress?.visibility = View.GONE
|
||||||
when (otpElement?.type) {
|
} else when (otpElement.type) {
|
||||||
// Only add token if HOTP
|
// Only add token if HOTP
|
||||||
OtpType.HOTP -> {
|
OtpType.HOTP -> {
|
||||||
entryProgress?.visibility = View.GONE
|
entryProgress?.visibility = View.GONE
|
||||||
@@ -201,7 +277,7 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
OtpType.TOTP -> {
|
OtpType.TOTP -> {
|
||||||
entryProgress?.apply {
|
entryProgress?.apply {
|
||||||
max = otpElement.period
|
max = otpElement.period
|
||||||
progress = otpElement.secondsRemaining
|
setProgressCompat(otpElement.secondsRemaining, true)
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,9 +285,8 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mEntryViewModel.attachmentSelected.observe(this) { attachmentSelected ->
|
mEntryViewModel.attachmentSelected.observe(this) { attachmentSelected ->
|
||||||
mExternalFileHelper?.createDocument(attachmentSelected.name)?.let { requestCode ->
|
mAttachmentSelected = attachmentSelected
|
||||||
mAttachmentsToDownload[requestCode] = attachmentSelected
|
mExternalFileHelper?.createDocument(attachmentSelected.name)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mEntryViewModel.historySelected.observe(this) { historySelected ->
|
mEntryViewModel.historySelected.observe(this) { historySelected ->
|
||||||
@@ -220,7 +295,8 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
this,
|
this,
|
||||||
database,
|
database,
|
||||||
historySelected.nodeId,
|
historySelected.nodeId,
|
||||||
historySelected.historyPosition
|
historySelected.historyPosition,
|
||||||
|
mEntryActivityResultLauncher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,13 +314,6 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
mEntryViewModel.loadDatabase(database)
|
mEntryViewModel.loadDatabase(database)
|
||||||
|
|
||||||
// Assign title icon
|
|
||||||
mIcon?.let { icon ->
|
|
||||||
titleIconView?.let { iconView ->
|
|
||||||
mIconDrawableFactory?.assignDatabaseIcon(iconView, icon, mIconColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
@@ -282,6 +351,11 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep the screen on
|
||||||
|
if (PreferencesUtil.isKeepScreenOnEnabled(this)) {
|
||||||
|
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -290,31 +364,33 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
private fun applyToolbarColors() {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
appBarLayout?.setBackgroundColor(mBackgroundColor ?: mColorPrimary)
|
||||||
|
collapsingToolbarLayout?.contentScrim = ColorDrawable(mBackgroundColor ?: mColorPrimary)
|
||||||
when (requestCode) {
|
val backgroundDarker = if (mBackgroundColor != null) {
|
||||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
ColorUtils.blendARGB(mBackgroundColor!!, Color.WHITE, 0.1f)
|
||||||
// Reload the current id from database
|
} else {
|
||||||
mEntryViewModel.loadDatabase(mDatabase)
|
mColorBackground
|
||||||
}
|
}
|
||||||
}
|
titleIconView?.background?.colorFilter = BlendModeColorFilterCompat
|
||||||
|
.createBlendModeColorFilterCompat(backgroundDarker, BlendModeCompat.SRC_IN)
|
||||||
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
mIcon?.let { icon ->
|
||||||
if (createdFileUri != null) {
|
titleIconView?.let { iconView ->
|
||||||
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
|
mIconDrawableFactory?.assignDatabaseIcon(
|
||||||
mAttachmentFileBinderManager
|
iconView,
|
||||||
?.startDownloadAttachment(createdFileUri, attachmentToDownload)
|
icon,
|
||||||
}
|
mForegroundColor ?: mColorAccent
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
toolbar?.changeControlColor(mForegroundColor ?: mControlColor)
|
||||||
|
collapsingToolbarLayout?.changeTitleColor(mForegroundColor ?: mControlColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
if (mEntryLoaded) {
|
if (mEntryLoaded) {
|
||||||
val inflater = menuInflater
|
val inflater = menuInflater
|
||||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
|
||||||
|
|
||||||
inflater.inflate(R.menu.entry, menu)
|
inflater.inflate(R.menu.entry, menu)
|
||||||
inflater.inflate(R.menu.database, menu)
|
inflater.inflate(R.menu.database, menu)
|
||||||
@@ -325,11 +401,7 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
|
|
||||||
// Show education views
|
// Show education views
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
performedNextEducation(
|
performedNextEducation(menu)
|
||||||
EntryActivityEducation(
|
|
||||||
this
|
|
||||||
), menu
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -341,39 +413,44 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
if (mEntryIsHistory || mDatabaseReadOnly) {
|
if (mEntryIsHistory || mDatabaseReadOnly) {
|
||||||
menu?.findItem(R.id.menu_save_database)?.isVisible = false
|
menu?.findItem(R.id.menu_save_database)?.isVisible = false
|
||||||
|
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||||
menu?.findItem(R.id.menu_edit)?.isVisible = false
|
menu?.findItem(R.id.menu_edit)?.isVisible = false
|
||||||
}
|
}
|
||||||
|
if (!mMergeDataAllowed) {
|
||||||
|
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||||
|
}
|
||||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||||
|
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||||
menu?.findItem(R.id.menu_reload_database)?.isVisible = false
|
menu?.findItem(R.id.menu_reload_database)?.isVisible = false
|
||||||
}
|
}
|
||||||
|
applyToolbarColors()
|
||||||
return super.onPrepareOptionsMenu(menu)
|
return super.onPrepareOptionsMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
private fun performedNextEducation(menu: Menu) {
|
||||||
menu: Menu) {
|
|
||||||
val entryFragment = supportFragmentManager.findFragmentByTag(ENTRY_FRAGMENT_TAG)
|
val entryFragment = supportFragmentManager.findFragmentByTag(ENTRY_FRAGMENT_TAG)
|
||||||
as? EntryFragment?
|
as? EntryFragment?
|
||||||
val entryFieldCopyView: View? = entryFragment?.firstEntryFieldCopyView()
|
val entryFieldCopyView: View? = entryFragment?.firstEntryFieldCopyView()
|
||||||
val entryCopyEducationPerformed = entryFieldCopyView != null
|
val entryCopyEducationPerformed = entryFieldCopyView != null
|
||||||
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
&& mEntryActivityEducation.checkAndPerformedEntryCopyEducation(
|
||||||
entryFieldCopyView,
|
entryFieldCopyView,
|
||||||
{
|
{
|
||||||
entryFragment.launchEntryCopyEducationAction()
|
entryFragment.launchEntryCopyEducationAction()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(entryActivityEducation, menu)
|
performedNextEducation(menu)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!entryCopyEducationPerformed) {
|
if (!entryCopyEducationPerformed) {
|
||||||
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
|
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
|
||||||
// entryEditEducationPerformed
|
// entryEditEducationPerformed
|
||||||
menuEditView != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
|
menuEditView != null && mEntryActivityEducation.checkAndPerformedEntryEditEducation(
|
||||||
menuEditView,
|
menuEditView,
|
||||||
{
|
{
|
||||||
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(entryActivityEducation, menu)
|
performedNextEducation(menu)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -381,17 +458,14 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.menu_contribute -> {
|
|
||||||
MenuUtil.onContributionItemSelected(this)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
R.id.menu_edit -> {
|
R.id.menu_edit -> {
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
mMainEntryId?.let { entryId ->
|
mMainEntryId?.let { entryId ->
|
||||||
EntryEditActivity.launchToUpdate(
|
EntryEditActivity.launchToUpdate(
|
||||||
this,
|
this,
|
||||||
database,
|
database,
|
||||||
entryId
|
entryId,
|
||||||
|
mEntryActivityResultLauncher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -420,6 +494,9 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
R.id.menu_save_database -> {
|
R.id.menu_save_database -> {
|
||||||
saveDatabase()
|
saveDatabase()
|
||||||
}
|
}
|
||||||
|
R.id.menu_merge_database -> {
|
||||||
|
mergeDatabase()
|
||||||
|
}
|
||||||
R.id.menu_reload_database -> {
|
R.id.menu_reload_database -> {
|
||||||
reloadDatabase()
|
reloadDatabase()
|
||||||
}
|
}
|
||||||
@@ -432,7 +509,7 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
// Transit data in previous Activity after an update
|
// Transit data in previous Activity after an update
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
|
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
|
||||||
setResult(EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE, this)
|
setResult(Activity.RESULT_OK, this)
|
||||||
}
|
}
|
||||||
super.finish()
|
super.finish()
|
||||||
}
|
}
|
||||||
@@ -450,15 +527,13 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
*/
|
*/
|
||||||
fun launch(activity: Activity,
|
fun launch(activity: Activity,
|
||||||
database: Database,
|
database: Database,
|
||||||
entryId: NodeId<UUID>) {
|
entryId: NodeId<UUID>,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
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, entryId)
|
intent.putExtra(KEY_ENTRY, entryId)
|
||||||
activity.startActivityForResult(
|
activityResultLauncher.launch(intent)
|
||||||
intent,
|
|
||||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -469,16 +544,14 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
fun launch(activity: Activity,
|
fun launch(activity: Activity,
|
||||||
database: Database,
|
database: Database,
|
||||||
entryId: NodeId<UUID>,
|
entryId: NodeId<UUID>,
|
||||||
historyPosition: Int) {
|
historyPosition: Int,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
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, entryId)
|
intent.putExtra(KEY_ENTRY, entryId)
|
||||||
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
||||||
activity.startActivityForResult(
|
activityResultLauncher.launch(intent)
|
||||||
intent,
|
|
||||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,12 +33,17 @@ import android.view.Menu
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.widget.NestedScrollView
|
import androidx.core.widget.NestedScrollView
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.*
|
import com.kunzisoft.keepass.activities.dialogs.*
|
||||||
@@ -53,9 +58,13 @@ import com.kunzisoft.keepass.autofill.AutofillHelper
|
|||||||
import com.kunzisoft.keepass.database.element.*
|
import com.kunzisoft.keepass.database.element.*
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.template.*
|
import com.kunzisoft.keepass.database.element.template.Template
|
||||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||||
import com.kunzisoft.keepass.model.*
|
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||||
|
import com.kunzisoft.keepass.model.AttachmentState
|
||||||
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
||||||
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||||
@@ -69,14 +78,13 @@ import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
|||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.view.*
|
import com.kunzisoft.keepass.view.*
|
||||||
|
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
|
||||||
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
class EntryEditActivity : DatabaseLockActivity(),
|
class EntryEditActivity : DatabaseLockActivity(),
|
||||||
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
|
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
|
||||||
GeneratePasswordDialogFragment.GeneratePasswordListener,
|
|
||||||
SetOTPDialogFragment.CreateOtpListener,
|
SetOTPDialogFragment.CreateOtpListener,
|
||||||
DatePickerDialog.OnDateSetListener,
|
DatePickerDialog.OnDateSetListener,
|
||||||
TimePickerDialog.OnTimeSetListener,
|
TimePickerDialog.OnTimeSetListener,
|
||||||
@@ -96,6 +104,9 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
private var mTemplate: Template? = null
|
private var mTemplate: Template? = null
|
||||||
private var mIsTemplate: Boolean = false
|
private var mIsTemplate: Boolean = false
|
||||||
private var mEntryLoaded: Boolean = false
|
private var mEntryLoaded: Boolean = false
|
||||||
|
private var mTemplatesSelectorAdapter: TemplatesSelectorAdapter? = null
|
||||||
|
|
||||||
|
private val mColorPickerViewModel: ColorPickerViewModel by viewModels()
|
||||||
|
|
||||||
private var mAllowCustomFields = false
|
private var mAllowCustomFields = false
|
||||||
private var mAllowOTP = false
|
private var mAllowOTP = false
|
||||||
@@ -104,7 +115,25 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||||
// Education
|
// Education
|
||||||
private var entryEditActivityEducation: EntryEditActivityEducation? = null
|
private var mEntryEditActivityEducation = EntryEditActivityEducation(this)
|
||||||
|
|
||||||
|
private var mIconSelectionActivityResultLauncher = IconPickerActivity.registerIconSelectionForResult(this) { icon ->
|
||||||
|
mEntryEditViewModel.selectIcon(icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mPasswordField: Field? = null
|
||||||
|
private var mKeyGeneratorResultLauncher = KeyGeneratorActivity.registerForGeneratedKeyResult(this) { keyGenerated ->
|
||||||
|
keyGenerated?.let {
|
||||||
|
mPasswordField?.let {
|
||||||
|
it.protectedValue.stringValue = keyGenerated
|
||||||
|
mEntryEditViewModel.selectPassword(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mPasswordField = null
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
performedNextEducation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// To ask data lost only one time
|
// To ask data lost only one time
|
||||||
private var backPressedAlreadyApproved = false
|
private var backPressedAlreadyApproved = false
|
||||||
@@ -154,9 +183,22 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
// To retrieve attachment
|
// To retrieve attachment
|
||||||
mExternalFileHelper = ExternalFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
|
uri?.let { attachmentToUploadUri ->
|
||||||
|
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 {
|
||||||
|
mEntryEditViewModel.buildNewAttachment(attachmentToUploadUri, fileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||||
// Verify the education views
|
|
||||||
entryEditActivityEducation = EntryEditActivityEducation(this)
|
|
||||||
|
|
||||||
// Lock button
|
// Lock button
|
||||||
lockView?.setOnClickListener { lockAndExit() }
|
lockView?.setOnClickListener { lockAndExit() }
|
||||||
@@ -175,11 +217,13 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
templateSelectorSpinner?.apply {
|
templateSelectorSpinner?.apply {
|
||||||
// Build template selector
|
// Build template selector
|
||||||
if (templates.isNotEmpty()) {
|
if (templates.isNotEmpty()) {
|
||||||
adapter = TemplatesSelectorAdapter(
|
mTemplatesSelectorAdapter = TemplatesSelectorAdapter(
|
||||||
this@EntryEditActivity,
|
this@EntryEditActivity,
|
||||||
mIconDrawableFactory,
|
|
||||||
templates
|
templates
|
||||||
)
|
).apply {
|
||||||
|
iconDrawableFactory = mIconDrawableFactory
|
||||||
|
}
|
||||||
|
adapter = mTemplatesSelectorAdapter
|
||||||
val selectedTemplate = if (mTemplate != null)
|
val selectedTemplate = if (mTemplate != null)
|
||||||
mTemplate
|
mTemplate
|
||||||
else
|
else
|
||||||
@@ -213,7 +257,16 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
// View model listeners
|
// View model listeners
|
||||||
mEntryEditViewModel.requestIconSelection.observe(this) { iconImage ->
|
mEntryEditViewModel.requestIconSelection.observe(this) { iconImage ->
|
||||||
IconPickerActivity.launch(this@EntryEditActivity, iconImage)
|
IconPickerActivity.launch(this@EntryEditActivity, iconImage, mIconSelectionActivityResultLauncher)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.requestColorSelection.observe(this) { color ->
|
||||||
|
ColorPickerDialogFragment.newInstance(color)
|
||||||
|
.show(supportFragmentManager, "ColorPickerFragment")
|
||||||
|
}
|
||||||
|
|
||||||
|
mColorPickerViewModel.colorPicked.observe(this) { color ->
|
||||||
|
mEntryEditViewModel.selectColor(color)
|
||||||
}
|
}
|
||||||
|
|
||||||
mEntryEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
|
mEntryEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
|
||||||
@@ -231,9 +284,8 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
mEntryEditViewModel.requestPasswordSelection.observe(this) { passwordField ->
|
mEntryEditViewModel.requestPasswordSelection.observe(this) { passwordField ->
|
||||||
GeneratePasswordDialogFragment
|
mPasswordField = passwordField
|
||||||
.getInstance(passwordField)
|
KeyGeneratorActivity.launch(this, mKeyGeneratorResultLauncher)
|
||||||
.show(supportFragmentManager, "PasswordGeneratorFragment")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mEntryEditViewModel.requestCustomFieldEdition.observe(this) { field ->
|
mEntryEditViewModel.requestCustomFieldEdition.observe(this) { field ->
|
||||||
@@ -321,6 +373,10 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
mAllowCustomFields = database?.allowEntryCustomFields() == true
|
mAllowCustomFields = database?.allowEntryCustomFields() == true
|
||||||
mAllowOTP = database?.allowOTP == true
|
mAllowOTP = database?.allowOTP == true
|
||||||
mEntryEditViewModel.loadDatabase(database)
|
mEntryEditViewModel.loadDatabase(database)
|
||||||
|
mTemplatesSelectorAdapter?.apply {
|
||||||
|
iconDrawableFactory = mIconDrawableFactory
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
@@ -379,9 +435,10 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
private fun entryValidatedForKeyboardSelection(database: Database, entry: Entry) {
|
private fun entryValidatedForKeyboardSelection(database: Database, entry: Entry) {
|
||||||
// Populate Magikeyboard with entry
|
// Populate Magikeyboard with entry
|
||||||
populateKeyboardAndMoveAppToBackground(this,
|
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||||
entry.getEntryInfo(database),
|
this,
|
||||||
intent)
|
entry.getEntryInfo(database)
|
||||||
|
)
|
||||||
onValidateSpecialMode()
|
onValidateSpecialMode()
|
||||||
// Don't keep activity history for entry edition
|
// Don't keep activity history for entry edition
|
||||||
finishForEntryResult(entry)
|
finishForEntryResult(entry)
|
||||||
@@ -472,29 +529,6 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
|
|
||||||
IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon ->
|
|
||||||
mEntryEditViewModel.selectIcon(icon)
|
|
||||||
}
|
|
||||||
|
|
||||||
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
|
||||||
uri?.let { attachmentToUploadUri ->
|
|
||||||
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 {
|
|
||||||
mEntryEditViewModel.buildNewAttachment(attachmentToUploadUri, fileName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up OTP (HOTP or TOTP) and add it as extra field
|
* Set up OTP (HOTP or TOTP) and add it as extra field
|
||||||
*/
|
*/
|
||||||
@@ -518,10 +552,8 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
if (mEntryLoaded) {
|
if (mEntryLoaded) {
|
||||||
menuInflater.inflate(R.menu.entry_edit, menu)
|
menuInflater.inflate(R.menu.entry_edit, menu)
|
||||||
entryEditActivityEducation?.let {
|
Handler(Looper.getMainLooper()).post {
|
||||||
Handler(Looper.getMainLooper()).post {
|
performedNextEducation()
|
||||||
performedNextEducation(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -548,19 +580,19 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
return super.onPrepareOptionsMenu(menu)
|
return super.onPrepareOptionsMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
private fun performedNextEducation() {
|
||||||
|
|
||||||
val entryEditFragment = supportFragmentManager.findFragmentById(R.id.entry_edit_content)
|
val entryEditFragment = supportFragmentManager.findFragmentById(R.id.entry_edit_content)
|
||||||
as? EntryEditFragment?
|
as? EntryEditFragment?
|
||||||
val generatePasswordView = entryEditFragment?.getActionImageView()
|
val generatePasswordView = entryEditFragment?.getActionImageView()
|
||||||
val generatePasswordEductionPerformed = generatePasswordView != null
|
val generatePasswordEductionPerformed = generatePasswordView != null
|
||||||
&& entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
&& mEntryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
||||||
generatePasswordView,
|
generatePasswordView,
|
||||||
{
|
{
|
||||||
entryEditFragment.launchGeneratePasswordEductionAction()
|
entryEditFragment.launchGeneratePasswordEductionAction()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(entryEditActivityEducation)
|
performedNextEducation()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -569,33 +601,33 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
val addNewFieldEducationPerformed = mAllowCustomFields
|
val addNewFieldEducationPerformed = mAllowCustomFields
|
||||||
&& addNewFieldView != null
|
&& addNewFieldView != null
|
||||||
&& addNewFieldView.isVisible
|
&& addNewFieldView.isVisible
|
||||||
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
|
&& mEntryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
|
||||||
addNewFieldView,
|
addNewFieldView,
|
||||||
{
|
{
|
||||||
addNewCustomField()
|
addNewCustomField()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(entryEditActivityEducation)
|
performedNextEducation()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (!addNewFieldEducationPerformed) {
|
if (!addNewFieldEducationPerformed) {
|
||||||
val attachmentView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_attachment)
|
val attachmentView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_attachment)
|
||||||
val addAttachmentEducationPerformed = attachmentView != null
|
val addAttachmentEducationPerformed = attachmentView != null
|
||||||
&& attachmentView.isVisible
|
&& attachmentView.isVisible
|
||||||
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation(
|
&& mEntryEditActivityEducation.checkAndPerformedAttachmentEducation(
|
||||||
attachmentView,
|
attachmentView,
|
||||||
{
|
{
|
||||||
mExternalFileHelper?.openDocument()
|
addNewAttachment()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(entryEditActivityEducation)
|
performedNextEducation()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (!addAttachmentEducationPerformed) {
|
if (!addAttachmentEducationPerformed) {
|
||||||
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
|
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
|
||||||
setupOtpView != null
|
setupOtpView != null
|
||||||
&& setupOtpView.isVisible
|
&& setupOtpView.isVisible
|
||||||
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
|
&& mEntryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
|
||||||
setupOtpView,
|
setupOtpView,
|
||||||
{
|
{
|
||||||
setupOtp()
|
setupOtp()
|
||||||
@@ -640,17 +672,6 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
mEntryEditViewModel.selectTime(hours, minutes)
|
mEntryEditViewModel.selectTime(hours, minutes)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun acceptPassword(passwordField: Field) {
|
|
||||||
mEntryEditViewModel.selectPassword(passwordField)
|
|
||||||
entryEditActivityEducation?.let {
|
|
||||||
Handler(Looper.getMainLooper()).post { performedNextEducation(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancelPassword(passwordField: Field) {
|
|
||||||
// Do nothing here
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
onApprovedBackPressed {
|
onApprovedBackPressed {
|
||||||
super@EntryEditActivity.onBackPressed()
|
super@EntryEditActivity.onBackPressed()
|
||||||
@@ -686,7 +707,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
val intentEntry = Intent()
|
val intentEntry = Intent()
|
||||||
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
|
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
|
||||||
intentEntry.putExtras(bundle)
|
intentEntry.putExtras(bundle)
|
||||||
setResult(ADD_OR_UPDATE_ENTRY_RESULT_CODE, intentEntry)
|
setResult(Activity.RESULT_OK, intentEntry)
|
||||||
super.finish()
|
super.finish()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Exception when parcelable can't be done
|
// Exception when parcelable can't be done
|
||||||
@@ -701,23 +722,46 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
// Keys for current Activity
|
// Keys for current Activity
|
||||||
const val KEY_ENTRY = "entry"
|
const val KEY_ENTRY = "entry"
|
||||||
const val KEY_PARENT = "parent"
|
const val KEY_PARENT = "parent"
|
||||||
|
|
||||||
// Keys for callback
|
|
||||||
const val ADD_OR_UPDATE_ENTRY_RESULT_CODE = 31
|
|
||||||
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"
|
||||||
|
|
||||||
|
fun registerForEntryResult(fragment: Fragment,
|
||||||
|
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
|
||||||
|
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
entryAddedOrUpdatedListener.invoke(
|
||||||
|
result.data?.getParcelableExtra(ADD_OR_UPDATE_ENTRY_KEY)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
entryAddedOrUpdatedListener.invoke(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registerForEntryResult(activity: FragmentActivity,
|
||||||
|
entryAddedOrUpdatedListener: (NodeId<UUID>?) -> Unit): ActivityResultLauncher<Intent> {
|
||||||
|
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
entryAddedOrUpdatedListener.invoke(
|
||||||
|
result.data?.getParcelableExtra(ADD_OR_UPDATE_ENTRY_KEY)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
entryAddedOrUpdatedListener.invoke(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch EntryEditActivity to update an existing entry by his [entryId]
|
* Launch EntryEditActivity to update an existing entry by his [entryId]
|
||||||
*/
|
*/
|
||||||
fun launchToUpdate(activity: Activity,
|
fun launchToUpdate(activity: Activity,
|
||||||
database: Database,
|
database: Database,
|
||||||
entryId: NodeId<UUID>) {
|
entryId: NodeId<UUID>,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
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, entryId)
|
intent.putExtra(KEY_ENTRY, entryId)
|
||||||
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
activityResultLauncher.launch(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -727,12 +771,13 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
*/
|
*/
|
||||||
fun launchToCreate(activity: Activity,
|
fun launchToCreate(activity: Activity,
|
||||||
database: Database,
|
database: Database,
|
||||||
groupId: NodeId<*>) {
|
groupId: NodeId<*>,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
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, groupId)
|
intent.putExtra(KEY_PARENT, groupId)
|
||||||
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
activityResultLauncher.launch(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -795,8 +840,9 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
* Launch EntryEditActivity to add a new entry in autofill selection
|
* Launch EntryEditActivity to add a new entry in autofill selection
|
||||||
*/
|
*/
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
fun launchForAutofillResult(activity: Activity,
|
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||||
database: Database,
|
database: Database,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
autofillComponent: AutofillComponent,
|
autofillComponent: AutofillComponent,
|
||||||
groupId: NodeId<*>,
|
groupId: NodeId<*>,
|
||||||
searchInfo: SearchInfo? = null) {
|
searchInfo: SearchInfo? = null) {
|
||||||
@@ -807,6 +853,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
AutofillHelper.startActivityForAutofillResult(
|
AutofillHelper.startActivityForAutofillResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
|
activityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo
|
searchInfo
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,20 +19,18 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.app.Activity
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity to search or select entry in database,
|
* Activity to search or select entry in database,
|
||||||
@@ -45,36 +43,61 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun finishActivityIfReloadRequested(): Boolean {
|
override fun finishActivityIfReloadRequested(): Boolean {
|
||||||
return true
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
var sharedWebDomain: String? = null
|
|
||||||
var otpString: String? = null
|
|
||||||
|
|
||||||
when (intent?.action) {
|
val keySelectionBundle = intent.getBundleExtra(KEY_SELECTION_BUNDLE)
|
||||||
Intent.ACTION_SEND -> {
|
if (keySelectionBundle != null) {
|
||||||
if ("text/plain" == intent.type) {
|
// To manage package name
|
||||||
// Retrieve web domain or OTP
|
var searchInfo = SearchInfo()
|
||||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
|
keySelectionBundle.getParcelable<SearchInfo>(KEY_SEARCH_INFO)?.let { mSearchInfo ->
|
||||||
|
searchInfo = mSearchInfo
|
||||||
|
}
|
||||||
|
launch(database, searchInfo)
|
||||||
|
} else {
|
||||||
|
// To manage share
|
||||||
|
var sharedWebDomain: String? = null
|
||||||
|
var otpString: String? = null
|
||||||
|
|
||||||
|
when (intent?.action) {
|
||||||
|
Intent.ACTION_SEND -> {
|
||||||
|
if ("text/plain" == intent.type) {
|
||||||
|
// Retrieve web domain or OTP
|
||||||
|
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
|
||||||
|
if (OtpEntryFields.isOTPUri(extra))
|
||||||
|
otpString = extra
|
||||||
|
else
|
||||||
|
sharedWebDomain = Uri.parse(extra).host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launchSelection(database, sharedWebDomain, otpString)
|
||||||
|
}
|
||||||
|
Intent.ACTION_VIEW -> {
|
||||||
|
// Retrieve OTP
|
||||||
|
intent.dataString?.let { extra ->
|
||||||
if (OtpEntryFields.isOTPUri(extra))
|
if (OtpEntryFields.isOTPUri(extra))
|
||||||
otpString = extra
|
otpString = extra
|
||||||
else
|
}
|
||||||
sharedWebDomain = Uri.parse(extra).host
|
launchSelection(database, sharedWebDomain, otpString)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
if (database != null) {
|
||||||
|
GroupActivity.launch(this, database)
|
||||||
|
} else {
|
||||||
|
FileDatabaseSelectActivity.launch(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Intent.ACTION_VIEW -> {
|
|
||||||
// Retrieve OTP
|
|
||||||
intent.dataString?.let { extra ->
|
|
||||||
if (OtpEntryFields.isOTPUri(extra))
|
|
||||||
otpString = extra
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
}
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchSelection(database: Database?,
|
||||||
|
sharedWebDomain: String?,
|
||||||
|
otpString: String?) {
|
||||||
// Build domain search param
|
// Build domain search param
|
||||||
val searchInfo = SearchInfo().apply {
|
val searchInfo = SearchInfo().apply {
|
||||||
this.webDomain = sharedWebDomain
|
this.webDomain = sharedWebDomain
|
||||||
@@ -90,111 +113,111 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
private fun launch(database: Database?,
|
private fun launch(database: Database?,
|
||||||
searchInfo: SearchInfo) {
|
searchInfo: SearchInfo) {
|
||||||
|
|
||||||
if (!searchInfo.containsOnlyNullValues()) {
|
// Setting to integrate Magikeyboard
|
||||||
// Setting to integrate Magikeyboard
|
val searchShareForMagikeyboard = MagikeyboardService.activatedInSettings(this)
|
||||||
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
|
|
||||||
|
|
||||||
// If database is open
|
// If database is open
|
||||||
val readOnly = database?.isReadOnly != false
|
val readOnly = database?.isReadOnly != false
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
database,
|
database,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
{ openedDatabase, items ->
|
{ openedDatabase, items ->
|
||||||
// Items found
|
// Items found
|
||||||
if (searchInfo.otpString != null) {
|
if (searchInfo.otpString != null) {
|
||||||
if (!readOnly) {
|
if (!readOnly) {
|
||||||
GroupActivity.launchForSaveResult(
|
GroupActivity.launchForSaveResult(
|
||||||
this,
|
this,
|
||||||
openedDatabase,
|
|
||||||
searchInfo,
|
|
||||||
false)
|
|
||||||
} else {
|
|
||||||
Toast.makeText(applicationContext,
|
|
||||||
R.string.autofill_read_only_save,
|
|
||||||
Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
} else if (searchShareForMagikeyboard) {
|
|
||||||
if (items.size == 1) {
|
|
||||||
// Automatically populate keyboard
|
|
||||||
val entryPopulate = items[0]
|
|
||||||
populateKeyboardAndMoveAppToBackground(
|
|
||||||
this,
|
|
||||||
entryPopulate,
|
|
||||||
intent)
|
|
||||||
} else {
|
|
||||||
// Select the one we want
|
|
||||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
|
||||||
openedDatabase,
|
|
||||||
searchInfo,
|
|
||||||
true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
GroupActivity.launchForSearchResult(this,
|
|
||||||
openedDatabase,
|
|
||||||
searchInfo,
|
|
||||||
true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ openedDatabase ->
|
|
||||||
// Show the database UI to select the entry
|
|
||||||
if (searchInfo.otpString != null) {
|
|
||||||
if (!readOnly) {
|
|
||||||
GroupActivity.launchForSaveResult(this,
|
|
||||||
openedDatabase,
|
|
||||||
searchInfo,
|
|
||||||
false)
|
|
||||||
} else {
|
|
||||||
Toast.makeText(applicationContext,
|
|
||||||
R.string.autofill_read_only_save,
|
|
||||||
Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
} else if (readOnly || searchShareForMagikeyboard) {
|
|
||||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
|
||||||
openedDatabase,
|
openedDatabase,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
false)
|
false)
|
||||||
} else {
|
} else {
|
||||||
|
Toast.makeText(applicationContext,
|
||||||
|
R.string.autofill_read_only_save,
|
||||||
|
Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
} else if (searchShareForMagikeyboard) {
|
||||||
|
MagikeyboardService.performSelection(
|
||||||
|
items,
|
||||||
|
{ entryInfo ->
|
||||||
|
// Automatically populate keyboard
|
||||||
|
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||||
|
this,
|
||||||
|
entryInfo
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ autoSearch ->
|
||||||
|
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||||
|
openedDatabase,
|
||||||
|
searchInfo,
|
||||||
|
autoSearch)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
GroupActivity.launchForSearchResult(this,
|
||||||
|
openedDatabase,
|
||||||
|
searchInfo,
|
||||||
|
true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ openedDatabase ->
|
||||||
|
// Show the database UI to select the entry
|
||||||
|
if (searchInfo.otpString != null) {
|
||||||
|
if (!readOnly) {
|
||||||
GroupActivity.launchForSaveResult(this,
|
GroupActivity.launchForSaveResult(this,
|
||||||
openedDatabase,
|
openedDatabase,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
false)
|
false)
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// If database not open
|
|
||||||
if (searchInfo.otpString != null) {
|
|
||||||
if (!readOnly) {
|
|
||||||
FileDatabaseSelectActivity.launchForSaveResult(this,
|
|
||||||
searchInfo)
|
|
||||||
} else {
|
|
||||||
Toast.makeText(applicationContext,
|
|
||||||
R.string.autofill_read_only_save,
|
|
||||||
Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
} else if (searchShareForMagikeyboard) {
|
|
||||||
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
|
|
||||||
searchInfo)
|
|
||||||
} else {
|
} else {
|
||||||
FileDatabaseSelectActivity.launchForSearchResult(this,
|
Toast.makeText(applicationContext,
|
||||||
searchInfo)
|
R.string.autofill_read_only_save,
|
||||||
|
Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
|
} else if (searchShareForMagikeyboard) {
|
||||||
|
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||||
|
openedDatabase,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
|
} else {
|
||||||
|
GroupActivity.launchForSearchResult(this,
|
||||||
|
openedDatabase,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
|
{
|
||||||
|
// If database not open
|
||||||
|
if (searchInfo.otpString != null) {
|
||||||
|
FileDatabaseSelectActivity.launchForSaveResult(this,
|
||||||
|
searchInfo)
|
||||||
|
} else if (searchShareForMagikeyboard) {
|
||||||
|
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
|
||||||
|
searchInfo)
|
||||||
|
} else {
|
||||||
|
FileDatabaseSelectActivity.launchForSearchResult(this,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
|
||||||
|
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
||||||
|
|
||||||
|
fun launch(context: Context,
|
||||||
|
searchInfo: SearchInfo? = null) {
|
||||||
|
val intent = Intent(context, EntrySelectionLauncherActivity::class.java).apply {
|
||||||
|
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
|
||||||
|
putParcelable(KEY_SEARCH_INFO, searchInfo)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// New task needed because don't launch from an Activity context
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||||
|
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
|
||||||
entry: EntryInfo,
|
|
||||||
intent: Intent,
|
|
||||||
toast: Boolean = true) {
|
|
||||||
// Populate Magikeyboard with entry
|
|
||||||
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
|
||||||
// Consume the selection mode
|
|
||||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
|
||||||
activity.moveTaskToBack(true)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -31,16 +31,19 @@ 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 androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
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.SetMainCredentialDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
@@ -67,15 +70,18 @@ import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
|||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||||
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
SetMainCredentialDialogFragment.AssignMainCredentialDialogListener {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private lateinit var coordinatorLayout: CoordinatorLayout
|
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||||
|
private var specialTitle: View? = null
|
||||||
private var createDatabaseButtonView: View? = null
|
private var createDatabaseButtonView: View? = null
|
||||||
private var openDatabaseButtonView: View? = null
|
private var openDatabaseButtonView: View? = null
|
||||||
|
|
||||||
private val databaseFilesViewModel: DatabaseFilesViewModel by viewModels()
|
private val databaseFilesViewModel: DatabaseFilesViewModel by viewModels()
|
||||||
|
|
||||||
|
private val mFileDatabaseSelectActivityEducation = FileDatabaseSelectActivityEducation(this)
|
||||||
|
|
||||||
// Adapter to manage database history list
|
// Adapter to manage database history list
|
||||||
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
|
private var mAdapterDatabaseHistory: FileDatabaseHistoryAdapter? = null
|
||||||
|
|
||||||
@@ -85,9 +91,20 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
|
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||||
|
AutofillHelper.buildActivityResultLauncher(this)
|
||||||
|
else null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Enabling/disabling MagikeyboardService is normally done by DexModeReceiver, but this
|
||||||
|
// additional check will allow the keyboard to be reenabled more easily if the app crashes
|
||||||
|
// or is force quit within DeX mode and then the user leaves DeX mode. Without this, the
|
||||||
|
// user would need to enter and exit DeX mode once to reenable the service.
|
||||||
|
MagikeyboardUtil.setEnabled(this, !DexUtil.isDexMode(resources.configuration))
|
||||||
|
|
||||||
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
|
mFileDatabaseHistoryAction = FileDatabaseHistoryAction.getInstance(applicationContext)
|
||||||
|
|
||||||
setContentView(R.layout.activity_file_selection)
|
setContentView(R.layout.activity_file_selection)
|
||||||
@@ -97,13 +114,32 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
toolbar.title = ""
|
toolbar.title = ""
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
|
|
||||||
|
// Special title
|
||||||
|
specialTitle = findViewById(R.id.file_selection_title_part_3)
|
||||||
|
|
||||||
// Create database button
|
// Create database button
|
||||||
createDatabaseButtonView = findViewById(R.id.create_database_button)
|
createDatabaseButtonView = findViewById(R.id.create_database_button)
|
||||||
createDatabaseButtonView?.setOnClickListener { createNewFile() }
|
createDatabaseButtonView?.setOnClickListener { createNewFile() }
|
||||||
|
|
||||||
// Open database button
|
// Open database button
|
||||||
mExternalFileHelper = ExternalFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
|
uri?.let {
|
||||||
|
launchPasswordActivityWithPath(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mExternalFileHelper?.buildCreateDocument("application/x-keepass") { databaseFileCreatedUri ->
|
||||||
|
mDatabaseFileUri = databaseFileCreatedUri
|
||||||
|
if (mDatabaseFileUri != null) {
|
||||||
|
SetMainCredentialDialogFragment.getInstance(true)
|
||||||
|
.show(supportFragmentManager, "passwordDialog")
|
||||||
|
} else {
|
||||||
|
val error = getString(R.string.error_create_database)
|
||||||
|
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
Log.e(TAG, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
openDatabaseButtonView = findViewById(R.id.open_database_button)
|
||||||
openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper)
|
openDatabaseButtonView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
|
|
||||||
// History list
|
// History list
|
||||||
@@ -250,8 +286,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
* Create a new file by calling the content provider
|
* Create a new file by calling the content provider
|
||||||
*/
|
*/
|
||||||
private fun createNewFile() {
|
private fun createNewFile() {
|
||||||
mExternalFileHelper?.createDocument( getString(R.string.database_file_name_default) +
|
mExternalFileHelper?.createDocument(
|
||||||
getString(R.string.database_file_extension_default), "application/x-keepass")
|
getString(R.string.database_file_name_default) +
|
||||||
|
getString(R.string.database_file_extension_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fileNoFoundAction(e: FileNotFoundException) {
|
private fun fileNoFoundAction(e: FileNotFoundException) {
|
||||||
@@ -261,14 +298,15 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
||||||
PasswordActivity.launch(this,
|
MainCredentialActivity.launch(this,
|
||||||
databaseUri,
|
databaseUri,
|
||||||
keyFile,
|
keyFile,
|
||||||
{ exception ->
|
{ exception ->
|
||||||
fileNoFoundAction(exception)
|
fileNoFoundAction(exception)
|
||||||
},
|
},
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() })
|
{ onLaunchActivitySpecialMode() },
|
||||||
|
mAutofillActivityResultLauncher)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchGroupActivityIfLoaded(database: Database) {
|
private fun launchGroupActivityIfLoaded(database: Database) {
|
||||||
@@ -277,20 +315,11 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
database,
|
database,
|
||||||
{ onValidateSpecialMode() },
|
{ onValidateSpecialMode() },
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() })
|
{ onLaunchActivitySpecialMode() },
|
||||||
|
mAutofillActivityResultLauncher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onValidateSpecialMode() {
|
|
||||||
super.onValidateSpecialMode()
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCancelSpecialMode() {
|
|
||||||
super.onCancelSpecialMode()
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
||||||
launchPasswordActivity(databaseUri, null)
|
launchPasswordActivity(databaseUri, null)
|
||||||
// Delete flickering for kitkat <=
|
// Delete flickering for kitkat <=
|
||||||
@@ -301,6 +330,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
|
// Define special title
|
||||||
|
specialTitle?.isVisible = UriUtil.contributingUser(this)
|
||||||
|
|
||||||
// Show open and create button or special mode
|
// Show open and create button or special mode
|
||||||
when (mSpecialMode) {
|
when (mSpecialMode) {
|
||||||
SpecialMode.DEFAULT -> {
|
SpecialMode.DEFAULT -> {
|
||||||
@@ -353,73 +385,47 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
|
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
|
||||||
if (uri != null) {
|
|
||||||
launchPasswordActivityWithPath(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the created URI from the file manager
|
|
||||||
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { databaseFileCreatedUri ->
|
|
||||||
mDatabaseFileUri = databaseFileCreatedUri
|
|
||||||
if (mDatabaseFileUri != null) {
|
|
||||||
AssignMasterKeyDialogFragment.getInstance(true)
|
|
||||||
.show(supportFragmentManager, "passwordDialog")
|
|
||||||
} else {
|
|
||||||
val error = getString(R.string.error_create_database)
|
|
||||||
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
|
||||||
Log.e(TAG, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
|
|
||||||
if (mSpecialMode == SpecialMode.DEFAULT) {
|
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||||
MenuUtil.defaultMenuInflater(menuInflater, menu)
|
MenuUtil.defaultMenuInflater(this, menuInflater, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
Handler(Looper.getMainLooper()).post { performedNextEducation(FileDatabaseSelectActivityEducation(this)) }
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
performedNextEducation()
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performedNextEducation(fileDatabaseSelectActivityEducation: FileDatabaseSelectActivityEducation) {
|
private fun performedNextEducation() {
|
||||||
// If no recent files
|
// If no recent files
|
||||||
val createDatabaseEducationPerformed =
|
val createDatabaseEducationPerformed =
|
||||||
createDatabaseButtonView != null
|
createDatabaseButtonView != null
|
||||||
&& createDatabaseButtonView!!.visibility == View.VISIBLE
|
&& createDatabaseButtonView!!.visibility == View.VISIBLE
|
||||||
&& mAdapterDatabaseHistory != null
|
&& mFileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
||||||
&& mAdapterDatabaseHistory!!.itemCount == 0
|
|
||||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
|
||||||
createDatabaseButtonView!!,
|
createDatabaseButtonView!!,
|
||||||
{
|
{
|
||||||
createNewFile()
|
createNewFile()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// But if the user cancel, it can also select a database
|
// But if the user cancel, it can also select a database
|
||||||
performedNextEducation(fileDatabaseSelectActivityEducation)
|
performedNextEducation()
|
||||||
})
|
})
|
||||||
if (!createDatabaseEducationPerformed) {
|
if (!createDatabaseEducationPerformed) {
|
||||||
// selectDatabaseEducationPerformed
|
// selectDatabaseEducationPerformed
|
||||||
openDatabaseButtonView != null
|
openDatabaseButtonView != null
|
||||||
&& fileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
&& mFileDatabaseSelectActivityEducation.checkAndPerformedSelectDatabaseEducation(
|
||||||
openDatabaseButtonView!!,
|
openDatabaseButtonView!!,
|
||||||
{ tapTargetView ->
|
{ tapTargetView ->
|
||||||
tapTargetView?.let {
|
tapTargetView?.let {
|
||||||
mExternalFileHelper?.openDocument()
|
mExternalFileHelper?.openDocument()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{}
|
{
|
||||||
)
|
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,11 +499,13 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
fun launchForAutofillResult(activity: Activity,
|
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
autofillComponent: AutofillComponent,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo? = null) {
|
searchInfo: SearchInfo? = null) {
|
||||||
AutofillHelper.startActivityForAutofillResult(activity,
|
AutofillHelper.startActivityForAutofillResult(activity,
|
||||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||||
|
activityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,12 +27,16 @@ import android.view.Menu
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.IconEditDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
|
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
@@ -81,6 +85,9 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
|
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
|
||||||
|
|
||||||
mExternalFileHelper = ExternalFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
|
addCustomIcon(uri)
|
||||||
|
}
|
||||||
|
|
||||||
uploadButton = findViewById(R.id.icon_picker_upload)
|
uploadButton = findViewById(R.id.icon_picker_upload)
|
||||||
|
|
||||||
@@ -139,6 +146,16 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
uploadButton.isEnabled = true
|
uploadButton.isEnabled = true
|
||||||
}
|
}
|
||||||
|
iconPickerViewModel.customIconUpdated.observe(this) { iconCustomUpdated ->
|
||||||
|
if (iconCustomUpdated.error && !iconCustomUpdated.errorConsumed) {
|
||||||
|
Snackbar.make(coordinatorLayout, iconCustomUpdated.errorStringId, Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
iconCustomUpdated.errorConsumed = true
|
||||||
|
}
|
||||||
|
iconCustomUpdated.iconCustom?.let {
|
||||||
|
mDatabase?.updateCustomIcon(it)
|
||||||
|
}
|
||||||
|
iconPickerViewModel.deselectAllCustomIcons()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun viewToInvalidateTimeout(): View? {
|
override fun viewToInvalidateTimeout(): View? {
|
||||||
@@ -197,6 +214,10 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||||
|
menu?.findItem(R.id.menu_edit)?.apply {
|
||||||
|
isEnabled = mIconsSelected.size == 1
|
||||||
|
isVisible = isEnabled
|
||||||
|
}
|
||||||
menu?.findItem(R.id.menu_delete)?.apply {
|
menu?.findItem(R.id.menu_delete)?.apply {
|
||||||
isEnabled = mCustomIconsSelectionMode
|
isEnabled = mCustomIconsSelectionMode
|
||||||
isVisible = isEnabled
|
isVisible = isEnabled
|
||||||
@@ -213,6 +234,9 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
onBackPressed()
|
onBackPressed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
R.id.menu_edit -> {
|
||||||
|
updateCustomIcon(mIconsSelected[0])
|
||||||
|
}
|
||||||
R.id.menu_delete -> {
|
R.id.menu_delete -> {
|
||||||
mIconsSelected.forEach { iconToRemove ->
|
mIconsSelected.forEach { iconToRemove ->
|
||||||
removeCustomIcon(iconToRemove)
|
removeCustomIcon(iconToRemove)
|
||||||
@@ -277,6 +301,11 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateCustomIcon(iconImageCustom: IconImageCustom) {
|
||||||
|
IconEditDialogFragment.update(iconImageCustom)
|
||||||
|
.show(supportFragmentManager, IconEditDialogFragment.TAG_UPDATE_ICON)
|
||||||
|
}
|
||||||
|
|
||||||
private fun removeCustomIcon(iconImageCustom: IconImageCustom) {
|
private fun removeCustomIcon(iconImageCustom: IconImageCustom) {
|
||||||
uploadButton.isEnabled = false
|
uploadButton.isEnabled = false
|
||||||
iconPickerViewModel.deselectAllCustomIcons()
|
iconPickerViewModel.deselectAllCustomIcons()
|
||||||
@@ -286,14 +315,6 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
|
|
||||||
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
|
||||||
addCustomIcon(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setResult() {
|
private fun setResult() {
|
||||||
setResult(Activity.RESULT_OK, Intent().apply {
|
setResult(Activity.RESULT_OK, Intent().apply {
|
||||||
putExtra(EXTRA_ICON, mIconImage)
|
putExtra(EXTRA_ICON, mIconImage)
|
||||||
@@ -308,30 +329,28 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val ICON_PICKER_FRAGMENT_TAG = "ICON_PICKER_FRAGMENT_TAG"
|
private const val ICON_PICKER_FRAGMENT_TAG = "ICON_PICKER_FRAGMENT_TAG"
|
||||||
|
|
||||||
private const val ICON_SELECTED_REQUEST = 15861
|
|
||||||
private const val EXTRA_ICON = "EXTRA_ICON"
|
private const val EXTRA_ICON = "EXTRA_ICON"
|
||||||
|
|
||||||
private const val MAX_ICON_SIZE = 5242880
|
private const val MAX_ICON_SIZE = 5242880
|
||||||
|
|
||||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?, listener: (icon: IconImage) -> Unit) {
|
fun registerIconSelectionForResult(context: FragmentActivity,
|
||||||
if (requestCode == ICON_SELECTED_REQUEST) {
|
listener: (icon: IconImage) -> Unit): ActivityResultLauncher<Intent> {
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
return context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
listener.invoke(data?.getParcelableExtra(EXTRA_ICON) ?: IconImage())
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
listener.invoke(result.data?.getParcelableExtra(EXTRA_ICON) ?: IconImage())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launch(context: Activity,
|
fun launch(context: FragmentActivity,
|
||||||
previousIcon: IconImage?) {
|
previousIcon: IconImage?,
|
||||||
|
resultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
// Create an instance to return the picker icon
|
// Create an instance to return the picker icon
|
||||||
context.startActivityForResult(
|
resultLauncher.launch(
|
||||||
Intent(context,
|
Intent(context, IconPickerActivity::class.java).apply {
|
||||||
IconPickerActivity::class.java).apply {
|
|
||||||
if (previousIcon != null)
|
if (previousIcon != null)
|
||||||
putExtra(EXTRA_ICON, previousIcon)
|
putExtra(EXTRA_ICON, previousIcon)
|
||||||
},
|
}
|
||||||
ICON_SELECTED_REQUEST)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.fragment.app.commit
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.fragments.KeyGeneratorFragment
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
||||||
|
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
||||||
|
|
||||||
|
class KeyGeneratorActivity : DatabaseLockActivity() {
|
||||||
|
|
||||||
|
private lateinit var toolbar: Toolbar
|
||||||
|
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||||
|
private lateinit var validationButton: View
|
||||||
|
private var lockView: View? = null
|
||||||
|
|
||||||
|
private val keyGeneratorViewModel: KeyGeneratorViewModel by viewModels()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_key_generator)
|
||||||
|
|
||||||
|
toolbar = findViewById(R.id.toolbar)
|
||||||
|
toolbar.title = " "
|
||||||
|
setSupportActionBar(toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
|
coordinatorLayout = findViewById(R.id.key_generator_coordinator)
|
||||||
|
|
||||||
|
lockView = findViewById(R.id.lock_button)
|
||||||
|
lockView?.setOnClickListener {
|
||||||
|
lockAndExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
validationButton = findViewById(R.id.key_generator_validation)
|
||||||
|
validationButton.setOnClickListener {
|
||||||
|
keyGeneratorViewModel.validateKeyGenerated()
|
||||||
|
}
|
||||||
|
|
||||||
|
supportFragmentManager.commit {
|
||||||
|
replace(R.id.key_generator_fragment, KeyGeneratorFragment.getInstance(
|
||||||
|
// Default selection tab
|
||||||
|
KeyGeneratorFragment.KeyGeneratorTab.PASSWORD
|
||||||
|
), KEY_GENERATED_FRAGMENT_TAG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyGeneratorViewModel.keyGenerated.observe(this) { keyGenerated ->
|
||||||
|
setResult(Activity.RESULT_OK, Intent().apply {
|
||||||
|
putExtra(KEY_GENERATED, keyGenerated)
|
||||||
|
})
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun viewToInvalidateTimeout(): View? {
|
||||||
|
return findViewById<ViewGroup>(R.id.key_generator_container)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// Show the lock button
|
||||||
|
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Padding if lock button visible
|
||||||
|
toolbar.updateLockPaddingLeft()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
|
super.onCreateOptionsMenu(menu)
|
||||||
|
menuInflater.inflate(R.menu.key_generator, menu)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> {
|
||||||
|
onBackPressed()
|
||||||
|
}
|
||||||
|
R.id.menu_generate -> {
|
||||||
|
keyGeneratorViewModel.requireKeyGeneration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
setResult(Activity.RESULT_CANCELED, Intent())
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val KEY_GENERATED = "KEY_GENERATED"
|
||||||
|
private const val KEY_GENERATED_FRAGMENT_TAG = "KEY_GENERATED_FRAGMENT_TAG"
|
||||||
|
|
||||||
|
fun registerForGeneratedKeyResult(activity: FragmentActivity,
|
||||||
|
keyGeneratedListener: (String?) -> Unit): ActivityResultLauncher<Intent> {
|
||||||
|
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
keyGeneratedListener.invoke(
|
||||||
|
result.data?.getStringExtra(KEY_GENERATED)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
keyGeneratedListener.invoke(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launch(context: FragmentActivity,
|
||||||
|
resultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
|
// Create an instance to return the picker icon
|
||||||
|
resultLauncher.launch(
|
||||||
|
Intent(context, KeyGeneratorActivity::class.java)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
|
||||||
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 : DatabaseModeActivity() {
|
|
||||||
|
|
||||||
override fun applyCustomStyle(): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun finishActivityIfReloadRequested(): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
|
||||||
super.onDatabaseRetrieved(database)
|
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
|
||||||
database,
|
|
||||||
null,
|
|
||||||
{ _, _ ->
|
|
||||||
// Not called
|
|
||||||
// if items found directly returns before calling this activity
|
|
||||||
},
|
|
||||||
{ openedDatabase ->
|
|
||||||
// Select if not found
|
|
||||||
GroupActivity.launchForKeyboardSelectionResult(this, openedDatabase)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Pass extra to get entry
|
|
||||||
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,102 +21,98 @@ package com.kunzisoft.keepass.activities
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
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.os.Looper
|
import android.os.Looper
|
||||||
import android.text.Editable
|
|
||||||
import android.text.TextWatcher
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.*
|
import android.view.Menu
|
||||||
import android.view.KeyEvent.KEYCODE_ENTER
|
import android.view.MenuItem
|
||||||
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
import android.view.View
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.ViewGroup
|
||||||
import android.widget.*
|
import android.widget.Button
|
||||||
import android.widget.TextView.OnEditorActionListener
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||||
import com.kunzisoft.keepass.activities.helpers.*
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
|
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
|
||||||
|
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.model.*
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
import com.kunzisoft.keepass.view.MainCredentialView
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
|
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
|
|
||||||
open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
private var filenameView: TextView? = null
|
private var filenameView: TextView? = null
|
||||||
private var passwordView: EditText? = null
|
private var advancedUnlockButton: View? = null
|
||||||
private var keyFileSelectionView: KeyFileSelectionView? = null
|
private var mainCredentialView: MainCredentialView? = null
|
||||||
private var confirmButtonView: Button? = null
|
private var confirmButtonView: Button? = null
|
||||||
private var checkboxPasswordView: CompoundButton? = null
|
|
||||||
private var checkboxKeyFileView: CompoundButton? = null
|
|
||||||
private var infoContainerView: ViewGroup? = null
|
private var infoContainerView: ViewGroup? = null
|
||||||
private lateinit var coordinatorLayout: CoordinatorLayout
|
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||||
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
|
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
|
||||||
|
|
||||||
private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
|
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||||
|
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by viewModels()
|
||||||
|
|
||||||
|
private val mPasswordActivityEducation = PasswordActivityEducation(this)
|
||||||
|
|
||||||
private var mDefaultDatabase: Boolean = false
|
private var mDefaultDatabase: Boolean = false
|
||||||
private var mDatabaseFileUri: Uri? = null
|
private var mDatabaseFileUri: Uri? = null
|
||||||
private var mDatabaseKeyFileUri: Uri? = null
|
|
||||||
|
|
||||||
private var mRememberKeyFile: Boolean = false
|
private var mRememberKeyFile: Boolean = false
|
||||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var mPermissionAsked = false
|
|
||||||
private var mReadOnly: Boolean = false
|
private var mReadOnly: Boolean = false
|
||||||
private var mForceReadOnly: Boolean = false
|
private var mForceReadOnly: Boolean = false
|
||||||
set(value) {
|
|
||||||
infoContainerView?.visibility = if (value) {
|
|
||||||
mReadOnly = true
|
|
||||||
View.VISIBLE
|
|
||||||
} else {
|
|
||||||
View.GONE
|
|
||||||
}
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
private var mAllowAutoOpenBiometricPrompt: Boolean = true
|
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||||
|
AutofillHelper.buildActivityResultLauncher(this)
|
||||||
|
else null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
setContentView(R.layout.activity_password)
|
setContentView(R.layout.activity_main_credential)
|
||||||
|
|
||||||
toolbar = findViewById(R.id.toolbar)
|
toolbar = findViewById(R.id.toolbar)
|
||||||
toolbar?.title = getString(R.string.app_name)
|
toolbar?.title = getString(R.string.app_name)
|
||||||
@@ -124,16 +120,13 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
|
||||||
filenameView = findViewById(R.id.filename)
|
filenameView = findViewById(R.id.filename)
|
||||||
passwordView = findViewById(R.id.password)
|
advancedUnlockButton = findViewById(R.id.activity_password_advanced_unlock_button)
|
||||||
keyFileSelectionView = findViewById(R.id.keyfile_selection)
|
mainCredentialView = findViewById(R.id.activity_password_credentials)
|
||||||
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
||||||
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
|
||||||
infoContainerView = findViewById(R.id.activity_password_info_container)
|
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||||
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
|
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
|
||||||
|
|
||||||
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
|
||||||
mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) {
|
mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) {
|
||||||
savedInstanceState.getBoolean(KEY_READ_ONLY)
|
savedInstanceState.getBoolean(KEY_READ_ONLY)
|
||||||
} else {
|
} else {
|
||||||
@@ -141,40 +134,26 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
}
|
}
|
||||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||||
|
|
||||||
mExternalFileHelper = ExternalFileHelper(this@PasswordActivity)
|
mExternalFileHelper = ExternalFileHelper(this@MainCredentialActivity)
|
||||||
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
|
if (uri != null) {
|
||||||
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
mainCredentialView?.populateKeyFileTextView(uri)
|
||||||
passwordView?.addTextChangedListener(object : TextWatcher {
|
|
||||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
|
||||||
|
|
||||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
|
||||||
|
|
||||||
override fun afterTextChanged(editable: Editable) {
|
|
||||||
if (editable.toString().isNotEmpty() && checkboxPasswordView?.isChecked != true)
|
|
||||||
checkboxPasswordView?.isChecked = true
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
passwordView?.setOnKeyListener { _, _, keyEvent ->
|
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
|
||||||
var handled = false
|
mainCredentialView?.onValidateListener = {
|
||||||
if (keyEvent.action == KeyEvent.ACTION_DOWN
|
loadDatabase()
|
||||||
&& keyEvent?.keyCode == KEYCODE_ENTER) {
|
|
||||||
verifyCheckboxesAndLoadDatabase()
|
|
||||||
handled = true
|
|
||||||
}
|
|
||||||
handled
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If is a view intent
|
// If is a view intent
|
||||||
getUriFromIntent(intent)
|
getUriFromIntent(intent)
|
||||||
if (savedInstanceState?.containsKey(KEY_KEYFILE) == true) {
|
|
||||||
mDatabaseKeyFileUri = UriUtil.parse(savedInstanceState.getString(KEY_KEYFILE))
|
|
||||||
}
|
|
||||||
if (savedInstanceState?.containsKey(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT) == true) {
|
|
||||||
mAllowAutoOpenBiometricPrompt = savedInstanceState.getBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init Biometric elements
|
// Init Biometric elements
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
advancedUnlockButton?.setOnClickListener {
|
||||||
|
startActivity(Intent(this, SettingsAdvancedUnlockActivity::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
advancedUnlockFragment = supportFragmentManager
|
advancedUnlockFragment = supportFragmentManager
|
||||||
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
|
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
|
||||||
if (advancedUnlockFragment == null) {
|
if (advancedUnlockFragment == null) {
|
||||||
@@ -187,31 +166,42 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Listen password checkbox to init advanced unlock and confirmation button
|
// Listen password checkbox to init advanced unlock and confirmation button
|
||||||
checkboxPasswordView?.setOnCheckedChangeListener { _, _ ->
|
mainCredentialView?.onPasswordChecked =
|
||||||
advancedUnlockFragment?.checkUnlockAvailability()
|
CompoundButton.OnCheckedChangeListener { _, _ ->
|
||||||
enableOrNotTheConfirmationButton()
|
mAdvancedUnlockViewModel.checkUnlockAvailability()
|
||||||
}
|
enableConfirmationButton()
|
||||||
|
}
|
||||||
|
|
||||||
// Observe if default database
|
// Observe if default database
|
||||||
databaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
||||||
mDefaultDatabase = isDefaultDatabase
|
mDefaultDatabase = isDefaultDatabase
|
||||||
}
|
}
|
||||||
|
|
||||||
// Observe database file change
|
// Observe database file change
|
||||||
databaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
|
mDatabaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
|
||||||
|
|
||||||
// Force read only if the file does not exists
|
// Force read only if the file does not exists
|
||||||
mForceReadOnly = databaseFile?.let {
|
val databaseFileNotExists = databaseFile?.let {
|
||||||
!it.databaseFileExists
|
!it.databaseFileExists
|
||||||
} ?: true
|
} ?: true
|
||||||
|
infoContainerView?.visibility = if (databaseFileNotExists) {
|
||||||
|
mReadOnly = true
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
mForceReadOnly = databaseFileNotExists
|
||||||
|
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
|
|
||||||
// Post init uri with KeyFile only if needed
|
// Post init uri with KeyFile only if needed
|
||||||
|
val databaseKeyFileUri = mainCredentialView?.getMainCredential()?.keyFileUri
|
||||||
val keyFileUri =
|
val keyFileUri =
|
||||||
if (mRememberKeyFile
|
if (mRememberKeyFile
|
||||||
&& (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) {
|
&& (databaseKeyFileUri == null || databaseKeyFileUri.toString().isEmpty())) {
|
||||||
databaseFile?.keyFileUri
|
databaseFile?.keyFileUri
|
||||||
} else {
|
} else {
|
||||||
mDatabaseKeyFileUri
|
databaseKeyFileUri
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define title
|
// Define title
|
||||||
@@ -224,23 +214,21 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@PasswordActivity)
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity)
|
||||||
|
|
||||||
// Back to previous keyboard is setting activated
|
// Back to previous keyboard is setting activated
|
||||||
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@PasswordActivity)) {
|
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@MainCredentialActivity)) {
|
||||||
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't allow auto open prompt if lock become when UI visible
|
// Don't allow auto open prompt if lock become when UI visible
|
||||||
mAllowAutoOpenBiometricPrompt = if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true)
|
if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true) {
|
||||||
false
|
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
|
||||||
else
|
|
||||||
mAllowAutoOpenBiometricPrompt
|
|
||||||
mDatabaseFileUri?.let { databaseFileUri ->
|
|
||||||
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
checkPermission()
|
mDatabaseFileUri?.let { databaseFileUri ->
|
||||||
|
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||||
|
}
|
||||||
|
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
launchGroupActivityIfLoaded(database)
|
launchGroupActivityIfLoaded(database)
|
||||||
@@ -250,6 +238,15 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
if (database != null) {
|
if (database != null) {
|
||||||
|
// Trying to load another database
|
||||||
|
if (mDatabaseFileUri != null
|
||||||
|
&& database.fileUri != null
|
||||||
|
&& mDatabaseFileUri != database.fileUri) {
|
||||||
|
Toast.makeText(this,
|
||||||
|
R.string.warning_database_already_opened,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
launchGroupActivityIfLoaded(database)
|
launchGroupActivityIfLoaded(database)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,12 +260,12 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
// Recheck advanced unlock if error
|
// Recheck advanced unlock if error
|
||||||
advancedUnlockFragment?.initAdvancedUnlockMode()
|
mAdvancedUnlockViewModel.initAdvancedUnlockMode()
|
||||||
|
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
launchGroupActivityIfLoaded(database)
|
launchGroupActivityIfLoaded(database)
|
||||||
} else {
|
} else {
|
||||||
passwordView?.requestFocusFromTouch()
|
mainCredentialView?.requestPasswordFocus()
|
||||||
|
|
||||||
var resultError = ""
|
var resultError = ""
|
||||||
val resultException = result.exception
|
val resultException = result.exception
|
||||||
@@ -285,7 +282,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
var databaseUri: Uri? = null
|
var databaseUri: Uri? = null
|
||||||
var mainCredential = MainCredential()
|
var mainCredential = MainCredential()
|
||||||
var readOnly = true
|
var readOnly = true
|
||||||
var cipherEntity: CipherDatabaseEntity? = null
|
var cipherEncryptDatabase: CipherEncryptDatabase? = null
|
||||||
|
|
||||||
result.data?.let { resultData ->
|
result.data?.let { resultData ->
|
||||||
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
||||||
@@ -293,8 +290,8 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
resultData.getParcelable(MAIN_CREDENTIAL_KEY)
|
resultData.getParcelable(MAIN_CREDENTIAL_KEY)
|
||||||
?: mainCredential
|
?: mainCredential
|
||||||
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
||||||
cipherEntity =
|
cipherEncryptDatabase =
|
||||||
resultData.getParcelable(CIPHER_ENTITY_KEY)
|
resultData.getParcelable(CIPHER_DATABASE_KEY)
|
||||||
}
|
}
|
||||||
|
|
||||||
databaseUri?.let { databaseFileUri ->
|
databaseUri?.let { databaseFileUri ->
|
||||||
@@ -302,7 +299,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
databaseFileUri,
|
databaseFileUri,
|
||||||
mainCredential,
|
mainCredential,
|
||||||
readOnly,
|
readOnly,
|
||||||
cipherEntity,
|
cipherEncryptDatabase,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -311,7 +308,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
is FileNotFoundDatabaseException -> {
|
is FileNotFoundDatabaseException -> {
|
||||||
// Remove this default database inaccessible
|
// Remove this default database inaccessible
|
||||||
if (mDefaultDatabase) {
|
if (mDefaultDatabase) {
|
||||||
databaseFileViewModel.removeDefaultDatabase()
|
mDatabaseFileViewModel.removeDefaultDatabase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -338,13 +335,18 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
if (action != null
|
if (action != null
|
||||||
&& action == VIEW_INTENT) {
|
&& action == VIEW_INTENT) {
|
||||||
mDatabaseFileUri = intent.data
|
mDatabaseFileUri = intent.data
|
||||||
mDatabaseKeyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
|
mainCredentialView?.populateKeyFileTextView(UriUtil.getUriFromIntent(intent, KEY_KEYFILE))
|
||||||
} else {
|
} else {
|
||||||
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
|
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
|
||||||
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
|
intent?.getParcelableExtra<Uri?>(KEY_KEYFILE)?.let {
|
||||||
|
mainCredentialView?.populateKeyFileTextView(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
intent?.removeExtra(KEY_KEYFILE)
|
||||||
|
} catch (e: Exception) {}
|
||||||
mDatabaseFileUri?.let {
|
mDatabaseFileUri?.let {
|
||||||
databaseFileViewModel.checkIfIsDefaultDatabase(it)
|
mDatabaseFileViewModel.checkIfIsDefaultDatabase(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,66 +363,74 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
database,
|
database,
|
||||||
{ onValidateSpecialMode() },
|
{ onValidateSpecialMode() },
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() }
|
{ onLaunchActivitySpecialMode() },
|
||||||
|
mAutofillActivityResultLauncher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onValidateSpecialMode() {
|
override fun retrieveCredentialForEncryption(): ByteArray {
|
||||||
super.onValidateSpecialMode()
|
return mainCredentialView?.retrieveCredentialForStorage(credentialStorageListener)
|
||||||
finish()
|
?: byteArrayOf()
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCancelSpecialMode() {
|
|
||||||
super.onCancelSpecialMode()
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun retrieveCredentialForEncryption(): String {
|
|
||||||
return passwordView?.text?.toString() ?: ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun conditionToStoreCredential(): Boolean {
|
override fun conditionToStoreCredential(): Boolean {
|
||||||
return checkboxPasswordView?.isChecked == true
|
return mainCredentialView?.conditionToStoreCredential() == true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCredentialEncrypted(databaseUri: Uri,
|
override fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) {
|
||||||
encryptedCredential: String,
|
|
||||||
ivSpec: String) {
|
|
||||||
// Load the database if password is registered with biometric
|
// Load the database if password is registered with biometric
|
||||||
verifyCheckboxesAndLoadDatabase(
|
loadDatabase(mDatabaseFileUri,
|
||||||
CipherDatabaseEntity(
|
mainCredentialView?.getMainCredential(),
|
||||||
databaseUri.toString(),
|
cipherEncryptDatabase
|
||||||
encryptedCredential,
|
|
||||||
ivSpec)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCredentialDecrypted(databaseUri: Uri,
|
private val credentialStorageListener = object: MainCredentialView.CredentialStorageListener {
|
||||||
decryptedCredential: String) {
|
override fun passwordToStore(password: String?): ByteArray? {
|
||||||
// Load the database if password is retrieve from biometric
|
return password?.toByteArray()
|
||||||
// Retrieve from biometric
|
}
|
||||||
verifyKeyFileCheckboxAndLoadDatabase(decryptedCredential)
|
|
||||||
|
override fun keyfileToStore(keyfile: Uri?): ByteArray? {
|
||||||
|
// TODO create byte array to store keyfile
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hardwareKeyToStore(): ByteArray? {
|
||||||
|
// TODO create byte array to store hardware key
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val onEditorActionListener = object : TextView.OnEditorActionListener {
|
override fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
|
||||||
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
|
// Load the database if password is retrieve from biometric
|
||||||
if (actionId == IME_ACTION_DONE) {
|
// Retrieve from biometric
|
||||||
verifyCheckboxesAndLoadDatabase()
|
val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
|
||||||
return true
|
when (cipherDecryptDatabase.credentialStorage) {
|
||||||
|
CredentialStorage.PASSWORD -> {
|
||||||
|
mainCredential.masterPassword = String(cipherDecryptDatabase.decryptedValue)
|
||||||
|
}
|
||||||
|
CredentialStorage.KEY_FILE -> {
|
||||||
|
// TODO advanced unlock key file
|
||||||
|
}
|
||||||
|
CredentialStorage.HARDWARE_KEY -> {
|
||||||
|
// TODO advanced unlock hardware key
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
loadDatabase(mDatabaseFileUri,
|
||||||
|
mainCredential,
|
||||||
|
null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
||||||
// Define Key File text
|
// Define Key File text
|
||||||
if (mRememberKeyFile) {
|
if (mRememberKeyFile) {
|
||||||
populateKeyFileTextView(keyFileUri)
|
mainCredentialView?.populateKeyFileTextView(keyFileUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define listener for validate button
|
// Define listener for validate button
|
||||||
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
|
confirmButtonView?.setOnClickListener { loadDatabase() }
|
||||||
|
|
||||||
// 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
|
||||||
@@ -429,116 +439,58 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
intent.removeExtra(KEY_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)
|
mainCredentialView?.populatePasswordTextView(password)
|
||||||
}
|
}
|
||||||
if (launchImmediately) {
|
if (launchImmediately) {
|
||||||
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
|
loadDatabase()
|
||||||
} else {
|
} else {
|
||||||
// Init Biometric elements
|
// Init Biometric elements
|
||||||
advancedUnlockFragment?.loadDatabase(databaseFileUri,
|
mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri)
|
||||||
mAllowAutoOpenBiometricPrompt)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enableOrNotTheConfirmationButton()
|
enableConfirmationButton()
|
||||||
|
|
||||||
// Auto select the password field and open keyboard
|
mainCredentialView?.focusPasswordFieldAndOpenKeyboard()
|
||||||
passwordView?.postDelayed({
|
|
||||||
passwordView?.requestFocusFromTouch()
|
|
||||||
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager?
|
|
||||||
inputMethodManager?.showSoftInput(passwordView, InputMethodManager.SHOW_IMPLICIT)
|
|
||||||
}, 100)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun enableOrNotTheConfirmationButton() {
|
private fun enableConfirmationButton() {
|
||||||
// Enable or not the open button if setting is checked
|
// Enable or not the open button if setting is checked
|
||||||
if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) {
|
if (!PreferencesUtil.emptyPasswordAllowed(this@MainCredentialActivity)) {
|
||||||
checkboxPasswordView?.let {
|
confirmButtonView?.isEnabled = mainCredentialView?.isFill() ?: false
|
||||||
confirmButtonView?.isEnabled = (checkboxPasswordView?.isChecked == true
|
|
||||||
|| checkboxKeyFileView?.isChecked == true)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
confirmButtonView?.isEnabled = true
|
confirmButtonView?.isEnabled = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
|
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
|
||||||
populatePasswordTextView(null)
|
mainCredentialView?.populatePasswordTextView(null)
|
||||||
if (clearKeyFile) {
|
if (clearKeyFile) {
|
||||||
mDatabaseKeyFileUri = null
|
mainCredentialView?.populateKeyFileTextView(null)
|
||||||
populateKeyFileTextView(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun populatePasswordTextView(text: String?) {
|
|
||||||
if (text == null || text.isEmpty()) {
|
|
||||||
passwordView?.setText("")
|
|
||||||
if (checkboxPasswordView?.isChecked == true)
|
|
||||||
checkboxPasswordView?.isChecked = false
|
|
||||||
} else {
|
|
||||||
passwordView?.setText(text)
|
|
||||||
if (checkboxPasswordView?.isChecked != true)
|
|
||||||
checkboxPasswordView?.isChecked = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun populateKeyFileTextView(uri: Uri?) {
|
|
||||||
if (uri == null || uri.toString().isEmpty()) {
|
|
||||||
keyFileSelectionView?.uri = null
|
|
||||||
if (checkboxKeyFileView?.isChecked == true)
|
|
||||||
checkboxKeyFileView?.isChecked = false
|
|
||||||
} else {
|
|
||||||
keyFileSelectionView?.uri = uri
|
|
||||||
if (checkboxKeyFileView?.isChecked != true)
|
|
||||||
checkboxKeyFileView?.isChecked = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
// Reinit locking activity UI variable
|
// Reinit locking activity UI variable
|
||||||
DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
||||||
mAllowAutoOpenBiometricPrompt = true
|
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
|
|
||||||
mDatabaseKeyFileUri?.let {
|
|
||||||
outState.putString(KEY_KEYFILE, it.toString())
|
|
||||||
}
|
|
||||||
outState.putBoolean(KEY_READ_ONLY, mReadOnly)
|
outState.putBoolean(KEY_READ_ONLY, mReadOnly)
|
||||||
outState.putBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT, false)
|
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
private fun loadDatabase() {
|
||||||
val password: String? = passwordView?.text?.toString()
|
loadDatabase(mDatabaseFileUri,
|
||||||
val keyFile: Uri? = keyFileSelectionView?.uri
|
mainCredentialView?.getMainCredential(),
|
||||||
verifyCheckboxesAndLoadDatabase(password, keyFile, cipherDatabaseEntity)
|
null
|
||||||
}
|
)
|
||||||
|
|
||||||
private fun verifyCheckboxesAndLoadDatabase(password: String?,
|
|
||||||
keyFile: Uri?,
|
|
||||||
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
|
||||||
val keyPassword = if (checkboxPasswordView?.isChecked != true) null else password
|
|
||||||
verifyKeyFileCheckbox(keyFile)
|
|
||||||
loadDatabase(mDatabaseFileUri, keyPassword, mDatabaseKeyFileUri, cipherDatabaseEntity)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun verifyKeyFileCheckboxAndLoadDatabase(password: String?) {
|
|
||||||
val keyFile: Uri? = keyFileSelectionView?.uri
|
|
||||||
verifyKeyFileCheckbox(keyFile)
|
|
||||||
loadDatabase(mDatabaseFileUri, password, mDatabaseKeyFileUri)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun verifyKeyFileCheckbox(keyFile: Uri?) {
|
|
||||||
mDatabaseKeyFileUri = if (checkboxKeyFileView?.isChecked != true) null else keyFile
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadDatabase(databaseFileUri: Uri?,
|
private fun loadDatabase(databaseFileUri: Uri?,
|
||||||
password: String?,
|
mainCredential: MainCredential?,
|
||||||
keyFileUri: Uri?,
|
cipherEncryptDatabase: CipherEncryptDatabase?) {
|
||||||
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
|
||||||
|
|
||||||
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
|
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
|
||||||
clearCredentialsViews()
|
clearCredentialsViews()
|
||||||
@@ -556,11 +508,12 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
databaseFileUri?.let { databaseUri ->
|
databaseFileUri?.let { databaseUri ->
|
||||||
// Show the progress dialog and load the database
|
// Show the progress dialog and load the database
|
||||||
showProgressDialogAndLoadDatabase(
|
showProgressDialogAndLoadDatabase(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
MainCredential(password, keyFileUri),
|
mainCredential ?: MainCredential(),
|
||||||
mReadOnly,
|
mReadOnly,
|
||||||
cipherDatabaseEntity,
|
cipherEncryptDatabase,
|
||||||
false)
|
false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -568,14 +521,14 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
|
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
|
||||||
mainCredential: MainCredential,
|
mainCredential: MainCredential,
|
||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
cipherDatabaseEntity: CipherDatabaseEntity?,
|
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||||
fixDuplicateUUID: Boolean) {
|
fixDuplicateUUID: Boolean) {
|
||||||
loadDatabase(
|
loadDatabase(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
mainCredential,
|
mainCredential,
|
||||||
readOnly,
|
readOnly,
|
||||||
cipherDatabaseEntity,
|
cipherEncryptDatabase,
|
||||||
fixDuplicateUUID
|
fixDuplicateUUID
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,7 +549,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mSpecialMode == SpecialMode.DEFAULT) {
|
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||||
MenuUtil.defaultMenuInflater(inflater, menu)
|
MenuUtil.defaultMenuInflater(this, inflater, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
@@ -606,61 +559,33 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check permission
|
|
||||||
private fun checkPermission() {
|
|
||||||
if (Build.VERSION.SDK_INT in 23..28
|
|
||||||
&& !mReadOnly
|
|
||||||
&& !mPermissionAsked) {
|
|
||||||
mPermissionAsked = true
|
|
||||||
// Check self permission to show or not the dialog
|
|
||||||
val writePermission = android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
|
||||||
val permissions = arrayOf(writePermission)
|
|
||||||
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
|
// To fix multiple view education
|
||||||
private var performedEductionInProgress = false
|
private var performedEductionInProgress = false
|
||||||
private fun launchEducation(menu: Menu) {
|
private fun launchEducation(menu: Menu) {
|
||||||
if (!performedEductionInProgress) {
|
if (!performedEductionInProgress) {
|
||||||
performedEductionInProgress = true
|
performedEductionInProgress = true
|
||||||
// Show education views
|
// Show education views
|
||||||
Handler(Looper.getMainLooper()).post { performedNextEducation(PasswordActivityEducation(this), menu) }
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
performedNextEducation(menu)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
|
private fun performedNextEducation(menu: Menu) {
|
||||||
menu: Menu) {
|
|
||||||
val educationToolbar = toolbar
|
val educationToolbar = toolbar
|
||||||
val unlockEducationPerformed = educationToolbar != null
|
val unlockEducationPerformed = educationToolbar != null
|
||||||
&& passwordActivityEducation.checkAndPerformedUnlockEducation(
|
&& mPasswordActivityEducation.checkAndPerformedUnlockEducation(
|
||||||
educationToolbar,
|
educationToolbar,
|
||||||
{
|
{
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
performedNextEducation(menu)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
performedNextEducation(menu)
|
||||||
})
|
})
|
||||||
if (!unlockEducationPerformed) {
|
if (!unlockEducationPerformed) {
|
||||||
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(
|
&& mPasswordActivityEducation.checkAndPerformedReadOnlyEducation(
|
||||||
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
|
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -668,20 +593,34 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to find read mode menu")
|
Log.e(TAG, "Unable to find read mode menu")
|
||||||
}
|
}
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
performedNextEducation(menu)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
performedNextEducation(menu)
|
||||||
})
|
})
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||||
|
&& !readOnlyEducationPerformed) {
|
||||||
|
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(this)
|
||||||
|
if ((biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
||||||
|
&& advancedUnlockButton != null) {
|
||||||
|
mPasswordActivityEducation.checkAndPerformedBiometricEducation(
|
||||||
|
advancedUnlockButton!!,
|
||||||
|
{
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
this,
|
||||||
|
SettingsAdvancedUnlockActivity::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
|
||||||
advancedUnlockFragment?.performEducation(passwordActivityEducation,
|
})
|
||||||
readOnlyEducationPerformed,
|
}
|
||||||
{
|
}
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
} catch (ignored: Exception) {}
|
||||||
},
|
|
||||||
{
|
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -709,48 +648,9 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(
|
|
||||||
requestCode: Int,
|
|
||||||
resultCode: Int,
|
|
||||||
data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
|
|
||||||
mAllowAutoOpenBiometricPrompt = false
|
|
||||||
|
|
||||||
// To get device credential unlock result
|
|
||||||
advancedUnlockFragment?.onActivityResult(requestCode, resultCode, data)
|
|
||||||
|
|
||||||
// To get entry in result
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
var keyFileResult = false
|
|
||||||
mExternalFileHelper?.let {
|
|
||||||
keyFileResult = it.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
|
||||||
if (uri != null) {
|
|
||||||
mDatabaseKeyFileUri = uri
|
|
||||||
populateKeyFileTextView(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!keyFileResult) {
|
|
||||||
// this block if not a key file response
|
|
||||||
when (resultCode) {
|
|
||||||
DatabaseLockActivity.RESULT_EXIT_LOCK -> {
|
|
||||||
clearCredentialsViews()
|
|
||||||
closeDatabase()
|
|
||||||
}
|
|
||||||
Activity.RESULT_CANCELED -> {
|
|
||||||
clearCredentialsViews()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private val TAG = PasswordActivity::class.java.name
|
private val TAG = MainCredentialActivity::class.java.name
|
||||||
|
|
||||||
private const val UNLOCK_FRAGMENT_TAG = "UNLOCK_FRAGMENT_TAG"
|
private const val UNLOCK_FRAGMENT_TAG = "UNLOCK_FRAGMENT_TAG"
|
||||||
|
|
||||||
@@ -761,14 +661,10 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
private const val KEY_READ_ONLY = "KEY_READ_ONLY"
|
private const val KEY_READ_ONLY = "KEY_READ_ONLY"
|
||||||
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) {
|
||||||
val intent = Intent(activity, PasswordActivity::class.java)
|
val intent = Intent(activity, MainCredentialActivity::class.java)
|
||||||
intent.putExtra(KEY_FILENAME, databaseFile)
|
intent.putExtra(KEY_FILENAME, databaseFile)
|
||||||
if (keyFile != null)
|
if (keyFile != null)
|
||||||
intent.putExtra(KEY_KEYFILE, keyFile)
|
intent.putExtra(KEY_KEYFILE, keyFile)
|
||||||
@@ -855,15 +751,17 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
fun launchForAutofillResult(activity: Activity,
|
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
autofillComponent: AutofillComponent,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo?) {
|
searchInfo: SearchInfo?) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
AutofillHelper.startActivityForAutofillResult(
|
AutofillHelper.startActivityForAutofillResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
|
activityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
@@ -891,41 +789,43 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
* Global Launch
|
* Global Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launch(activity: Activity,
|
fun launch(activity: AppCompatActivity,
|
||||||
databaseUri: Uri,
|
databaseUri: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
||||||
onCancelSpecialMode: () -> Unit,
|
onCancelSpecialMode: () -> Unit,
|
||||||
onLaunchActivitySpecialMode: () -> Unit) {
|
onLaunchActivitySpecialMode: () -> Unit,
|
||||||
|
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||||
{
|
{
|
||||||
PasswordActivity.launch(activity,
|
MainCredentialActivity.launch(activity,
|
||||||
databaseUri, keyFile)
|
databaseUri, keyFile)
|
||||||
},
|
},
|
||||||
{ searchInfo -> // Search Action
|
{ searchInfo -> // Search Action
|
||||||
PasswordActivity.launchForSearchResult(activity,
|
MainCredentialActivity.launchForSearchResult(activity,
|
||||||
databaseUri, keyFile,
|
databaseUri, keyFile,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{ searchInfo -> // Save Action
|
{ searchInfo -> // Save Action
|
||||||
PasswordActivity.launchForSaveResult(activity,
|
MainCredentialActivity.launchForSaveResult(activity,
|
||||||
databaseUri, keyFile,
|
databaseUri, keyFile,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{ searchInfo -> // Keyboard Selection Action
|
{ searchInfo -> // Keyboard Selection Action
|
||||||
PasswordActivity.launchForKeyboardResult(activity,
|
MainCredentialActivity.launchForKeyboardResult(activity,
|
||||||
databaseUri, keyFile,
|
databaseUri, keyFile,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{ searchInfo, autofillComponent -> // Autofill Selection Action
|
{ searchInfo, autofillComponent -> // Autofill Selection Action
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
PasswordActivity.launchForAutofillResult(activity,
|
MainCredentialActivity.launchForAutofillResult(activity,
|
||||||
databaseUri, keyFile,
|
databaseUri, keyFile,
|
||||||
|
autofillActivityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
@@ -934,7 +834,7 @@ open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bui
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ registerInfo -> // Registration Action
|
{ registerInfo -> // Registration Action
|
||||||
PasswordActivity.launchForRegistration(activity,
|
MainCredentialActivity.launchForRegistration(activity,
|
||||||
databaseUri, keyFile,
|
databaseUri, keyFile,
|
||||||
registerInfo)
|
registerInfo)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import com.kunzisoft.androidclearchroma.view.ChromaColorView
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
|
||||||
|
|
||||||
|
class ColorPickerDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
|
private val mColorPickerViewModel: ColorPickerViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private lateinit var enableSwitchView: CompoundButton
|
||||||
|
private lateinit var chromaColorView: ChromaColorView
|
||||||
|
|
||||||
|
private var mDefaultColor = Color.WHITE
|
||||||
|
private var mActivated = false
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|
||||||
|
activity?.let { activity ->
|
||||||
|
val root = activity.layoutInflater.inflate(R.layout.fragment_color_picker, null)
|
||||||
|
enableSwitchView = root.findViewById(R.id.switch_element)
|
||||||
|
chromaColorView = root.findViewById(R.id.chroma_color_view)
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
if (savedInstanceState.containsKey(ARG_INITIAL_COLOR)) {
|
||||||
|
mDefaultColor = savedInstanceState.getInt(ARG_INITIAL_COLOR)
|
||||||
|
}
|
||||||
|
if (savedInstanceState.containsKey(ARG_ACTIVATED)) {
|
||||||
|
mActivated = savedInstanceState.getBoolean(ARG_ACTIVATED)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(ARG_INITIAL_COLOR)) {
|
||||||
|
mDefaultColor = getInt(ARG_INITIAL_COLOR)
|
||||||
|
}
|
||||||
|
if (containsKey(ARG_ACTIVATED)) {
|
||||||
|
mActivated = getBoolean(ARG_ACTIVATED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enableSwitchView.isChecked = mActivated
|
||||||
|
chromaColorView.currentColor = mDefaultColor
|
||||||
|
|
||||||
|
chromaColorView.setOnColorChangedListener {
|
||||||
|
if (!enableSwitchView.isChecked)
|
||||||
|
enableSwitchView.isChecked = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
builder.setView(root)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
val color: Int? = if (enableSwitchView.isChecked)
|
||||||
|
chromaColorView.currentColor
|
||||||
|
else
|
||||||
|
null
|
||||||
|
mColorPickerViewModel.pickColor(color)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putInt(ARG_INITIAL_COLOR, chromaColorView.currentColor)
|
||||||
|
outState.putBoolean(ARG_ACTIVATED, mActivated)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ARG_INITIAL_COLOR = "ARG_INITIAL_COLOR"
|
||||||
|
private const val ARG_ACTIVATED = "ARG_ACTIVATED"
|
||||||
|
|
||||||
|
fun newInstance(
|
||||||
|
@ColorInt initialColor: Int?,
|
||||||
|
): ColorPickerDialogFragment {
|
||||||
|
return ColorPickerDialogFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putInt(ARG_INITIAL_COLOR, initialColor ?: Color.WHITE)
|
||||||
|
putBoolean(ARG_ACTIVATED, initialColor != null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
|
||||||
|
|||||||
@@ -1,224 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.*
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.database.element.Field
|
|
||||||
import com.kunzisoft.keepass.password.PasswordGenerator
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
|
||||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
|
||||||
|
|
||||||
class GeneratePasswordDialogFragment : DatabaseDialogFragment() {
|
|
||||||
|
|
||||||
private var mListener: GeneratePasswordListener? = null
|
|
||||||
|
|
||||||
private var root: View? = null
|
|
||||||
private var lengthTextView: EditText? = null
|
|
||||||
private var passwordInputLayoutView: TextInputLayout? = null
|
|
||||||
private var passwordView: EditText? = null
|
|
||||||
|
|
||||||
private var mPasswordField: Field? = null
|
|
||||||
|
|
||||||
private var uppercaseBox: CompoundButton? = null
|
|
||||||
private var lowercaseBox: CompoundButton? = null
|
|
||||||
private var digitsBox: CompoundButton? = null
|
|
||||||
private var minusBox: CompoundButton? = null
|
|
||||||
private var underlineBox: CompoundButton? = null
|
|
||||||
private var spaceBox: CompoundButton? = null
|
|
||||||
private var specialsBox: CompoundButton? = null
|
|
||||||
private var bracketsBox: CompoundButton? = null
|
|
||||||
private var extendedBox: CompoundButton? = null
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
try {
|
|
||||||
mListener = context as GeneratePasswordListener
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
throw ClassCastException(context.toString()
|
|
||||||
+ " must implement " + GeneratePasswordListener::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetach() {
|
|
||||||
mListener = null
|
|
||||||
super.onDetach()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
activity?.let { activity ->
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
|
||||||
val inflater = activity.layoutInflater
|
|
||||||
root = inflater.inflate(R.layout.fragment_generate_password, null)
|
|
||||||
|
|
||||||
passwordInputLayoutView = root?.findViewById(R.id.password_input_layout)
|
|
||||||
passwordView = root?.findViewById(R.id.password)
|
|
||||||
passwordView?.applyFontVisibility()
|
|
||||||
val passwordCopyView: ImageView? = root?.findViewById(R.id.password_copy_button)
|
|
||||||
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(activity))
|
|
||||||
View.VISIBLE else View.GONE
|
|
||||||
val clipboardHelper = ClipboardHelper(activity)
|
|
||||||
passwordCopyView?.setOnClickListener {
|
|
||||||
clipboardHelper.timeoutCopyToClipboard(passwordView!!.text.toString(),
|
|
||||||
getString(R.string.copy_field,
|
|
||||||
getString(R.string.entry_password)))
|
|
||||||
}
|
|
||||||
|
|
||||||
lengthTextView = root?.findViewById(R.id.length)
|
|
||||||
|
|
||||||
uppercaseBox = root?.findViewById(R.id.cb_uppercase)
|
|
||||||
lowercaseBox = root?.findViewById(R.id.cb_lowercase)
|
|
||||||
digitsBox = root?.findViewById(R.id.cb_digits)
|
|
||||||
minusBox = root?.findViewById(R.id.cb_minus)
|
|
||||||
underlineBox = root?.findViewById(R.id.cb_underline)
|
|
||||||
spaceBox = root?.findViewById(R.id.cb_space)
|
|
||||||
specialsBox = root?.findViewById(R.id.cb_specials)
|
|
||||||
bracketsBox = root?.findViewById(R.id.cb_brackets)
|
|
||||||
extendedBox = root?.findViewById(R.id.cb_extended)
|
|
||||||
|
|
||||||
mPasswordField = arguments?.getParcelable(KEY_PASSWORD_FIELD)
|
|
||||||
|
|
||||||
assignDefaultCharacters()
|
|
||||||
|
|
||||||
val seekBar = root?.findViewById<SeekBar>(R.id.seekbar_length)
|
|
||||||
seekBar?.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
||||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
|
||||||
lengthTextView?.setText(progress.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
|
||||||
|
|
||||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
|
||||||
})
|
|
||||||
|
|
||||||
context?.let { context ->
|
|
||||||
seekBar?.progress = PreferencesUtil.getDefaultPasswordLength(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
root?.findViewById<Button>(R.id.generate_password_button)
|
|
||||||
?.setOnClickListener { fillPassword() }
|
|
||||||
|
|
||||||
builder.setView(root)
|
|
||||||
.setPositiveButton(R.string.accept) { _, _ ->
|
|
||||||
mPasswordField?.let { passwordField ->
|
|
||||||
passwordView?.text?.toString()?.let { passwordValue ->
|
|
||||||
passwordField.protectedValue.stringValue = passwordValue
|
|
||||||
}
|
|
||||||
mListener?.acceptPassword(passwordField)
|
|
||||||
}
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
mPasswordField?.let { passwordField ->
|
|
||||||
mListener?.cancelPassword(passwordField)
|
|
||||||
}
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pre-populate a password to possibly save the user a few clicks
|
|
||||||
fillPassword()
|
|
||||||
|
|
||||||
return builder.create()
|
|
||||||
}
|
|
||||||
return super.onCreateDialog(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun assignDefaultCharacters() {
|
|
||||||
uppercaseBox?.isChecked = false
|
|
||||||
lowercaseBox?.isChecked = false
|
|
||||||
digitsBox?.isChecked = false
|
|
||||||
minusBox?.isChecked = false
|
|
||||||
underlineBox?.isChecked = false
|
|
||||||
spaceBox?.isChecked = false
|
|
||||||
specialsBox?.isChecked = false
|
|
||||||
bracketsBox?.isChecked = false
|
|
||||||
extendedBox?.isChecked = false
|
|
||||||
|
|
||||||
context?.let { context ->
|
|
||||||
PreferencesUtil.getDefaultPasswordCharacters(context)?.let { charSet ->
|
|
||||||
for (passwordChar in charSet) {
|
|
||||||
when (passwordChar) {
|
|
||||||
getString(R.string.value_password_uppercase) -> uppercaseBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_lowercase) -> lowercaseBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_digits) -> digitsBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_minus) -> minusBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_underline) -> underlineBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_space) -> spaceBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_special) -> specialsBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_brackets) -> bracketsBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_extended) -> extendedBox?.isChecked = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fillPassword() {
|
|
||||||
root?.findViewById<EditText>(R.id.password)?.setText(generatePassword())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generatePassword(): String {
|
|
||||||
var password = ""
|
|
||||||
try {
|
|
||||||
val length = Integer.valueOf(root?.findViewById<EditText>(R.id.length)?.text.toString())
|
|
||||||
password = PasswordGenerator(resources).generatePassword(length,
|
|
||||||
uppercaseBox?.isChecked == true,
|
|
||||||
lowercaseBox?.isChecked == true,
|
|
||||||
digitsBox?.isChecked == true,
|
|
||||||
minusBox?.isChecked == true,
|
|
||||||
underlineBox?.isChecked == true,
|
|
||||||
spaceBox?.isChecked == true,
|
|
||||||
specialsBox?.isChecked == true,
|
|
||||||
bracketsBox?.isChecked == true,
|
|
||||||
extendedBox?.isChecked == true)
|
|
||||||
passwordInputLayoutView?.error = null
|
|
||||||
} catch (e: NumberFormatException) {
|
|
||||||
passwordInputLayoutView?.error = getString(R.string.error_wrong_length)
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
passwordInputLayoutView?.error = e.message
|
|
||||||
}
|
|
||||||
|
|
||||||
return password
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GeneratePasswordListener {
|
|
||||||
fun acceptPassword(passwordField: Field)
|
|
||||||
fun cancelPassword(passwordField: Field)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val KEY_PASSWORD_FIELD = "KEY_PASSWORD_FIELD"
|
|
||||||
|
|
||||||
fun getInstance(field: Field): GeneratePasswordDialogFragment {
|
|
||||||
return GeneratePasswordDialogFragment().apply {
|
|
||||||
arguments = Bundle().apply {
|
|
||||||
putParcelable(KEY_PASSWORD_FIELD, field)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.adapters.TagsAdapter
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
|
import com.kunzisoft.keepass.view.DateTimeFieldView
|
||||||
|
|
||||||
|
class GroupDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
|
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
|
||||||
|
private var mGroupInfo = GroupInfo()
|
||||||
|
|
||||||
|
private lateinit var iconView: ImageView
|
||||||
|
private var mIconColor: Int = 0
|
||||||
|
private lateinit var nameTextView: TextView
|
||||||
|
private lateinit var tagsListView: RecyclerView
|
||||||
|
private var tagsAdapter: TagsAdapter? = null
|
||||||
|
private lateinit var notesTextLabelView: TextView
|
||||||
|
private lateinit var notesTextView: TextView
|
||||||
|
private lateinit var expirationView: DateTimeFieldView
|
||||||
|
private lateinit var creationView: TextView
|
||||||
|
private lateinit var modificationView: TextView
|
||||||
|
private lateinit var searchableLabelView: TextView
|
||||||
|
private lateinit var searchableView: TextView
|
||||||
|
private lateinit var autoTypeLabelView: TextView
|
||||||
|
private lateinit var autoTypeView: TextView
|
||||||
|
private lateinit var uuidContainerView: ViewGroup
|
||||||
|
private lateinit var uuidReferenceView: TextView
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
mPopulateIconMethod = { imageView, icon ->
|
||||||
|
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||||
|
}
|
||||||
|
mPopulateIconMethod?.invoke(iconView, mGroupInfo.icon)
|
||||||
|
|
||||||
|
if (database?.allowCustomSearchableGroup() == true) {
|
||||||
|
searchableLabelView.visibility = View.VISIBLE
|
||||||
|
searchableView.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
searchableLabelView.visibility = View.GONE
|
||||||
|
searchableView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Auto-Type
|
||||||
|
/*
|
||||||
|
if (database?.allowAutoType() == true) {
|
||||||
|
autoTypeLabelView.visibility = View.VISIBLE
|
||||||
|
autoTypeView.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
autoTypeLabelView.visibility = View.GONE
|
||||||
|
autoTypeView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
val root = activity.layoutInflater.inflate(R.layout.fragment_group, null)
|
||||||
|
iconView = root.findViewById(R.id.group_icon)
|
||||||
|
nameTextView = root.findViewById(R.id.group_name)
|
||||||
|
tagsListView = root.findViewById(R.id.group_tags_list_view)
|
||||||
|
notesTextLabelView = root.findViewById(R.id.group_note_label)
|
||||||
|
notesTextView = root.findViewById(R.id.group_note)
|
||||||
|
expirationView = root.findViewById(R.id.group_expiration)
|
||||||
|
creationView = root.findViewById(R.id.group_created)
|
||||||
|
modificationView = root.findViewById(R.id.group_modified)
|
||||||
|
searchableLabelView = root.findViewById(R.id.group_searchable_label)
|
||||||
|
searchableView = root.findViewById(R.id.group_searchable)
|
||||||
|
autoTypeLabelView = root.findViewById(R.id.group_auto_type_label)
|
||||||
|
autoTypeView = root.findViewById(R.id.group_auto_type)
|
||||||
|
uuidContainerView = root.findViewById(R.id.group_UUID_container)
|
||||||
|
uuidReferenceView = root.findViewById(R.id.group_UUID_reference)
|
||||||
|
|
||||||
|
// Retrieve the textColor to tint the icon
|
||||||
|
val ta = activity.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||||
|
mIconColor = ta.getColor(0, Color.WHITE)
|
||||||
|
ta.recycle()
|
||||||
|
|
||||||
|
if (savedInstanceState != null
|
||||||
|
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
|
||||||
|
mGroupInfo = savedInstanceState.getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
||||||
|
} else {
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(KEY_GROUP_INFO)) {
|
||||||
|
mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// populate info in views
|
||||||
|
val title = mGroupInfo.title
|
||||||
|
if (title.isEmpty()) {
|
||||||
|
nameTextView.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
nameTextView.text = title
|
||||||
|
nameTextView.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
tagsAdapter = TagsAdapter(activity)
|
||||||
|
tagsListView.apply {
|
||||||
|
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
adapter = tagsAdapter
|
||||||
|
}
|
||||||
|
val tags = mGroupInfo.tags
|
||||||
|
tagsListView.visibility = if (tags.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
tagsAdapter?.setTags(tags)
|
||||||
|
val notes = mGroupInfo.notes
|
||||||
|
if (notes == null || notes.isEmpty()) {
|
||||||
|
notesTextLabelView.visibility = View.GONE
|
||||||
|
notesTextView.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
notesTextView.text = notes
|
||||||
|
notesTextLabelView.visibility = View.VISIBLE
|
||||||
|
notesTextView.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
expirationView.activation = mGroupInfo.expires
|
||||||
|
expirationView.dateTime = mGroupInfo.expiryTime
|
||||||
|
creationView.text = mGroupInfo.creationTime.getDateTimeString(resources)
|
||||||
|
modificationView.text = mGroupInfo.lastModificationTime.getDateTimeString(resources)
|
||||||
|
searchableView.text = stringFromInheritableBoolean(mGroupInfo.searchable)
|
||||||
|
autoTypeView.text = stringFromInheritableBoolean(mGroupInfo.enableAutoType,
|
||||||
|
mGroupInfo.defaultAutoTypeSequence)
|
||||||
|
val uuid = UuidUtil.toHexString(mGroupInfo.id)
|
||||||
|
if (uuid == null || uuid.isEmpty()) {
|
||||||
|
uuidContainerView.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
uuidReferenceView.text = uuid
|
||||||
|
uuidContainerView.apply {
|
||||||
|
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
builder.setView(root)
|
||||||
|
.setPositiveButton(android.R.string.ok){ _, _ ->
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stringFromInheritableBoolean(enable: Boolean?, value: String? = null): String {
|
||||||
|
val valueString = if (value != null && value.isNotEmpty()) " [$value]" else ""
|
||||||
|
return when {
|
||||||
|
enable == null -> getString(R.string.inherited) + valueString
|
||||||
|
enable -> getString(R.string.enable) + valueString
|
||||||
|
else -> getString(R.string.disable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
outState.putParcelable(KEY_GROUP_INFO, mGroupInfo)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Error(val isError: Boolean, val messageId: Int?)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG_SHOW_GROUP = "TAG_SHOW_GROUP"
|
||||||
|
private const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
||||||
|
|
||||||
|
fun launch(groupInfo: GroupInfo): GroupDialogFragment {
|
||||||
|
val bundle = Bundle()
|
||||||
|
bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
|
||||||
|
val fragment = GroupDialogFragment()
|
||||||
|
fragment.arguments = bundle
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,20 +23,23 @@ import android.app.Dialog
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Button
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.*
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.*
|
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.*
|
||||||
|
import com.kunzisoft.keepass.adapters.TagsProposalAdapter
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.model.GroupInfo
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
import com.kunzisoft.keepass.view.DateTimeEditFieldView
|
import com.kunzisoft.keepass.view.DateTimeEditFieldView
|
||||||
|
import com.kunzisoft.keepass.view.InheritedCompletionView
|
||||||
|
import com.kunzisoft.keepass.view.TagsCompletionView
|
||||||
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
|
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
|
||||||
|
import com.tokenautocomplete.FilteredArrayAdapter
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
class GroupEditDialogFragment : DatabaseDialogFragment() {
|
class GroupEditDialogFragment : DatabaseDialogFragment() {
|
||||||
@@ -55,6 +58,14 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
private lateinit var notesTextLayoutView: TextInputLayout
|
private lateinit var notesTextLayoutView: TextInputLayout
|
||||||
private lateinit var notesTextView: TextView
|
private lateinit var notesTextView: TextView
|
||||||
private lateinit var expirationView: DateTimeEditFieldView
|
private lateinit var expirationView: DateTimeEditFieldView
|
||||||
|
private lateinit var searchableContainerView: TextInputLayout
|
||||||
|
private lateinit var searchableView: InheritedCompletionView
|
||||||
|
private lateinit var autoTypeContainerView: ViewGroup
|
||||||
|
private lateinit var autoTypeInheritedView: InheritedCompletionView
|
||||||
|
private lateinit var autoTypeSequenceView: TextView
|
||||||
|
private lateinit var tagsContainerView: TextInputLayout
|
||||||
|
private lateinit var tagsCompletionView: TagsCompletionView
|
||||||
|
private var tagsAdapter: FilteredArrayAdapter<String>? = null
|
||||||
|
|
||||||
enum class EditGroupDialogAction {
|
enum class EditGroupDialogAction {
|
||||||
CREATION, UPDATE, NONE;
|
CREATION, UPDATE, NONE;
|
||||||
@@ -107,10 +118,30 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
mPopulateIconMethod = { imageView, icon ->
|
mPopulateIconMethod = { imageView, icon ->
|
||||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||||
}
|
}
|
||||||
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
||||||
|
|
||||||
|
searchableContainerView.visibility = if (database?.allowCustomSearchableGroup() == true) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
if (database?.allowAutoType() == true) {
|
||||||
|
autoTypeContainerView.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
autoTypeContainerView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
|
||||||
|
tagsCompletionView.apply {
|
||||||
|
threshold = 1
|
||||||
|
setAdapter(tagsAdapter)
|
||||||
|
}
|
||||||
|
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
@@ -122,6 +153,13 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
notesTextLayoutView = root.findViewById(R.id.group_edit_note_container)
|
notesTextLayoutView = root.findViewById(R.id.group_edit_note_container)
|
||||||
notesTextView = root.findViewById(R.id.group_edit_note)
|
notesTextView = root.findViewById(R.id.group_edit_note)
|
||||||
expirationView = root.findViewById(R.id.group_edit_expiration)
|
expirationView = root.findViewById(R.id.group_edit_expiration)
|
||||||
|
searchableContainerView = root.findViewById(R.id.group_edit_searchable_container)
|
||||||
|
searchableView = root.findViewById(R.id.group_edit_searchable)
|
||||||
|
autoTypeContainerView = root.findViewById(R.id.group_edit_auto_type_container)
|
||||||
|
autoTypeInheritedView = root.findViewById(R.id.group_edit_auto_type_inherited)
|
||||||
|
autoTypeSequenceView = root.findViewById(R.id.group_edit_auto_type_sequence)
|
||||||
|
tagsContainerView = root.findViewById(R.id.group_tags_label)
|
||||||
|
tagsCompletionView = root.findViewById(R.id.group_tags_completion_view)
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
// Retrieve the textColor to tint the icon
|
||||||
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
@@ -197,6 +235,19 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
expirationView.activation = groupInfo.expires
|
expirationView.activation = groupInfo.expires
|
||||||
expirationView.dateTime = groupInfo.expiryTime
|
expirationView.dateTime = groupInfo.expiryTime
|
||||||
|
|
||||||
|
// Set searchable
|
||||||
|
searchableView.setValue(groupInfo.searchable)
|
||||||
|
// Set auto-type
|
||||||
|
autoTypeInheritedView.setValue(groupInfo.enableAutoType)
|
||||||
|
autoTypeSequenceView.text = groupInfo.defaultAutoTypeSequence
|
||||||
|
// Set Tags
|
||||||
|
groupInfo.tags.let { tags ->
|
||||||
|
tagsCompletionView.setText("")
|
||||||
|
for (i in 0 until tags.size()) {
|
||||||
|
tagsCompletionView.addObjectSync(tags.get(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun retrieveGroupInfoFromViews() {
|
private fun retrieveGroupInfoFromViews() {
|
||||||
@@ -208,6 +259,10 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
mGroupInfo.expires = expirationView.activation
|
mGroupInfo.expires = expirationView.activation
|
||||||
mGroupInfo.expiryTime = expirationView.dateTime
|
mGroupInfo.expiryTime = expirationView.dateTime
|
||||||
|
mGroupInfo.searchable = searchableView.getValue()
|
||||||
|
mGroupInfo.enableAutoType = autoTypeInheritedView.getValue()
|
||||||
|
mGroupInfo.defaultAutoTypeSequence = autoTypeSequenceView.text.toString()
|
||||||
|
mGroupInfo.tags = tagsCompletionView.getTags()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
@@ -246,8 +301,8 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
|
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
|
||||||
const val KEY_ACTION_ID = "KEY_ACTION_ID"
|
private const val KEY_ACTION_ID = "KEY_ACTION_ID"
|
||||||
const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
private const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
||||||
|
|
||||||
fun create(groupInfo: GroupInfo): GroupEditDialogFragment {
|
fun create(groupInfo: GroupInfo): GroupEditDialogFragment {
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
|
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||||
|
|
||||||
|
class IconEditDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
|
private val mIconPickerViewModel: IconPickerViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
|
||||||
|
private lateinit var iconView: ImageView
|
||||||
|
private lateinit var nameTextLayoutView: TextInputLayout
|
||||||
|
private lateinit var nameTextView: TextView
|
||||||
|
|
||||||
|
private var mCustomIcon: IconImageCustom? = null
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
mPopulateIconMethod = { imageView, icon ->
|
||||||
|
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon)
|
||||||
|
}
|
||||||
|
mCustomIcon?.let { customIcon ->
|
||||||
|
populateViewsWithCustomIcon(customIcon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
val root = activity.layoutInflater.inflate(R.layout.fragment_icon_edit, null)
|
||||||
|
iconView = root.findViewById(R.id.icon_edit_image)
|
||||||
|
nameTextLayoutView = root.findViewById(R.id.icon_edit_name_container)
|
||||||
|
nameTextView = root.findViewById(R.id.icon_edit_name)
|
||||||
|
|
||||||
|
if (savedInstanceState != null
|
||||||
|
&& savedInstanceState.containsKey(KEY_CUSTOM_ICON_ID)) {
|
||||||
|
mCustomIcon = savedInstanceState.getParcelable(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
|
||||||
|
} else {
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(KEY_CUSTOM_ICON_ID)) {
|
||||||
|
mCustomIcon = getParcelable(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
builder.setView(root)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
retrieveIconInfoFromViews()
|
||||||
|
mCustomIcon?.let { customIcon ->
|
||||||
|
mIconPickerViewModel.updateCustomIcon(
|
||||||
|
IconPickerViewModel.IconCustomState(customIcon, false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
// Do nothing
|
||||||
|
mIconPickerViewModel.updateCustomIcon(
|
||||||
|
IconPickerViewModel.IconCustomState(null, false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateViewsWithCustomIcon(customIcon: IconImageCustom) {
|
||||||
|
mPopulateIconMethod?.invoke(iconView, customIcon.getIconImageToDraw())
|
||||||
|
nameTextView.text = customIcon.name
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun retrieveIconInfoFromViews() {
|
||||||
|
mCustomIcon?.name = nameTextView.text.toString()
|
||||||
|
mCustomIcon?.lastModificationTime = DateInstant()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
retrieveIconInfoFromViews()
|
||||||
|
outState.putParcelable(KEY_CUSTOM_ICON_ID, mCustomIcon)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val TAG_UPDATE_ICON = "TAG_UPDATE_ICON"
|
||||||
|
const val KEY_CUSTOM_ICON_ID = "KEY_CUSTOM_ICON_ID"
|
||||||
|
|
||||||
|
fun update(customIcon: IconImageCustom): IconEditDialogFragment {
|
||||||
|
val bundle = Bundle()
|
||||||
|
bundle.putParcelable(KEY_CUSTOM_ICON_ID, IconImageCustom(customIcon))
|
||||||
|
val fragment = IconEditDialogFragment()
|
||||||
|
fragment.arguments = bundle
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 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.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.view.MainCredentialView
|
||||||
|
|
||||||
|
class MainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
|
private var mainCredentialView: MainCredentialView? = null
|
||||||
|
|
||||||
|
private var mListener: AskMainCredentialDialogListener? = null
|
||||||
|
|
||||||
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
|
interface AskMainCredentialDialogListener {
|
||||||
|
fun onAskMainCredentialDialogPositiveClick(databaseUri: Uri?, mainCredential: MainCredential)
|
||||||
|
fun onAskMainCredentialDialogNegativeClick(databaseUri: Uri?, mainCredential: MainCredential)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttach(activity: Context) {
|
||||||
|
super.onAttach(activity)
|
||||||
|
try {
|
||||||
|
mListener = activity as AskMainCredentialDialogListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ClassCastException(activity.toString()
|
||||||
|
+ " must implement " + AskMainCredentialDialogListener::class.java.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
mListener = null
|
||||||
|
super.onDetach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
activity?.let { activity ->
|
||||||
|
|
||||||
|
var databaseUri: Uri? = null
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(KEY_ASK_CREDENTIAL_URI))
|
||||||
|
databaseUri = getParcelable(KEY_ASK_CREDENTIAL_URI)
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|
||||||
|
val root = activity.layoutInflater.inflate(R.layout.fragment_main_credential, null)
|
||||||
|
mainCredentialView = root.findViewById(R.id.main_credential_view)
|
||||||
|
databaseUri?.let {
|
||||||
|
root.findViewById<TextView>(R.id.title_database)?.text =
|
||||||
|
UriUtil.getFileData(requireContext(), it)?.name
|
||||||
|
}
|
||||||
|
builder.setView(root)
|
||||||
|
// Add action buttons
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
mListener?.onAskMainCredentialDialogPositiveClick(
|
||||||
|
databaseUri,
|
||||||
|
retrieveMainCredential()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
mListener?.onAskMainCredentialDialogNegativeClick(
|
||||||
|
databaseUri,
|
||||||
|
retrieveMainCredential()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
|
if (uri != null) {
|
||||||
|
mainCredentialView?.populateKeyFileTextView(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
|
||||||
|
|
||||||
|
return builder.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onCreateDialog(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun retrieveMainCredential(): MainCredential {
|
||||||
|
return mainCredentialView?.getMainCredential() ?: MainCredential()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val KEY_ASK_CREDENTIAL_URI = "KEY_ASK_CREDENTIAL_URI"
|
||||||
|
const val TAG_ASK_MAIN_CREDENTIAL = "TAG_ASK_MAIN_CREDENTIAL"
|
||||||
|
|
||||||
|
fun getInstance(uri: Uri?): MainCredentialDialogFragment {
|
||||||
|
val fragment = MainCredentialDialogFragment()
|
||||||
|
val args = Bundle()
|
||||||
|
args.putParcelable(KEY_ASK_CREDENTIAL_URI, uri)
|
||||||
|
fragment.arguments = args
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
@@ -37,10 +36,13 @@ import com.kunzisoft.keepass.R
|
|||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
|
import com.kunzisoft.keepass.password.PasswordEntropy
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
|
import com.kunzisoft.keepass.view.PassKeyView
|
||||||
|
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||||
|
|
||||||
class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
private var mMasterPassword: String? = null
|
private var mMasterPassword: String? = null
|
||||||
private var mKeyFile: Uri? = null
|
private var mKeyFile: Uri? = null
|
||||||
@@ -49,17 +51,17 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
|||||||
|
|
||||||
private var passwordCheckBox: CompoundButton? = null
|
private var passwordCheckBox: CompoundButton? = null
|
||||||
|
|
||||||
private var passwordTextInputLayout: TextInputLayout? = null
|
private var passKeyView: PassKeyView? = null
|
||||||
private var passwordView: TextView? = null
|
|
||||||
private var passwordRepeatTextInputLayout: TextInputLayout? = null
|
private var passwordRepeatTextInputLayout: TextInputLayout? = null
|
||||||
private var passwordRepeatView: TextView? = null
|
private var passwordRepeatView: TextView? = null
|
||||||
|
|
||||||
private var keyFileCheckBox: CompoundButton? = null
|
private var keyFileCheckBox: CompoundButton? = null
|
||||||
private var keyFileSelectionView: KeyFileSelectionView? = null
|
private var keyFileSelectionView: KeyFileSelectionView? = null
|
||||||
|
|
||||||
private var mListener: AssignPasswordDialogListener? = null
|
private var mListener: AssignMainCredentialDialogListener? = null
|
||||||
|
|
||||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
private var mPasswordEntropyCalculator: PasswordEntropy? = null
|
||||||
|
|
||||||
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
|
private var mEmptyPasswordConfirmationDialog: AlertDialog? = null
|
||||||
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
||||||
@@ -75,7 +77,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AssignPasswordDialogListener {
|
interface AssignMainCredentialDialogListener {
|
||||||
fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential)
|
fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential)
|
||||||
fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential)
|
fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential)
|
||||||
}
|
}
|
||||||
@@ -83,10 +85,10 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
|||||||
override fun onAttach(activity: Context) {
|
override fun onAttach(activity: Context) {
|
||||||
super.onAttach(activity)
|
super.onAttach(activity)
|
||||||
try {
|
try {
|
||||||
mListener = activity as AssignPasswordDialogListener
|
mListener = activity as AssignMainCredentialDialogListener
|
||||||
} catch (e: ClassCastException) {
|
} catch (e: ClassCastException) {
|
||||||
throw ClassCastException(activity.toString()
|
throw ClassCastException(activity.toString()
|
||||||
+ " must implement " + AssignPasswordDialogListener::class.java.name)
|
+ " must implement " + AssignMainCredentialDialogListener::class.java.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +103,13 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
|||||||
super.onDetach()
|
super.onDetach()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Create the password entropy object
|
||||||
|
mPasswordEntropyCalculator = PasswordEntropy()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
|
|
||||||
@@ -113,7 +122,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
|||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
val inflater = activity.layoutInflater
|
val inflater = activity.layoutInflater
|
||||||
|
|
||||||
rootView = inflater.inflate(R.layout.fragment_set_password, null)
|
rootView = inflater.inflate(R.layout.fragment_set_main_credential, null)
|
||||||
builder.setView(rootView)
|
builder.setView(rootView)
|
||||||
// Add action buttons
|
// Add action buttons
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||||
@@ -124,15 +133,27 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
||||||
passwordTextInputLayout = rootView?.findViewById(R.id.password_input_layout)
|
passKeyView = rootView?.findViewById(R.id.password_view)
|
||||||
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.password_confirmation)
|
||||||
|
passwordRepeatView?.applyFontVisibility()
|
||||||
|
|
||||||
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
||||||
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
||||||
|
|
||||||
mExternalFileHelper = ExternalFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
|
uri?.let { pathUri ->
|
||||||
|
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
||||||
|
keyFileSelectionView?.error = null
|
||||||
|
keyFileCheckBox?.isChecked = true
|
||||||
|
keyFileSelectionView?.uri = pathUri
|
||||||
|
if (lengthFile <= 0L) {
|
||||||
|
showEmptyKeyFileConfirmationDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
|
|
||||||
val dialog = builder.create()
|
val dialog = builder.create()
|
||||||
@@ -151,7 +172,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
|||||||
if (allowNoMasterKey)
|
if (allowNoMasterKey)
|
||||||
showNoKeyConfirmationDialog()
|
showNoKeyConfirmationDialog()
|
||||||
else {
|
else {
|
||||||
passwordTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
|
passwordRepeatTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!error) {
|
if (!error) {
|
||||||
@@ -183,22 +204,22 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
// To check checkboxes if a text is present
|
// To check checkboxes if a text is present
|
||||||
passwordView?.addTextChangedListener(passwordTextWatcher)
|
passKeyView?.addTextChangedListener(passwordTextWatcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
passwordView?.removeTextChangedListener(passwordTextWatcher)
|
passKeyView?.removeTextChangedListener(passwordTextWatcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyPassword(): Boolean {
|
private fun verifyPassword(): Boolean {
|
||||||
var error = false
|
var error = false
|
||||||
if (passwordCheckBox != null
|
if (passwordCheckBox != null
|
||||||
&& passwordCheckBox!!.isChecked
|
&& passwordCheckBox!!.isChecked
|
||||||
&& passwordView != null
|
&& passKeyView != null
|
||||||
&& passwordRepeatView != null) {
|
&& passwordRepeatView != null) {
|
||||||
mMasterPassword = passwordView!!.text.toString()
|
mMasterPassword = passKeyView!!.passwordString
|
||||||
val confPassword = passwordRepeatView!!.text.toString()
|
val confPassword = passwordRepeatView!!.text.toString()
|
||||||
|
|
||||||
// Verify that passwords match
|
// Verify that passwords match
|
||||||
@@ -208,7 +229,11 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
|||||||
passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match)
|
passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mMasterPassword == null || mMasterPassword!!.isEmpty()) {
|
if ((mMasterPassword == null
|
||||||
|
|| mMasterPassword!!.isEmpty())
|
||||||
|
&& (keyFileCheckBox == null
|
||||||
|
|| !keyFileCheckBox!!.isChecked
|
||||||
|
|| keyFileSelectionView?.uri == null)) {
|
||||||
error = true
|
error = true
|
||||||
showEmptyPasswordConfirmationDialog()
|
showEmptyPasswordConfirmationDialog()
|
||||||
}
|
}
|
||||||
@@ -239,7 +264,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
|||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
if (!verifyKeyFile()) {
|
if (!verifyKeyFile()) {
|
||||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||||
this@AssignMasterKeyDialogFragment.dismiss()
|
this@SetMainCredentialDialogFragment.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
@@ -254,7 +279,7 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
|||||||
builder.setMessage(R.string.warning_no_encryption_key)
|
builder.setMessage(R.string.warning_no_encryption_key)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||||
this@AssignMasterKeyDialogFragment.dismiss()
|
this@SetMainCredentialDialogFragment.dismiss()
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
mNoKeyConfirmationDialog = builder.create()
|
mNoKeyConfirmationDialog = builder.create()
|
||||||
@@ -282,29 +307,12 @@ class AssignMasterKeyDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
|
|
||||||
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
|
||||||
uri?.let { pathUri ->
|
|
||||||
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
|
||||||
keyFileSelectionView?.error = null
|
|
||||||
keyFileCheckBox?.isChecked = true
|
|
||||||
keyFileSelectionView?.uri = pathUri
|
|
||||||
if (lengthFile <= 0L) {
|
|
||||||
showEmptyKeyFileConfirmationDialog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
|
private const val ALLOW_NO_MASTER_KEY_ARG = "ALLOW_NO_MASTER_KEY_ARG"
|
||||||
|
|
||||||
fun getInstance(allowNoMasterKey: Boolean): AssignMasterKeyDialogFragment {
|
fun getInstance(allowNoMasterKey: Boolean): SetMainCredentialDialogFragment {
|
||||||
val fragment = AssignMasterKeyDialogFragment()
|
val fragment = SetMainCredentialDialogFragment()
|
||||||
val args = Bundle()
|
val args = Bundle()
|
||||||
args.putBoolean(ALLOW_NO_MASTER_KEY_ARG, allowNoMasterKey)
|
args.putBoolean(ALLOW_NO_MASTER_KEY_ARG, allowNoMasterKey)
|
||||||
fragment.arguments = args
|
fragment.arguments = args
|
||||||
@@ -204,9 +204,10 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
|||||||
android.R.layout.simple_spinner_item, mHotpTokenTypeArray!!).apply {
|
android.R.layout.simple_spinner_item, mHotpTokenTypeArray!!).apply {
|
||||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
}
|
}
|
||||||
// Proprietary only on closed and full version
|
// Proprietary only on full version
|
||||||
mTotpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
|
mTotpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
|
||||||
BuildConfig.CLOSED_STORE && BuildConfig.FULL_VERSION)
|
UriUtil.contributingUser(activity)
|
||||||
|
)
|
||||||
totpTokenTypeAdapter = ArrayAdapter(activity,
|
totpTokenTypeAdapter = ArrayAdapter(activity,
|
||||||
android.R.layout.simple_spinner_item, mTotpTokenTypeArray!!).apply {
|
android.R.layout.simple_spinner_item, mTotpTokenTypeArray!!).apply {
|
||||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
@@ -309,7 +310,7 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
|||||||
override fun afterTextChanged(s: Editable?) {
|
override fun afterTextChanged(s: Editable?) {
|
||||||
s?.toString()?.let { userString ->
|
s?.toString()?.let { userString ->
|
||||||
try {
|
try {
|
||||||
mOtpElement.setBase32Secret(userString.toUpperCase(Locale.ENGLISH))
|
mOtpElement.setBase32Secret(userString.uppercase(Locale.ENGLISH))
|
||||||
otpSecretContainer?.error = null
|
otpSecretContainer?.error = null
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
|
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import android.text.SpannableStringBuilder
|
|||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.BuildConfig
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
@@ -40,22 +39,12 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
|
|||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|
||||||
val stringBuilder = SpannableStringBuilder()
|
val stringBuilder = SpannableStringBuilder()
|
||||||
if (BuildConfig.CLOSED_STORE) {
|
if (UriUtil.contributingUser(activity)) {
|
||||||
if (BuildConfig.FULL_VERSION) {
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_thanks), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_thanks), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
.append(HtmlCompat.fromHtml(getString(R.string.html_rose), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_rose), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_work_hard), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
|
||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_work_hard), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
|
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_upgrade), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_upgrade), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() }
|
||||||
builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() }
|
|
||||||
} else {
|
|
||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
|
||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_buy_pro), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n")
|
|
||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
|
||||||
builder.setPositiveButton(R.string.download) { _, _ ->
|
|
||||||
UriUtil.gotoUrl(requireContext(), R.string.app_pro_url)
|
|
||||||
}
|
|
||||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_contibute), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_contibute), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
||||||
|
|||||||
@@ -29,10 +29,12 @@ import androidx.fragment.app.activityViewModels
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
||||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||||
|
import com.kunzisoft.keepass.adapters.TagsProposalAdapter
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.template.Template
|
import com.kunzisoft.keepass.database.element.template.Template
|
||||||
@@ -40,11 +42,10 @@ import com.kunzisoft.keepass.model.AttachmentState
|
|||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.StreamDirection
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
import com.kunzisoft.keepass.view.TemplateEditView
|
import com.kunzisoft.keepass.view.*
|
||||||
import com.kunzisoft.keepass.view.collapse
|
|
||||||
import com.kunzisoft.keepass.view.expand
|
|
||||||
import com.kunzisoft.keepass.view.showByFading
|
|
||||||
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
||||||
|
import com.tokenautocomplete.FilteredArrayAdapter
|
||||||
|
|
||||||
|
|
||||||
class EntryEditFragment: DatabaseFragment() {
|
class EntryEditFragment: DatabaseFragment() {
|
||||||
|
|
||||||
@@ -55,6 +56,9 @@ class EntryEditFragment: DatabaseFragment() {
|
|||||||
private lateinit var attachmentsContainerView: ViewGroup
|
private lateinit var attachmentsContainerView: ViewGroup
|
||||||
private lateinit var attachmentsListView: RecyclerView
|
private lateinit var attachmentsListView: RecyclerView
|
||||||
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
|
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
|
||||||
|
private lateinit var tagsContainerView: TextInputLayout
|
||||||
|
private lateinit var tagsCompletionView: TagsCompletionView
|
||||||
|
private var tagsAdapter: FilteredArrayAdapter<String>? = null
|
||||||
|
|
||||||
private var mTemplate: Template? = null
|
private var mTemplate: Template? = null
|
||||||
private var mAllowMultipleAttachments: Boolean = false
|
private var mAllowMultipleAttachments: Boolean = false
|
||||||
@@ -87,6 +91,8 @@ class EntryEditFragment: DatabaseFragment() {
|
|||||||
templateView = view.findViewById(R.id.template_view)
|
templateView = view.findViewById(R.id.template_view)
|
||||||
attachmentsContainerView = view.findViewById(R.id.entry_attachments_container)
|
attachmentsContainerView = view.findViewById(R.id.entry_attachments_container)
|
||||||
attachmentsListView = view.findViewById(R.id.entry_attachments_list)
|
attachmentsListView = view.findViewById(R.id.entry_attachments_list)
|
||||||
|
tagsContainerView = view.findViewById(R.id.entry_tags_label)
|
||||||
|
tagsCompletionView = view.findViewById(R.id.entry_tags_completion_view)
|
||||||
|
|
||||||
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
|
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
|
||||||
attachmentsListView.apply {
|
attachmentsListView.apply {
|
||||||
@@ -99,6 +105,12 @@ class EntryEditFragment: DatabaseFragment() {
|
|||||||
setOnIconClickListener {
|
setOnIconClickListener {
|
||||||
mEntryEditViewModel.requestIconSelection(templateView.getIcon())
|
mEntryEditViewModel.requestIconSelection(templateView.getIcon())
|
||||||
}
|
}
|
||||||
|
setOnBackgroundColorClickListener {
|
||||||
|
mEntryEditViewModel.requestBackgroundColorSelection(templateView.getBackgroundColor())
|
||||||
|
}
|
||||||
|
setOnForegroundColorClickListener {
|
||||||
|
mEntryEditViewModel.requestForegroundColorSelection(templateView.getForegroundColor())
|
||||||
|
}
|
||||||
setOnCustomEditionActionClickListener { field ->
|
setOnCustomEditionActionClickListener { field ->
|
||||||
mEntryEditViewModel.requestCustomFieldEdition(field)
|
mEntryEditViewModel.requestCustomFieldEdition(field)
|
||||||
}
|
}
|
||||||
@@ -140,13 +152,22 @@ class EntryEditFragment: DatabaseFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mEntryEditViewModel.requestEntryInfoUpdate.observe(viewLifecycleOwner) {
|
mEntryEditViewModel.requestEntryInfoUpdate.observe(viewLifecycleOwner) {
|
||||||
mEntryEditViewModel.saveEntryInfo(it.database, it.entry, it.parent, retrieveEntryInfo())
|
val entryInfo = retrieveEntryInfo()
|
||||||
|
mEntryEditViewModel.saveEntryInfo(it.database, it.entry, it.parent, entryInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
mEntryEditViewModel.onIconSelected.observe(viewLifecycleOwner) { iconImage ->
|
mEntryEditViewModel.onIconSelected.observe(viewLifecycleOwner) { iconImage ->
|
||||||
templateView.setIcon(iconImage)
|
templateView.setIcon(iconImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onBackgroundColorSelected.observe(viewLifecycleOwner) { color ->
|
||||||
|
templateView.setBackgroundColor(color)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onForegroundColorSelected.observe(viewLifecycleOwner) { color ->
|
||||||
|
templateView.setForegroundColor(color)
|
||||||
|
}
|
||||||
|
|
||||||
mEntryEditViewModel.onPasswordSelected.observe(viewLifecycleOwner) { passwordField ->
|
mEntryEditViewModel.onPasswordSelected.observe(viewLifecycleOwner) { passwordField ->
|
||||||
templateView.setPasswordField(passwordField)
|
templateView.setPasswordField(passwordField)
|
||||||
}
|
}
|
||||||
@@ -263,18 +284,34 @@ class EntryEditFragment: DatabaseFragment() {
|
|||||||
attachmentsContainerView.expand(true)
|
attachmentsContainerView.expand(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tagsAdapter = TagsProposalAdapter(requireContext(), database?.tagPool)
|
||||||
|
tagsCompletionView.apply {
|
||||||
|
threshold = 1
|
||||||
|
setAdapter(tagsAdapter)
|
||||||
|
}
|
||||||
|
tagsContainerView.visibility = if (database?.allowTags() == true) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assignEntryInfo(entryInfo: EntryInfo?) {
|
private fun assignEntryInfo(entryInfo: EntryInfo?) {
|
||||||
// Populate entry views
|
// Populate entry views
|
||||||
templateView.setEntryInfo(entryInfo)
|
templateView.setEntryInfo(entryInfo)
|
||||||
|
|
||||||
|
// Set Tags
|
||||||
|
entryInfo?.tags?.let { tags ->
|
||||||
|
tagsCompletionView.setText("")
|
||||||
|
for (i in 0 until tags.size()) {
|
||||||
|
tagsCompletionView.addObjectSync(tags.get(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Manage attachments
|
// Manage attachments
|
||||||
setAttachments(entryInfo?.attachments ?: listOf())
|
setAttachments(entryInfo?.attachments ?: listOf())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun retrieveEntryInfo(): EntryInfo {
|
private fun retrieveEntryInfo(): EntryInfo {
|
||||||
val entryInfo = templateView.getEntryInfo()
|
val entryInfo = templateView.getEntryInfo()
|
||||||
|
entryInfo.tags = tagsCompletionView.getTags()
|
||||||
entryInfo.attachments = getAttachments().toMutableList()
|
entryInfo.attachments = getAttachments().toMutableList()
|
||||||
return entryInfo
|
return entryInfo
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import com.kunzisoft.keepass.R
|
|||||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
|
||||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
@@ -25,6 +24,7 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
|||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
import com.kunzisoft.keepass.utils.UuidUtil
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
import com.kunzisoft.keepass.view.TemplateView
|
import com.kunzisoft.keepass.view.TemplateView
|
||||||
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
import com.kunzisoft.keepass.view.showByFading
|
import com.kunzisoft.keepass.view.showByFading
|
||||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -32,6 +32,9 @@ import java.util.*
|
|||||||
class EntryFragment: DatabaseFragment() {
|
class EntryFragment: DatabaseFragment() {
|
||||||
|
|
||||||
private lateinit var rootView: View
|
private lateinit var rootView: View
|
||||||
|
private lateinit var mainSection: View
|
||||||
|
private lateinit var advancedSection: View
|
||||||
|
|
||||||
private lateinit var templateView: TemplateView
|
private lateinit var templateView: TemplateView
|
||||||
|
|
||||||
private lateinit var creationDateView: TextView
|
private lateinit var creationDateView: TextView
|
||||||
@@ -41,8 +44,9 @@ class EntryFragment: DatabaseFragment() {
|
|||||||
private lateinit var attachmentsListView: RecyclerView
|
private lateinit var attachmentsListView: RecyclerView
|
||||||
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
|
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
|
||||||
|
|
||||||
|
private lateinit var customDataView: TextView
|
||||||
|
|
||||||
private lateinit var uuidContainerView: View
|
private lateinit var uuidContainerView: View
|
||||||
private lateinit var uuidView: TextView
|
|
||||||
private lateinit var uuidReferenceView: TextView
|
private lateinit var uuidReferenceView: TextView
|
||||||
|
|
||||||
private var mClipboardHelper: ClipboardHelper? = null
|
private var mClipboardHelper: ClipboardHelper? = null
|
||||||
@@ -71,6 +75,10 @@ class EntryFragment: DatabaseFragment() {
|
|||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
view.isVisible = false
|
view.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mainSection = view.findViewById(R.id.entry_section_main)
|
||||||
|
advancedSection = view.findViewById(R.id.entry_section_advanced)
|
||||||
|
|
||||||
templateView = view.findViewById(R.id.entry_template)
|
templateView = view.findViewById(R.id.entry_template)
|
||||||
loadTemplateSettings()
|
loadTemplateSettings()
|
||||||
|
|
||||||
@@ -84,11 +92,13 @@ class EntryFragment: DatabaseFragment() {
|
|||||||
creationDateView = view.findViewById(R.id.entry_created)
|
creationDateView = view.findViewById(R.id.entry_created)
|
||||||
modificationDateView = view.findViewById(R.id.entry_modified)
|
modificationDateView = view.findViewById(R.id.entry_modified)
|
||||||
|
|
||||||
|
// TODO Custom data
|
||||||
|
// customDataView = view.findViewById(R.id.entry_custom_data)
|
||||||
|
|
||||||
uuidContainerView = view.findViewById(R.id.entry_UUID_container)
|
uuidContainerView = view.findViewById(R.id.entry_UUID_container)
|
||||||
uuidContainerView.apply {
|
uuidContainerView.apply {
|
||||||
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
|
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
uuidView = view.findViewById(R.id.entry_UUID)
|
|
||||||
uuidReferenceView = view.findViewById(R.id.entry_UUID_reference)
|
uuidReferenceView = view.findViewById(R.id.entry_UUID_reference)
|
||||||
|
|
||||||
mEntryViewModel.entryInfoHistory.observe(viewLifecycleOwner) { entryInfoHistory ->
|
mEntryViewModel.entryInfoHistory.observe(viewLifecycleOwner) { entryInfoHistory ->
|
||||||
@@ -108,6 +118,19 @@ class EntryFragment: DatabaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mEntryViewModel.sectionSelected.observe(viewLifecycleOwner) { entrySection ->
|
||||||
|
when (entrySection ?: EntryViewModel.EntrySection.MAIN) {
|
||||||
|
EntryViewModel.EntrySection.MAIN -> {
|
||||||
|
mainSection.showByFading()
|
||||||
|
advancedSection.hideByFading()
|
||||||
|
}
|
||||||
|
EntryViewModel.EntrySection.ADVANCED -> {
|
||||||
|
mainSection.hideByFading()
|
||||||
|
advancedSection.showByFading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
@@ -156,11 +179,14 @@ class EntryFragment: DatabaseFragment() {
|
|||||||
assignAttachments(entryInfo?.attachments ?: listOf())
|
assignAttachments(entryInfo?.attachments ?: listOf())
|
||||||
|
|
||||||
// Assign dates
|
// Assign dates
|
||||||
assignCreationDate(entryInfo?.creationTime)
|
creationDateView.text = entryInfo?.creationTime?.getDateTimeString(resources)
|
||||||
assignModificationDate(entryInfo?.lastModificationTime)
|
modificationDateView.text = entryInfo?.lastModificationTime?.getDateTimeString(resources)
|
||||||
|
|
||||||
|
// TODO Custom data
|
||||||
|
// customDataView.text = entryInfo?.customData?.toString()
|
||||||
|
|
||||||
// Assign special data
|
// Assign special data
|
||||||
assignUUID(entryInfo?.id)
|
uuidReferenceView.text = UuidUtil.toHexString(entryInfo?.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showClipboardDialog() {
|
private fun showClipboardDialog() {
|
||||||
@@ -191,19 +217,6 @@ class EntryFragment: DatabaseFragment() {
|
|||||||
templateView.reload()
|
templateView.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assignCreationDate(date: DateInstant?) {
|
|
||||||
creationDateView.text = date?.getDateTimeString(resources)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun assignModificationDate(date: DateInstant?) {
|
|
||||||
modificationDateView.text = date?.getDateTimeString(resources)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun assignUUID(uuid: UUID?) {
|
|
||||||
uuidView.text = uuid?.toString()
|
|
||||||
uuidReferenceView.text = UuidUtil.toHexString(uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------
|
/* -------------
|
||||||
* Attachments
|
* Attachments
|
||||||
* -------------
|
* -------------
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
package com.kunzisoft.keepass.activities.fragments
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.*
|
import android.view.*
|
||||||
@@ -34,12 +33,11 @@ import com.kunzisoft.keepass.activities.EntryEditActivity
|
|||||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.adapters.NodeAdapter
|
import com.kunzisoft.keepass.adapters.NodesAdapter
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
@@ -50,10 +48,11 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
|
|
||||||
private var nodeClickListener: NodeClickListener? = null
|
private var nodeClickListener: NodeClickListener? = null
|
||||||
private var onScrollListener: OnScrollListener? = null
|
private var onScrollListener: OnScrollListener? = null
|
||||||
|
private var groupRefreshed: GroupRefreshedListener? = null
|
||||||
|
|
||||||
private var mNodesRecyclerView: RecyclerView? = null
|
private var mNodesRecyclerView: RecyclerView? = null
|
||||||
private var mLayoutManager: LinearLayoutManager? = null
|
private var mLayoutManager: LinearLayoutManager? = null
|
||||||
private var mAdapter: NodeAdapter? = null
|
private var mAdapter: NodesAdapter? = null
|
||||||
|
|
||||||
private val mGroupViewModel: GroupViewModel by activityViewModels()
|
private val mGroupViewModel: GroupViewModel by activityViewModels()
|
||||||
|
|
||||||
@@ -74,8 +73,18 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
private var mRecycleBinEnable: Boolean = false
|
private var mRecycleBinEnable: Boolean = false
|
||||||
private var mRecycleBin: Group? = null
|
private var mRecycleBin: Group? = null
|
||||||
|
|
||||||
val isEmpty: Boolean
|
var mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) { entryId ->
|
||||||
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
|
entryId?.let {
|
||||||
|
// Simply refresh the list
|
||||||
|
rebuildList()
|
||||||
|
// Scroll to the new entry
|
||||||
|
mDatabase?.getEntryById(it)?.let { entry ->
|
||||||
|
mAdapter?.indexOf(entry)?.let { position ->
|
||||||
|
mNodesRecyclerView?.scrollToPosition(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: Log.e(this.javaClass.name, "Entry cannot be retrieved in Activity Result")
|
||||||
|
}
|
||||||
|
|
||||||
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
|
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
|
||||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||||
@@ -92,12 +101,14 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
|
|
||||||
|
// TODO Change to ViewModel
|
||||||
try {
|
try {
|
||||||
nodeClickListener = context as NodeClickListener
|
nodeClickListener = context as NodeClickListener
|
||||||
} catch (e: ClassCastException) {
|
} catch (e: ClassCastException) {
|
||||||
// The activity doesn't implement the interface, throw exception
|
// The activity doesn't implement the interface, throw exception
|
||||||
throw ClassCastException(context.toString()
|
throw ClassCastException(context.toString()
|
||||||
+ " must implement " + NodeAdapter.NodeClickCallback::class.java.name)
|
+ " must implement " + NodesAdapter.NodeClickCallback::class.java.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -105,14 +116,24 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
} catch (e: ClassCastException) {
|
} catch (e: ClassCastException) {
|
||||||
onScrollListener = null
|
onScrollListener = null
|
||||||
// Context menu can be omit
|
// Context menu can be omit
|
||||||
Log.w(TAG, context.toString()
|
Log.w(
|
||||||
|
TAG, context.toString()
|
||||||
+ " must implement " + RecyclerView.OnScrollListener::class.java.name)
|
+ " must implement " + RecyclerView.OnScrollListener::class.java.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
groupRefreshed = context as GroupRefreshedListener
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
// The activity doesn't implement the interface, throw exception
|
||||||
|
throw ClassCastException(context.toString()
|
||||||
|
+ " must implement " + GroupRefreshedListener::class.java.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetach() {
|
override fun onDetach() {
|
||||||
nodeClickListener = null
|
nodeClickListener = null
|
||||||
onScrollListener = null
|
onScrollListener = null
|
||||||
|
groupRefreshed = null
|
||||||
super.onDetach()
|
super.onDetach()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,10 +149,11 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
|
|
||||||
contextThemed?.let { context ->
|
contextThemed?.let { context ->
|
||||||
database?.let { database ->
|
database?.let { database ->
|
||||||
mAdapter = NodeAdapter(context, database).apply {
|
mAdapter = NodesAdapter(context, database).apply {
|
||||||
setOnNodeClickListener(object : NodeAdapter.NodeClickCallback {
|
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
|
||||||
override fun onNodeClick(database: Database, node: Node) {
|
override fun onNodeClick(database: Database, node: Node) {
|
||||||
if (nodeActionSelectionMode) {
|
if (mCurrentGroup?.isVirtual == false
|
||||||
|
&& nodeActionSelectionMode) {
|
||||||
if (listActionNodes.contains(node)) {
|
if (listActionNodes.contains(node)) {
|
||||||
// Remove selected item if already selected
|
// Remove selected item if already selected
|
||||||
listActionNodes.remove(node)
|
listActionNodes.remove(node)
|
||||||
@@ -148,7 +170,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onNodeLongClick(database: Database, node: Node): Boolean {
|
override fun onNodeLongClick(database: Database, node: Node): Boolean {
|
||||||
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
|
if (mCurrentGroup?.isVirtual == false
|
||||||
|
&& 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))
|
||||||
listActionNodes.add(node)
|
listActionNodes.add(node)
|
||||||
@@ -185,7 +208,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
super.onCreateView(inflater, container, savedInstanceState)
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
// To apply theme
|
// To apply theme
|
||||||
return inflater.cloneInContext(contextThemed)
|
return inflater.cloneInContext(contextThemed)
|
||||||
.inflate(R.layout.fragment_group, container, false)
|
.inflate(R.layout.fragment_nodes, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
@@ -236,9 +259,9 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
private fun rebuildList() {
|
private fun rebuildList() {
|
||||||
try {
|
try {
|
||||||
// Add elements to the list
|
// Add elements to the list
|
||||||
mCurrentGroup?.let { mainGroup ->
|
mCurrentGroup?.let { currentGroup ->
|
||||||
// Thrown an exception when sort cannot be performed
|
// Thrown an exception when sort cannot be performed
|
||||||
mAdapter?.rebuildList(mainGroup)
|
mAdapter?.rebuildList(currentGroup)
|
||||||
}
|
}
|
||||||
} catch (e:Exception) {
|
} catch (e:Exception) {
|
||||||
Log.e(TAG, "Unable to rebuild the list", e)
|
Log.e(TAG, "Unable to rebuild the list", e)
|
||||||
@@ -250,6 +273,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
} else {
|
} else {
|
||||||
notFoundView?.visibility = View.GONE
|
notFoundView?.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
groupRefreshed?.onGroupRefreshed()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSortSelected(sortNodeEnum: SortNodeEnum,
|
override fun onSortSelected(sortNodeEnum: SortNodeEnum,
|
||||||
@@ -282,15 +307,17 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
val sortDialogFragment: SortDialogFragment =
|
val sortDialogFragment: SortDialogFragment =
|
||||||
if (mRecycleBinEnable) {
|
if (mRecycleBinEnable) {
|
||||||
SortDialogFragment.getInstance(
|
SortDialogFragment.getInstance(
|
||||||
PreferencesUtil.getListSort(context),
|
PreferencesUtil.getListSort(context),
|
||||||
PreferencesUtil.getAscendingSort(context),
|
PreferencesUtil.getAscendingSort(context),
|
||||||
PreferencesUtil.getGroupsBeforeSort(context),
|
PreferencesUtil.getGroupsBeforeSort(context),
|
||||||
PreferencesUtil.getRecycleBinBottomSort(context))
|
PreferencesUtil.getRecycleBinBottomSort(context)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
SortDialogFragment.getInstance(
|
SortDialogFragment.getInstance(
|
||||||
PreferencesUtil.getListSort(context),
|
PreferencesUtil.getListSort(context),
|
||||||
PreferencesUtil.getAscendingSort(context),
|
PreferencesUtil.getAscendingSort(context),
|
||||||
PreferencesUtil.getGroupsBeforeSort(context))
|
PreferencesUtil.getGroupsBeforeSort(context)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
||||||
@@ -402,27 +429,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
|
|
||||||
when (requestCode) {
|
|
||||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
|
||||||
if (resultCode == EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE) {
|
|
||||||
data?.getParcelableExtra<NodeId<UUID>>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let {
|
|
||||||
// Simply refresh the list
|
|
||||||
rebuildList()
|
|
||||||
// Scroll to the new entry
|
|
||||||
mDatabase?.getEntryById(it)?.let { entry ->
|
|
||||||
mAdapter?.indexOf(entry)?.let { position ->
|
|
||||||
mNodesRecyclerView?.scrollToPosition(position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: Log.e(this.javaClass.name, "Entry cannot be retrieved in Activity Result")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
@@ -458,6 +464,10 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
fun onScrolled(dy: Int)
|
fun onScrolled(dy: Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GroupRefreshedListener {
|
||||||
|
fun onGroupRefreshed()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = GroupFragment::class.java.name
|
private val TAG = GroupFragment::class.java.name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,8 +55,10 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
|
|||||||
iconCustomAdded?.iconCustom?.let { icon ->
|
iconCustomAdded?.iconCustom?.let { icon ->
|
||||||
iconPickerAdapter.addIcon(icon)
|
iconPickerAdapter.addIcon(icon)
|
||||||
iconCustomAdded.iconCustom = null
|
iconCustomAdded.iconCustom = null
|
||||||
|
try {
|
||||||
|
iconsGridView.smoothScrollToPosition(iconPickerAdapter.lastPosition)
|
||||||
|
} catch (ignore: Exception) {}
|
||||||
}
|
}
|
||||||
iconsGridView.smoothScrollToPosition(iconPickerAdapter.lastPosition)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
iconPickerViewModel.customIconRemoved.observe(viewLifecycleOwner) { iconCustomRemoved ->
|
iconPickerViewModel.customIconRemoved.observe(viewLifecycleOwner) { iconCustomRemoved ->
|
||||||
@@ -67,6 +69,14 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
iconPickerViewModel.customIconUpdated.observe(viewLifecycleOwner) { iconCustomUpdated ->
|
||||||
|
if (!iconCustomUpdated.error) {
|
||||||
|
iconCustomUpdated?.iconCustom?.let { icon ->
|
||||||
|
iconPickerAdapter.updateIcon(icon)
|
||||||
|
iconCustomUpdated.iconCustom = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIconClickListener(icon: IconImageCustom) {
|
override fun onIconClickListener(icon: IconImageCustom) {
|
||||||
|
|||||||
@@ -26,14 +26,14 @@ class IconPickerFragment : DatabaseFragment() {
|
|||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
return inflater.inflate(R.layout.fragment_icon_picker, container, false)
|
return inflater.inflate(R.layout.fragment_tabs_pagination, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
viewPager = view.findViewById(R.id.icon_picker_pager)
|
viewPager = view.findViewById(R.id.tabs_view_pager)
|
||||||
tabLayout = view.findViewById(R.id.icon_picker_tabs)
|
tabLayout = view.findViewById(R.id.tabs_layout)
|
||||||
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||||
|
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
|
import com.google.android.material.tabs.TabLayout
|
||||||
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.adapters.KeyGeneratorPagerAdapter
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
||||||
|
|
||||||
|
class KeyGeneratorFragment : DatabaseFragment() {
|
||||||
|
|
||||||
|
private var keyGeneratorPagerAdapter: KeyGeneratorPagerAdapter? = null
|
||||||
|
private lateinit var viewPager: ViewPager2
|
||||||
|
private lateinit var tabLayout: TabLayout
|
||||||
|
|
||||||
|
private val mKeyGeneratorViewModel: KeyGeneratorViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private var mSelectedTab = KeyGeneratorTab.PASSWORD
|
||||||
|
private var mOnPageChangeCallback: ViewPager2.OnPageChangeCallback = object:
|
||||||
|
ViewPager2.OnPageChangeCallback() {
|
||||||
|
override fun onPageSelected(position: Int) {
|
||||||
|
super.onPageSelected(position)
|
||||||
|
mSelectedTab = KeyGeneratorTab.getKeyGeneratorTabByPosition(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
return inflater.inflate(R.layout.fragment_tabs_pagination, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
keyGeneratorPagerAdapter = KeyGeneratorPagerAdapter(this, )
|
||||||
|
viewPager = view.findViewById(R.id.tabs_view_pager)
|
||||||
|
tabLayout = view.findViewById(R.id.tabs_layout)
|
||||||
|
viewPager.adapter = keyGeneratorPagerAdapter
|
||||||
|
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
|
||||||
|
tab.text = getString(KeyGeneratorTab.getKeyGeneratorTabByPosition(position).stringId)
|
||||||
|
}.attach()
|
||||||
|
viewPager.registerOnPageChangeCallback(mOnPageChangeCallback)
|
||||||
|
|
||||||
|
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||||
|
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(PASSWORD_TAB_ARG)) {
|
||||||
|
viewPager.currentItem = getInt(PASSWORD_TAB_ARG)
|
||||||
|
}
|
||||||
|
remove(PASSWORD_TAB_ARG)
|
||||||
|
}
|
||||||
|
|
||||||
|
mKeyGeneratorViewModel.requireKeyGeneration.observe(viewLifecycleOwner) {
|
||||||
|
when (mSelectedTab) {
|
||||||
|
KeyGeneratorTab.PASSWORD -> {
|
||||||
|
mKeyGeneratorViewModel.requirePasswordGeneration()
|
||||||
|
}
|
||||||
|
KeyGeneratorTab.PASSPHRASE -> {
|
||||||
|
mKeyGeneratorViewModel.requirePassphraseGeneration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mKeyGeneratorViewModel.keyGeneratedValidated.observe(viewLifecycleOwner) {
|
||||||
|
when (mSelectedTab) {
|
||||||
|
KeyGeneratorTab.PASSWORD -> {
|
||||||
|
mKeyGeneratorViewModel.validatePasswordGenerated()
|
||||||
|
}
|
||||||
|
KeyGeneratorTab.PASSPHRASE -> {
|
||||||
|
mKeyGeneratorViewModel.validatePassphraseGenerated()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
viewPager.unregisterOnPageChangeCallback(mOnPageChangeCallback)
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
// Nothing here
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class KeyGeneratorTab(@StringRes val stringId: Int) {
|
||||||
|
PASSWORD(R.string.password), PASSPHRASE(R.string.passphrase);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getKeyGeneratorTabByPosition(position: Int): KeyGeneratorTab {
|
||||||
|
return when (position) {
|
||||||
|
0 -> PASSWORD
|
||||||
|
else -> PASSPHRASE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val PASSWORD_TAB_ARG = "PASSWORD_TAB_ARG"
|
||||||
|
|
||||||
|
fun getInstance(keyGeneratorTab: KeyGeneratorTab): KeyGeneratorFragment {
|
||||||
|
val fragment = KeyGeneratorFragment()
|
||||||
|
fragment.arguments = Bundle().apply {
|
||||||
|
putInt(PASSWORD_TAB_ARG, keyGeneratorTab.ordinal)
|
||||||
|
}
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.*
|
||||||
|
import androidx.core.widget.doOnTextChanged
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import com.google.android.material.slider.Slider
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.password.PassphraseGenerator
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
|
import com.kunzisoft.keepass.view.PassKeyView
|
||||||
|
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
||||||
|
|
||||||
|
class PassphraseGeneratorFragment : DatabaseFragment() {
|
||||||
|
|
||||||
|
private lateinit var passKeyView: PassKeyView
|
||||||
|
|
||||||
|
private lateinit var sliderWordCount: Slider
|
||||||
|
private lateinit var wordCountText: EditText
|
||||||
|
private lateinit var charactersCountText: TextView
|
||||||
|
private lateinit var wordSeparator: EditText
|
||||||
|
private lateinit var wordCaseSpinner: Spinner
|
||||||
|
|
||||||
|
private var minSliderWordCount: Int = 0
|
||||||
|
private var maxSliderWordCount: Int = 0
|
||||||
|
private var wordCaseAdapter: ArrayAdapter<String>? = null
|
||||||
|
|
||||||
|
private val mKeyGeneratorViewModel: KeyGeneratorViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?): View {
|
||||||
|
return inflater.inflate(R.layout.fragment_generate_passphrase, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
passKeyView = view.findViewById(R.id.passphrase_view)
|
||||||
|
val passphraseCopyView: ImageView? = view.findViewById(R.id.passphrase_copy_button)
|
||||||
|
sliderWordCount = view.findViewById(R.id.slider_word_count)
|
||||||
|
wordCountText = view.findViewById(R.id.word_count)
|
||||||
|
charactersCountText = view.findViewById(R.id.character_count)
|
||||||
|
wordSeparator = view.findViewById(R.id.word_separator)
|
||||||
|
wordCaseSpinner = view.findViewById(R.id.word_case)
|
||||||
|
|
||||||
|
minSliderWordCount = resources.getInteger(R.integer.passphrase_generator_word_count_min)
|
||||||
|
maxSliderWordCount = resources.getInteger(R.integer.passphrase_generator_word_count_max)
|
||||||
|
|
||||||
|
contextThemed?.let { context ->
|
||||||
|
passphraseCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(context))
|
||||||
|
View.VISIBLE else View.GONE
|
||||||
|
val clipboardHelper = ClipboardHelper(context)
|
||||||
|
passphraseCopyView?.setOnClickListener {
|
||||||
|
clipboardHelper.timeoutCopyToClipboard(passKeyView.passwordString,
|
||||||
|
getString(R.string.copy_field,
|
||||||
|
getString(R.string.entry_password)))
|
||||||
|
}
|
||||||
|
|
||||||
|
wordCaseAdapter = ArrayAdapter(context,
|
||||||
|
android.R.layout.simple_spinner_item, resources.getStringArray(R.array.word_case_array)).apply {
|
||||||
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
|
}
|
||||||
|
wordCaseSpinner.adapter = wordCaseAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSettings()
|
||||||
|
|
||||||
|
var listenSlider = true
|
||||||
|
var listenEditText = true
|
||||||
|
sliderWordCount.addOnChangeListener { _, value, _ ->
|
||||||
|
try {
|
||||||
|
listenEditText = false
|
||||||
|
if (listenSlider) {
|
||||||
|
wordCountText.setText(value.toInt().toString())
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to set the word count value", e)
|
||||||
|
} finally {
|
||||||
|
listenEditText = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sliderWordCount.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
|
||||||
|
// TODO upgrade material-components lib
|
||||||
|
// https://stackoverflow.com/questions/70873160/material-slider-onslidertouchlisteners-methods-can-only-be-called-from-within-t
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
override fun onStartTrackingTouch(slider: Slider) {}
|
||||||
|
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
override fun onStopTrackingTouch(slider: Slider) {
|
||||||
|
generatePassphrase()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
wordCountText.doOnTextChanged { _, _, _, _ ->
|
||||||
|
if (listenEditText) {
|
||||||
|
try {
|
||||||
|
listenSlider = false
|
||||||
|
setSliderValue(getWordCount())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to get the word count value", e)
|
||||||
|
} finally {
|
||||||
|
listenSlider = true
|
||||||
|
generatePassphrase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wordSeparator.doOnTextChanged { _, _, _, _ ->
|
||||||
|
generatePassphrase()
|
||||||
|
}
|
||||||
|
wordCaseSpinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||||
|
generatePassphrase()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
generatePassphrase()
|
||||||
|
|
||||||
|
mKeyGeneratorViewModel.passphraseGeneratedValidated.observe(viewLifecycleOwner) {
|
||||||
|
mKeyGeneratorViewModel.setKeyGenerated(passKeyView.passwordString)
|
||||||
|
}
|
||||||
|
|
||||||
|
mKeyGeneratorViewModel.requirePassphraseGeneration.observe(viewLifecycleOwner) {
|
||||||
|
generatePassphrase()
|
||||||
|
}
|
||||||
|
|
||||||
|
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getWordCount(): Int {
|
||||||
|
return try {
|
||||||
|
Integer.valueOf(wordCountText.text.toString())
|
||||||
|
} catch (numberException: NumberFormatException) {
|
||||||
|
minSliderWordCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setWordCount(wordCount: Int) {
|
||||||
|
setSliderValue(wordCount)
|
||||||
|
wordCountText.setText(wordCount.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setSliderValue(value: Int) {
|
||||||
|
when {
|
||||||
|
value < minSliderWordCount -> {
|
||||||
|
sliderWordCount.value = minSliderWordCount.toFloat()
|
||||||
|
}
|
||||||
|
value > maxSliderWordCount -> {
|
||||||
|
sliderWordCount.value = maxSliderWordCount.toFloat()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
sliderWordCount.value = value.toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getWordSeparator(): String {
|
||||||
|
return wordSeparator.text.toString().ifEmpty { " " }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getWordCase(): PassphraseGenerator.WordCase {
|
||||||
|
var wordCase = PassphraseGenerator.WordCase.LOWER_CASE
|
||||||
|
try {
|
||||||
|
wordCase = PassphraseGenerator.WordCase.getByOrdinal(wordCaseSpinner.selectedItemPosition)
|
||||||
|
} catch (caseException: Exception) {
|
||||||
|
Log.e(TAG, "Unable to retrieve the word case", caseException)
|
||||||
|
}
|
||||||
|
return wordCase
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setWordCase(wordCase: PassphraseGenerator.WordCase) {
|
||||||
|
wordCaseSpinner.setSelection(wordCase.ordinal)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSeparator(): String {
|
||||||
|
return wordSeparator.text?.toString() ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setSeparator(separator: String) {
|
||||||
|
wordSeparator.setText(separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generatePassphrase() {
|
||||||
|
var passphrase = ""
|
||||||
|
try {
|
||||||
|
passphrase = PassphraseGenerator().generatePassphrase(
|
||||||
|
getWordCount(),
|
||||||
|
getWordSeparator(),
|
||||||
|
getWordCase())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to generate a passphrase", e)
|
||||||
|
}
|
||||||
|
passKeyView.passwordString = passphrase
|
||||||
|
charactersCountText.text = getString(R.string.character_count, passphrase.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
saveSettings()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveSettings() {
|
||||||
|
context?.let { context ->
|
||||||
|
PreferencesUtil.setDefaultPassphraseWordCount(context, getWordCount())
|
||||||
|
PreferencesUtil.setDefaultPassphraseWordCase(context, getWordCase())
|
||||||
|
PreferencesUtil.setDefaultPassphraseSeparator(context, getSeparator())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadSettings() {
|
||||||
|
context?.let { context ->
|
||||||
|
setWordCount(PreferencesUtil.getDefaultPassphraseWordCount(context))
|
||||||
|
setWordCase(PreferencesUtil.getDefaultPassphraseWordCase(context))
|
||||||
|
setSeparator(PreferencesUtil.getDefaultPassphraseSeparator(context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
// Nothing here
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "PassphraseGnrtrFrgmt"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.core.widget.doOnTextChanged
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import com.google.android.material.slider.Slider
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.password.PasswordGenerator
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
|
import com.kunzisoft.keepass.view.PassKeyView
|
||||||
|
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
||||||
|
|
||||||
|
class PasswordGeneratorFragment : DatabaseFragment() {
|
||||||
|
|
||||||
|
private lateinit var passKeyView: PassKeyView
|
||||||
|
|
||||||
|
private lateinit var sliderLength: Slider
|
||||||
|
private lateinit var lengthEditView: EditText
|
||||||
|
|
||||||
|
private lateinit var uppercaseCompound: CompoundButton
|
||||||
|
private lateinit var lowercaseCompound: CompoundButton
|
||||||
|
private lateinit var digitsCompound: CompoundButton
|
||||||
|
private lateinit var minusCompound: CompoundButton
|
||||||
|
private lateinit var underlineCompound: CompoundButton
|
||||||
|
private lateinit var spaceCompound: CompoundButton
|
||||||
|
private lateinit var specialsCompound: CompoundButton
|
||||||
|
private lateinit var bracketsCompound: CompoundButton
|
||||||
|
private lateinit var extendedCompound: CompoundButton
|
||||||
|
private lateinit var considerCharsEditText: EditText
|
||||||
|
private lateinit var ignoreCharsEditText: EditText
|
||||||
|
private lateinit var atLeastOneCompound: CompoundButton
|
||||||
|
private lateinit var excludeAmbiguousCompound: CompoundButton
|
||||||
|
|
||||||
|
private var minLengthSlider: Int = 0
|
||||||
|
private var maxLengthSlider: Int = 0
|
||||||
|
|
||||||
|
private val mKeyGeneratorViewModel: KeyGeneratorViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?): View {
|
||||||
|
return inflater.inflate(R.layout.fragment_generate_password, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
passKeyView = view.findViewById(R.id.password_view)
|
||||||
|
val passwordCopyView: ImageView? = view.findViewById(R.id.password_copy_button)
|
||||||
|
|
||||||
|
sliderLength = view.findViewById(R.id.slider_length)
|
||||||
|
lengthEditView = view.findViewById(R.id.length)
|
||||||
|
|
||||||
|
uppercaseCompound = view.findViewById(R.id.upperCase_filter)
|
||||||
|
lowercaseCompound = view.findViewById(R.id.lowerCase_filter)
|
||||||
|
digitsCompound = view.findViewById(R.id.digits_filter)
|
||||||
|
minusCompound = view.findViewById(R.id.minus_filter)
|
||||||
|
underlineCompound = view.findViewById(R.id.underline_filter)
|
||||||
|
spaceCompound = view.findViewById(R.id.space_filter)
|
||||||
|
specialsCompound = view.findViewById(R.id.special_filter)
|
||||||
|
bracketsCompound = view.findViewById(R.id.brackets_filter)
|
||||||
|
extendedCompound = view.findViewById(R.id.extendedASCII_filter)
|
||||||
|
considerCharsEditText = view.findViewById(R.id.consider_chars_filter)
|
||||||
|
ignoreCharsEditText = view.findViewById(R.id.ignore_chars_filter)
|
||||||
|
atLeastOneCompound = view.findViewById(R.id.atLeastOne_filter)
|
||||||
|
excludeAmbiguousCompound = view.findViewById(R.id.excludeAmbiguous_filter)
|
||||||
|
|
||||||
|
contextThemed?.let { context ->
|
||||||
|
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(context))
|
||||||
|
View.VISIBLE else View.GONE
|
||||||
|
val clipboardHelper = ClipboardHelper(context)
|
||||||
|
passwordCopyView?.setOnClickListener {
|
||||||
|
clipboardHelper.timeoutCopyToClipboard(passKeyView.passwordString,
|
||||||
|
getString(R.string.copy_field,
|
||||||
|
getString(R.string.entry_password)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
minLengthSlider = resources.getInteger(R.integer.password_generator_length_min)
|
||||||
|
maxLengthSlider = resources.getInteger(R.integer.password_generator_length_max)
|
||||||
|
|
||||||
|
loadSettings()
|
||||||
|
|
||||||
|
uppercaseCompound.setOnCheckedChangeListener { _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
lowercaseCompound.setOnCheckedChangeListener { _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
digitsCompound.setOnCheckedChangeListener { _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
minusCompound.setOnCheckedChangeListener { _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
underlineCompound.setOnCheckedChangeListener { _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
spaceCompound.setOnCheckedChangeListener { _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
specialsCompound.setOnCheckedChangeListener { _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
bracketsCompound.setOnCheckedChangeListener { _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
extendedCompound.setOnCheckedChangeListener { _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
considerCharsEditText.doOnTextChanged { _, _, _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
ignoreCharsEditText.doOnTextChanged { _, _, _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
atLeastOneCompound.setOnCheckedChangeListener { _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
excludeAmbiguousCompound.setOnCheckedChangeListener { _, _ ->
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
|
||||||
|
var listenSlider = true
|
||||||
|
var listenEditText = true
|
||||||
|
sliderLength.addOnChangeListener { _, value, _ ->
|
||||||
|
try {
|
||||||
|
listenEditText = false
|
||||||
|
if (listenSlider) {
|
||||||
|
lengthEditView.setText(value.toInt().toString())
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to set the length value", e)
|
||||||
|
} finally {
|
||||||
|
listenEditText = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sliderLength.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
|
||||||
|
// TODO upgrade material-components lib
|
||||||
|
// https://stackoverflow.com/questions/70873160/material-slider-onslidertouchlisteners-methods-can-only-be-called-from-within-t
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
override fun onStartTrackingTouch(slider: Slider) {}
|
||||||
|
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
override fun onStopTrackingTouch(slider: Slider) {
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
lengthEditView.doOnTextChanged { _, _, _, _ ->
|
||||||
|
if (listenEditText) {
|
||||||
|
try {
|
||||||
|
listenSlider = false
|
||||||
|
setSliderValue(getPasswordLength())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to get the length value", e)
|
||||||
|
} finally {
|
||||||
|
listenSlider = true
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-populate a password to possibly save the user a few clicks
|
||||||
|
generatePassword()
|
||||||
|
|
||||||
|
mKeyGeneratorViewModel.passwordGeneratedValidated.observe(viewLifecycleOwner) {
|
||||||
|
mKeyGeneratorViewModel.setKeyGenerated(passKeyView.passwordString)
|
||||||
|
}
|
||||||
|
|
||||||
|
mKeyGeneratorViewModel.requirePasswordGeneration.observe(viewLifecycleOwner) {
|
||||||
|
generatePassword()
|
||||||
|
}
|
||||||
|
|
||||||
|
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPasswordLength(): Int {
|
||||||
|
return try {
|
||||||
|
Integer.valueOf(lengthEditView.text.toString())
|
||||||
|
} catch (numberException: NumberFormatException) {
|
||||||
|
minLengthSlider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setPasswordLength(passwordLength: Int) {
|
||||||
|
setSliderValue(passwordLength)
|
||||||
|
lengthEditView.setText(passwordLength.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getOptions(): Set<String> {
|
||||||
|
val optionsSet = mutableSetOf<String>()
|
||||||
|
if (uppercaseCompound.isChecked)
|
||||||
|
optionsSet.add(getString(R.string.value_password_uppercase))
|
||||||
|
if (lowercaseCompound.isChecked)
|
||||||
|
optionsSet.add(getString(R.string.value_password_lowercase))
|
||||||
|
if (digitsCompound.isChecked)
|
||||||
|
optionsSet.add(getString(R.string.value_password_digits))
|
||||||
|
if (minusCompound.isChecked)
|
||||||
|
optionsSet.add(getString(R.string.value_password_minus))
|
||||||
|
if (underlineCompound.isChecked)
|
||||||
|
optionsSet.add(getString(R.string.value_password_underline))
|
||||||
|
if (spaceCompound.isChecked)
|
||||||
|
optionsSet.add(getString(R.string.value_password_space))
|
||||||
|
if (specialsCompound.isChecked)
|
||||||
|
optionsSet.add(getString(R.string.value_password_special))
|
||||||
|
if (bracketsCompound.isChecked)
|
||||||
|
optionsSet.add(getString(R.string.value_password_brackets))
|
||||||
|
if (extendedCompound.isChecked)
|
||||||
|
optionsSet.add(getString(R.string.value_password_extended))
|
||||||
|
if (atLeastOneCompound.isChecked)
|
||||||
|
optionsSet.add(getString(R.string.value_password_atLeastOne))
|
||||||
|
if (excludeAmbiguousCompound.isChecked)
|
||||||
|
optionsSet.add(getString(R.string.value_password_excludeAmbiguous))
|
||||||
|
return optionsSet
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setOptions(options: Set<String>) {
|
||||||
|
uppercaseCompound.isChecked = false
|
||||||
|
lowercaseCompound.isChecked = false
|
||||||
|
digitsCompound.isChecked = false
|
||||||
|
minusCompound.isChecked = false
|
||||||
|
underlineCompound.isChecked = false
|
||||||
|
spaceCompound.isChecked = false
|
||||||
|
specialsCompound.isChecked = false
|
||||||
|
bracketsCompound.isChecked = false
|
||||||
|
extendedCompound.isChecked = false
|
||||||
|
atLeastOneCompound.isChecked = false
|
||||||
|
excludeAmbiguousCompound.isChecked = false
|
||||||
|
for (option in options) {
|
||||||
|
when (option) {
|
||||||
|
getString(R.string.value_password_uppercase) -> uppercaseCompound.isChecked = true
|
||||||
|
getString(R.string.value_password_lowercase) -> lowercaseCompound.isChecked = true
|
||||||
|
getString(R.string.value_password_digits) -> digitsCompound.isChecked = true
|
||||||
|
getString(R.string.value_password_minus) -> minusCompound.isChecked = true
|
||||||
|
getString(R.string.value_password_underline) -> underlineCompound.isChecked = true
|
||||||
|
getString(R.string.value_password_space) -> spaceCompound.isChecked = true
|
||||||
|
getString(R.string.value_password_special) -> specialsCompound.isChecked = true
|
||||||
|
getString(R.string.value_password_brackets) -> bracketsCompound.isChecked = true
|
||||||
|
getString(R.string.value_password_extended) -> extendedCompound.isChecked = true
|
||||||
|
getString(R.string.value_password_atLeastOne) -> atLeastOneCompound.isChecked = true
|
||||||
|
getString(R.string.value_password_excludeAmbiguous) -> excludeAmbiguousCompound.isChecked = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getConsiderChars(): String {
|
||||||
|
return considerCharsEditText.text.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setConsiderChars(chars: String) {
|
||||||
|
considerCharsEditText.setText(chars)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getIgnoreChars(): String {
|
||||||
|
return ignoreCharsEditText.text.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setIgnoreChars(chars: String) {
|
||||||
|
ignoreCharsEditText.setText(chars)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generatePassword() {
|
||||||
|
var password = ""
|
||||||
|
try {
|
||||||
|
password = PasswordGenerator(resources).generatePassword(getPasswordLength(),
|
||||||
|
uppercaseCompound.isChecked,
|
||||||
|
lowercaseCompound.isChecked,
|
||||||
|
digitsCompound.isChecked,
|
||||||
|
minusCompound.isChecked,
|
||||||
|
underlineCompound.isChecked,
|
||||||
|
spaceCompound.isChecked,
|
||||||
|
specialsCompound.isChecked,
|
||||||
|
bracketsCompound.isChecked,
|
||||||
|
extendedCompound.isChecked,
|
||||||
|
getConsiderChars(),
|
||||||
|
getIgnoreChars(),
|
||||||
|
atLeastOneCompound.isChecked,
|
||||||
|
excludeAmbiguousCompound.isChecked)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to generate a password", e)
|
||||||
|
}
|
||||||
|
passKeyView.passwordString = password
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
saveSettings()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
// Nothing here
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveSettings() {
|
||||||
|
context?.let { context ->
|
||||||
|
PreferencesUtil.setDefaultPasswordOptions(context, getOptions())
|
||||||
|
PreferencesUtil.setDefaultPasswordLength(context, getPasswordLength())
|
||||||
|
PreferencesUtil.setDefaultPasswordConsiderChars(context, getConsiderChars())
|
||||||
|
PreferencesUtil.setDefaultPasswordIgnoreChars(context, getIgnoreChars())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadSettings() {
|
||||||
|
context?.let { context ->
|
||||||
|
setOptions(PreferencesUtil.getDefaultPasswordOptions(context))
|
||||||
|
setPasswordLength(PreferencesUtil.getDefaultPasswordLength(context))
|
||||||
|
setConsiderChars(PreferencesUtil.getDefaultPasswordConsiderChars(context))
|
||||||
|
setIgnoreChars(PreferencesUtil.getDefaultPasswordIgnoreChars(context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setSliderValue(value: Int) {
|
||||||
|
when {
|
||||||
|
value < minLengthSlider -> {
|
||||||
|
sliderLength.value = minLengthSlider.toFloat()
|
||||||
|
}
|
||||||
|
value > maxLengthSlider -> {
|
||||||
|
sliderLength.value = maxLengthSlider.toFloat()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
sliderLength.value = value.toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "PasswordGeneratorFrgmt"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,7 +85,7 @@ object EntrySelectionHelper {
|
|||||||
return intent.getParcelableExtra(KEY_SEARCH_INFO)
|
return intent.getParcelableExtra(KEY_SEARCH_INFO)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
|
private fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
|
||||||
registerInfo?.let {
|
registerInfo?.let {
|
||||||
intent.putExtra(KEY_REGISTER_INFO, it)
|
intent.putExtra(KEY_REGISTER_INFO, it)
|
||||||
}
|
}
|
||||||
@@ -113,7 +113,7 @@ object EntrySelectionHelper {
|
|||||||
?: SpecialMode.DEFAULT
|
?: SpecialMode.DEFAULT
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
|
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
|
||||||
intent.putExtra(KEY_TYPE_MODE, typeMode as Serializable)
|
intent.putExtra(KEY_TYPE_MODE, typeMode as Serializable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,14 +20,16 @@
|
|||||||
package com.kunzisoft.keepass.activities.helpers
|
package com.kunzisoft.keepass.activities.helpers
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity.RESULT_OK
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
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.View
|
import android.view.View
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.activity.result.ActivityResultCallback
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
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.FileManagerDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
||||||
@@ -38,6 +40,10 @@ class ExternalFileHelper {
|
|||||||
private var activity: FragmentActivity? = null
|
private var activity: FragmentActivity? = null
|
||||||
private var fragment: Fragment? = null
|
private var fragment: Fragment? = null
|
||||||
|
|
||||||
|
private var getContentResultLauncher: ActivityResultLauncher<String>? = null
|
||||||
|
private var openDocumentResultLauncher: ActivityResultLauncher<Array<String>>? = null
|
||||||
|
private var createDocumentResultLauncher: ActivityResultLauncher<String>? = null
|
||||||
|
|
||||||
constructor(context: FragmentActivity) {
|
constructor(context: FragmentActivity) {
|
||||||
this.activity = context
|
this.activity = context
|
||||||
this.fragment = null
|
this.fragment = null
|
||||||
@@ -48,94 +54,81 @@ class ExternalFileHelper {
|
|||||||
this.fragment = context
|
this.fragment = context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun buildOpenDocument(onFileSelected: ((uri: Uri?) -> Unit)?) {
|
||||||
|
|
||||||
|
val resultCallback = ActivityResultCallback<Uri> { result ->
|
||||||
|
result?.let { uri ->
|
||||||
|
UriUtil.takeUriPermission(activity?.contentResolver, uri)
|
||||||
|
onFileSelected?.invoke(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getContentResultLauncher = if (fragment != null) {
|
||||||
|
fragment?.registerForActivityResult(
|
||||||
|
GetContent(),
|
||||||
|
resultCallback
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
activity?.registerForActivityResult(
|
||||||
|
GetContent(),
|
||||||
|
resultCallback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
openDocumentResultLauncher = if (fragment != null) {
|
||||||
|
fragment?.registerForActivityResult(
|
||||||
|
OpenDocument(),
|
||||||
|
resultCallback
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
activity?.registerForActivityResult(
|
||||||
|
OpenDocument(),
|
||||||
|
resultCallback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildCreateDocument(typeString: String = "application/octet-stream",
|
||||||
|
onFileCreated: (fileCreated: Uri?)->Unit) {
|
||||||
|
|
||||||
|
val resultCallback = ActivityResultCallback<Uri> { result ->
|
||||||
|
onFileCreated.invoke(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
createDocumentResultLauncher = if (fragment != null) {
|
||||||
|
fragment?.registerForActivityResult(
|
||||||
|
CreateDocument(typeString),
|
||||||
|
resultCallback
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
activity?.registerForActivityResult(
|
||||||
|
CreateDocument(typeString),
|
||||||
|
resultCallback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun openDocument(getContent: Boolean = false,
|
fun openDocument(getContent: Boolean = false,
|
||||||
typeString: String = "*/*") {
|
typeString: String = "*/*") {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
try {
|
||||||
try {
|
if (getContent) {
|
||||||
if (getContent) {
|
getContentResultLauncher?.launch(typeString)
|
||||||
openActivityWithActionGetContent(typeString)
|
} else {
|
||||||
} else {
|
openDocumentResultLauncher?.launch(arrayOf(typeString))
|
||||||
openActivityWithActionOpenDocument(typeString)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to open document", e)
|
|
||||||
showFileManagerDialogFragment()
|
|
||||||
}
|
}
|
||||||
} else {
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to open document", e)
|
||||||
showFileManagerDialogFragment()
|
showFileManagerDialogFragment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
fun createDocument(titleString: String) {
|
||||||
private fun openActivityWithActionOpenDocument(typeString: String) {
|
try {
|
||||||
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
createDocumentResultLauncher?.launch(titleString)
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
} catch (e: Exception) {
|
||||||
type = typeString
|
Log.e(TAG, "Unable to create document", e)
|
||||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
showFileManagerDialogFragment()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
||||||
}
|
}
|
||||||
if (fragment != null)
|
|
||||||
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
|
||||||
else
|
|
||||||
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
|
||||||
private fun openActivityWithActionGetContent(typeString: String) {
|
|
||||||
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
type = typeString
|
|
||||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
if (fragment != null)
|
|
||||||
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
|
|
||||||
else
|
|
||||||
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To use in onActivityResultCallback in Fragment or Activity
|
|
||||||
* @param onFileSelected Callback retrieve from data
|
|
||||||
* @return true if requestCode was captured, false elsewhere
|
|
||||||
*/
|
|
||||||
fun onOpenDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
|
|
||||||
onFileSelected: ((uri: Uri?) -> Unit)?): Boolean {
|
|
||||||
|
|
||||||
when (requestCode) {
|
|
||||||
FILE_BROWSE -> {
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
val filename = data?.dataString
|
|
||||||
var keyUri: Uri? = null
|
|
||||||
if (filename != null) {
|
|
||||||
keyUri = UriUtil.parse(filename)
|
|
||||||
}
|
|
||||||
onFileSelected?.invoke(keyUri)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
GET_CONTENT, OPEN_DOC -> {
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
if (data != null) {
|
|
||||||
val uri = data.data
|
|
||||||
if (uri != null) {
|
|
||||||
UriUtil.takeUriPermission(activity?.contentResolver, uri)
|
|
||||||
onFileSelected?.invoke(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -155,62 +148,50 @@ class ExternalFileHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createDocument(titleString: String,
|
class OpenDocument : ActivityResultContracts.OpenDocument() {
|
||||||
typeString: String = "application/octet-stream"): Int? {
|
@SuppressLint("InlinedApi")
|
||||||
val idCode = getUnusedCreateFileRequestCode()
|
override fun createIntent(context: Context, input: Array<out String>): Intent {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
return super.createIntent(context, input).apply {
|
||||||
try {
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
type = typeString
|
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||||
putExtra(Intent.EXTRA_TITLE, titleString)
|
|
||||||
}
|
}
|
||||||
if (fragment != null)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
fragment?.startActivityForResult(intent, idCode)
|
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
else
|
|
||||||
activity?.startActivityForResult(intent, idCode)
|
|
||||||
return idCode
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to create document", e)
|
|
||||||
showFileManagerDialogFragment()
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
showFileManagerDialogFragment()
|
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
class GetContent : ActivityResultContracts.GetContent() {
|
||||||
* To use in onActivityResultCallback in Fragment or Activity
|
@SuppressLint("InlinedApi")
|
||||||
* @param onFileCreated Callback retrieve from data
|
override fun createIntent(context: Context, input: String): Intent {
|
||||||
* @return true if requestCode was captured, false elsewhere
|
return super.createIntent(context, input).apply {
|
||||||
*/
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
fun onCreateDocumentResult(requestCode: Int, resultCode: Int, data: Intent?,
|
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
onFileCreated: (fileCreated: Uri?)->Unit) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
// Retrieve the created URI from the file manager
|
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
||||||
if (fileRequestCodes.contains(requestCode) && resultCode == RESULT_OK) {
|
}
|
||||||
onFileCreated.invoke(data?.data)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
fileRequestCodes.remove(requestCode)
|
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CreateDocument(private val typeString: String) : ActivityResultContracts.CreateDocument() {
|
||||||
|
override fun createIntent(context: Context, input: String): Intent {
|
||||||
|
return super.createIntent(context, input).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = typeString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val TAG = "OpenFileHelper"
|
private const val TAG = "OpenFileHelper"
|
||||||
|
|
||||||
private const val GET_CONTENT = 25745
|
|
||||||
private const val OPEN_DOC = 25845
|
|
||||||
private const val FILE_BROWSE = 25645
|
|
||||||
|
|
||||||
private var CREATE_FILE_REQUEST_CODE_DEFAULT = 3853
|
|
||||||
private var fileRequestCodes = ArrayList<Int>()
|
|
||||||
|
|
||||||
private fun getUnusedCreateFileRequestCode(): Int {
|
|
||||||
val newCreateFileRequestCode = CREATE_FILE_REQUEST_CODE_DEFAULT++
|
|
||||||
fileRequestCodes.add(newCreateFileRequestCode)
|
|
||||||
return newCreateFileRequestCode
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
fun allowCreateDocumentByStorageAccessFramework(packageManager: PackageManager,
|
fun allowCreateDocumentByStorageAccessFramework(packageManager: PackageManager,
|
||||||
typeString: String = "application/octet-stream"): Boolean {
|
typeString: String = "application/octet-stream"): Boolean {
|
||||||
@@ -231,7 +212,7 @@ class ExternalFileHelper {
|
|||||||
fun View.setOpenDocumentClickListener(externalFileHelper: ExternalFileHelper?) {
|
fun View.setOpenDocumentClickListener(externalFileHelper: ExternalFileHelper?) {
|
||||||
externalFileHelper?.let { fileHelper ->
|
externalFileHelper?.let { fileHelper ->
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
fileHelper.openDocument()
|
fileHelper.openDocument(false)
|
||||||
}
|
}
|
||||||
setOnLongClickListener {
|
setOnLongClickListener {
|
||||||
fileHelper.openDocument(true)
|
fileHelper.openDocument(true)
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
|
||||||
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
|
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
@@ -59,9 +59,9 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
|||||||
fun loadDatabase(databaseUri: Uri,
|
fun loadDatabase(databaseUri: Uri,
|
||||||
mainCredential: MainCredential,
|
mainCredential: MainCredential,
|
||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
cipherEntity: CipherDatabaseEntity?,
|
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||||
fixDuplicateUuid: Boolean) {
|
fixDuplicateUuid: Boolean) {
|
||||||
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEntity, fixDuplicateUuid)
|
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEncryptDatabase, fixDuplicateUuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun closeDatabase() {
|
protected fun closeDatabase() {
|
||||||
|
|||||||
@@ -24,12 +24,15 @@ import android.content.Context
|
|||||||
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 android.util.Log
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.DatabaseDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
@@ -62,6 +65,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
private var mExitLock: Boolean = false
|
private var mExitLock: Boolean = false
|
||||||
|
|
||||||
protected var mDatabaseReadOnly: Boolean = true
|
protected var mDatabaseReadOnly: Boolean = true
|
||||||
|
protected var mMergeDataAllowed: Boolean = false
|
||||||
private var mAutoSaveEnable: Boolean = true
|
private var mAutoSaveEnable: Boolean = true
|
||||||
|
|
||||||
protected var mIconDrawableFactory: IconDrawableFactory? = null
|
protected var mIconDrawableFactory: IconDrawableFactory? = null
|
||||||
@@ -87,8 +91,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
mDatabaseTaskProvider?.startDatabaseSave(save)
|
mDatabaseTaskProvider?.startDatabaseSave(save)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.mergeDatabase.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseMerge()
|
||||||
|
}
|
||||||
|
|
||||||
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
|
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
|
||||||
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
|
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mDatabaseViewModel.saveName.observe(this) {
|
mDatabaseViewModel.saveName.observe(this) {
|
||||||
@@ -100,7 +110,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
mDatabaseViewModel.saveDefaultUsername.observe(this) {
|
mDatabaseViewModel.saveDefaultUsername.observe(this) {
|
||||||
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
|
mDatabaseTaskProvider?.startDatabaseSaveDefaultUsername(it.oldValue, it.newValue, it.save)
|
||||||
}
|
}
|
||||||
|
|
||||||
mDatabaseViewModel.saveColor.observe(this) {
|
mDatabaseViewModel.saveColor.observe(this) {
|
||||||
@@ -180,8 +190,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
closeDatabase(database)
|
closeDatabase(database)
|
||||||
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
|
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
|
||||||
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
|
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
|
||||||
// Add onActivityForResult response
|
mExitLock = true
|
||||||
setResult(RESULT_EXIT_LOCK)
|
|
||||||
closeOptionsMenu()
|
closeOptionsMenu()
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
@@ -198,6 +207,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
mDatabaseReadOnly = database.isReadOnly
|
mDatabaseReadOnly = database.isReadOnly
|
||||||
|
mMergeDataAllowed = database.isMergeDataAllowed()
|
||||||
mIconDrawableFactory = database.iconDrawableFactory
|
mIconDrawableFactory = database.iconDrawableFactory
|
||||||
|
|
||||||
checkRegister()
|
checkRegister()
|
||||||
@@ -213,6 +223,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
) {
|
) {
|
||||||
super.onDatabaseActionFinished(database, actionTask, result)
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
|
DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK,
|
||||||
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
|
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
|
||||||
// Reload the current activity
|
// Reload the current activity
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
@@ -255,8 +266,22 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
mDatabaseTaskProvider?.startDatabaseSave(true)
|
mDatabaseTaskProvider?.startDatabaseSave(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveDatabaseTo(uri: Uri) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSave(true, uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mergeDatabase() {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseMerge()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseMerge(uri, mainCredential)
|
||||||
|
}
|
||||||
|
|
||||||
fun reloadDatabase() {
|
fun reloadDatabase() {
|
||||||
mDatabaseTaskProvider?.startDatabaseReload(false)
|
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseReload(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createEntry(newEntry: Entry,
|
fun createEntry(newEntry: Entry,
|
||||||
@@ -353,14 +378,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
if (resultCode == RESULT_EXIT_LOCK) {
|
|
||||||
mExitLock = true
|
|
||||||
lockAndExit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkRegister() {
|
private fun checkRegister() {
|
||||||
// If in ave or registration mode, don't allow read only
|
// If in ave or registration mode, don't allow read only
|
||||||
if ((mSpecialMode == SpecialMode.SAVE
|
if ((mSpecialMode == SpecialMode.SAVE
|
||||||
@@ -417,7 +434,17 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected fun lockAndExit() {
|
protected fun lockAndExit() {
|
||||||
sendBroadcast(Intent(LOCK_ACTION))
|
// Ask confirmation if modification not saved
|
||||||
|
if (mDatabase?.dataModifiedSinceLastLoading == true) {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setMessage(R.string.discard_changes)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setPositiveButton(R.string.lock) { _, _ ->
|
||||||
|
sendBroadcast(Intent(LOCK_ACTION))
|
||||||
|
}.create().show()
|
||||||
|
} else {
|
||||||
|
sendBroadcast(Intent(LOCK_ACTION))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetAppTimeout() {
|
fun resetAppTimeout() {
|
||||||
@@ -440,8 +467,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
const val TAG = "LockingActivity"
|
const val TAG = "LockingActivity"
|
||||||
|
|
||||||
const val RESULT_EXIT_LOCK = 1450
|
|
||||||
|
|
||||||
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
||||||
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
||||||
|
|
||||||
@@ -455,25 +480,33 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
*/
|
*/
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
fun View.resetAppTimeoutWhenViewTouchedOrFocused(context: Context, databaseLoaded: Boolean?) {
|
fun View.resetAppTimeoutWhenViewTouchedOrFocused(context: Context, databaseLoaded: Boolean?) {
|
||||||
// Log.d(DatabaseLockActivity.TAG, "View prepared to reset app timeout")
|
try {
|
||||||
setOnTouchListener { _, event ->
|
// Log.d(DatabaseLockActivity.TAG, "View prepared to reset app timeout")
|
||||||
when (event.action) {
|
setOnTouchListener { _, event ->
|
||||||
MotionEvent.ACTION_DOWN -> {
|
when (event.action) {
|
||||||
// Log.d(DatabaseLockActivity.TAG, "View touched, try to reset app timeout")
|
MotionEvent.ACTION_DOWN -> {
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context,
|
// Log.d(DatabaseLockActivity.TAG, "View touched, try to reset app timeout")
|
||||||
databaseLoaded ?: false)
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(
|
||||||
|
context,
|
||||||
|
databaseLoaded ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
setOnFocusChangeListener { _, _ ->
|
||||||
|
// Log.d(DatabaseLockActivity.TAG, "View focused, try to reset app timeout")
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(
|
||||||
|
context,
|
||||||
|
databaseLoaded ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (this is ViewGroup) {
|
||||||
|
for (i in 0..childCount) {
|
||||||
|
getChildAt(i)?.resetAppTimeoutWhenViewTouchedOrFocused(context, databaseLoaded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
} catch (e: Exception) {
|
||||||
}
|
Log.e("AppTimeout", "Unable to reset app timeout", e)
|
||||||
setOnFocusChangeListener { _, _ ->
|
|
||||||
// Log.d(DatabaseLockActivity.TAG, "View focused, try to reset app timeout")
|
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context,
|
|
||||||
databaseLoaded ?: false)
|
|
||||||
}
|
|
||||||
if (this is ViewGroup) {
|
|
||||||
for (i in 0..childCount) {
|
|
||||||
getChildAt(i)?.resetAppTimeoutWhenViewTouchedOrFocused(context, databaseLoaded)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.kunzisoft.keepass.model.SearchInfo
|
|||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.SpecialModeView
|
import com.kunzisoft.keepass.view.SpecialModeView
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity to manage database special mode (ie: selection mode)
|
* Activity to manage database special mode (ie: selection mode)
|
||||||
*/
|
*/
|
||||||
@@ -63,8 +64,7 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||||
// To move the app in background
|
backToTheMainAppAndFinish()
|
||||||
moveTaskToBack(true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,8 +77,7 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||||
// To move the app in background
|
backToTheMainAppAndFinish()
|
||||||
moveTaskToBack(true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,11 +87,16 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
// To get the app caller, only for IntentSender
|
// To get the app caller, only for IntentSender
|
||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
} else {
|
} else {
|
||||||
// To move the app in background
|
backToTheMainAppAndFinish()
|
||||||
moveTaskToBack(true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun backToTheMainAppAndFinish() {
|
||||||
|
// To move the app in background and return to the main app
|
||||||
|
moveTaskToBack(true)
|
||||||
|
// Not using FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS or finish() because kills the service
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -160,12 +164,17 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// To hide home button from the regular toolbar in special mode
|
// To hide home button from the regular toolbar in special mode
|
||||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
if (mSpecialMode != SpecialMode.DEFAULT
|
||||||
|
&& hideHomeButtonIfModeIsNotDefault()) {
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(false)
|
supportActionBar?.setDisplayShowHomeEnabled(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open fun hideHomeButtonIfModeIsNotDefault(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
private fun blockAutofill(searchInfo: SearchInfo?) {
|
private fun blockAutofill(searchInfo: SearchInfo?) {
|
||||||
val webDomain = searchInfo?.webDomain
|
val webDomain = searchInfo?.webDomain
|
||||||
val applicationId = searchInfo?.applicationId
|
val applicationId = searchInfo?.applicationId
|
||||||
|
|||||||
@@ -39,7 +39,11 @@ object Stylish {
|
|||||||
*/
|
*/
|
||||||
fun load(context: Context) {
|
fun load(context: Context) {
|
||||||
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
|
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
|
||||||
themeString = PreferencesUtil.getStyle(context)
|
try {
|
||||||
|
themeString = PreferencesUtil.getStyle(context)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Stylish", "Unable to get preference style", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {
|
fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {
|
||||||
@@ -65,8 +69,10 @@ object Stylish {
|
|||||||
context.getString(R.string.list_style_name_night) -> context.getString(R.string.list_style_name_light)
|
context.getString(R.string.list_style_name_night) -> context.getString(R.string.list_style_name_light)
|
||||||
context.getString(R.string.list_style_name_black) -> context.getString(R.string.list_style_name_white)
|
context.getString(R.string.list_style_name_black) -> context.getString(R.string.list_style_name_white)
|
||||||
context.getString(R.string.list_style_name_dark) -> context.getString(R.string.list_style_name_clear)
|
context.getString(R.string.list_style_name_dark) -> context.getString(R.string.list_style_name_clear)
|
||||||
|
context.getString(R.string.list_style_name_simple_night) -> context.getString(R.string.list_style_name_simple)
|
||||||
context.getString(R.string.list_style_name_blue_night) -> context.getString(R.string.list_style_name_blue)
|
context.getString(R.string.list_style_name_blue_night) -> context.getString(R.string.list_style_name_blue)
|
||||||
context.getString(R.string.list_style_name_red_night) -> context.getString(R.string.list_style_name_red)
|
context.getString(R.string.list_style_name_red_night) -> context.getString(R.string.list_style_name_red)
|
||||||
|
context.getString(R.string.list_style_name_reply_night) -> context.getString(R.string.list_style_name_reply)
|
||||||
context.getString(R.string.list_style_name_purple_dark) -> context.getString(R.string.list_style_name_purple)
|
context.getString(R.string.list_style_name_purple_dark) -> context.getString(R.string.list_style_name_purple)
|
||||||
else -> styleString
|
else -> styleString
|
||||||
}
|
}
|
||||||
@@ -77,8 +83,10 @@ object Stylish {
|
|||||||
context.getString(R.string.list_style_name_light) -> context.getString(R.string.list_style_name_night)
|
context.getString(R.string.list_style_name_light) -> context.getString(R.string.list_style_name_night)
|
||||||
context.getString(R.string.list_style_name_white) -> context.getString(R.string.list_style_name_black)
|
context.getString(R.string.list_style_name_white) -> context.getString(R.string.list_style_name_black)
|
||||||
context.getString(R.string.list_style_name_clear) -> context.getString(R.string.list_style_name_dark)
|
context.getString(R.string.list_style_name_clear) -> context.getString(R.string.list_style_name_dark)
|
||||||
|
context.getString(R.string.list_style_name_simple) -> context.getString(R.string.list_style_name_simple_night)
|
||||||
context.getString(R.string.list_style_name_blue) -> context.getString(R.string.list_style_name_blue_night)
|
context.getString(R.string.list_style_name_blue) -> context.getString(R.string.list_style_name_blue_night)
|
||||||
context.getString(R.string.list_style_name_red) -> context.getString(R.string.list_style_name_red_night)
|
context.getString(R.string.list_style_name_red) -> context.getString(R.string.list_style_name_red_night)
|
||||||
|
context.getString(R.string.list_style_name_reply) -> context.getString(R.string.list_style_name_reply_night)
|
||||||
context.getString(R.string.list_style_name_purple) -> context.getString(R.string.list_style_name_purple_dark)
|
context.getString(R.string.list_style_name_purple) -> context.getString(R.string.list_style_name_purple_dark)
|
||||||
else -> styleString
|
else -> styleString
|
||||||
}
|
}
|
||||||
@@ -109,10 +117,14 @@ object Stylish {
|
|||||||
context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black
|
context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black
|
||||||
context.getString(R.string.list_style_name_clear) -> R.style.KeepassDXStyle_Clear
|
context.getString(R.string.list_style_name_clear) -> R.style.KeepassDXStyle_Clear
|
||||||
context.getString(R.string.list_style_name_dark) -> R.style.KeepassDXStyle_Dark
|
context.getString(R.string.list_style_name_dark) -> R.style.KeepassDXStyle_Dark
|
||||||
|
context.getString(R.string.list_style_name_simple) -> R.style.KeepassDXStyle_Simple
|
||||||
|
context.getString(R.string.list_style_name_simple_night) -> R.style.KeepassDXStyle_Simple_Night
|
||||||
context.getString(R.string.list_style_name_blue) -> R.style.KeepassDXStyle_Blue
|
context.getString(R.string.list_style_name_blue) -> R.style.KeepassDXStyle_Blue
|
||||||
context.getString(R.string.list_style_name_blue_night) -> R.style.KeepassDXStyle_Blue_Night
|
context.getString(R.string.list_style_name_blue_night) -> R.style.KeepassDXStyle_Blue_Night
|
||||||
context.getString(R.string.list_style_name_red) -> R.style.KeepassDXStyle_Red
|
context.getString(R.string.list_style_name_red) -> R.style.KeepassDXStyle_Red
|
||||||
context.getString(R.string.list_style_name_red_night) -> R.style.KeepassDXStyle_Red_Night
|
context.getString(R.string.list_style_name_red_night) -> R.style.KeepassDXStyle_Red_Night
|
||||||
|
context.getString(R.string.list_style_name_reply) -> R.style.KeepassDXStyle_Reply
|
||||||
|
context.getString(R.string.list_style_name_reply_night) -> R.style.KeepassDXStyle_Reply_Night
|
||||||
context.getString(R.string.list_style_name_purple) -> R.style.KeepassDXStyle_Purple
|
context.getString(R.string.list_style_name_purple) -> R.style.KeepassDXStyle_Purple
|
||||||
context.getString(R.string.list_style_name_purple_dark) -> R.style.KeepassDXStyle_Purple_Dark
|
context.getString(R.string.list_style_name_purple_dark) -> R.style.KeepassDXStyle_Purple_Dark
|
||||||
else -> R.style.KeepassDXStyle_Light
|
else -> R.style.KeepassDXStyle_Light
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import android.util.Log
|
|||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.annotation.StyleRes
|
import androidx.annotation.StyleRes
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_APPEARANCE_PREFERENCE_CHANGED
|
import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_PREFERENCE_CHANGED
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
|
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
|
||||||
@@ -89,8 +89,8 @@ abstract class StylishActivity : AppCompatActivity() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
if ((customStyle && Stylish.getThemeId(this) != this.themeId)
|
if ((customStyle && Stylish.getThemeId(this) != this.themeId)
|
||||||
|| DATABASE_APPEARANCE_PREFERENCE_CHANGED) {
|
|| DATABASE_PREFERENCE_CHANGED) {
|
||||||
DATABASE_APPEARANCE_PREFERENCE_CHANGED = false
|
DATABASE_PREFERENCE_CHANGED = false
|
||||||
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
|
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
|
||||||
recreateActivity()
|
recreateActivity()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,12 +23,14 @@ import android.content.Context
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.annotation.StyleRes
|
import android.util.Log
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.appcompat.view.ContextThemeWrapper
|
|
||||||
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 androidx.annotation.StyleRes
|
||||||
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
|
||||||
abstract class StylishFragment : Fragment() {
|
abstract class StylishFragment : Fragment() {
|
||||||
|
|
||||||
@@ -42,32 +44,46 @@ abstract class StylishFragment : Fragment() {
|
|||||||
contextThemed = ContextThemeWrapper(context, themeId)
|
contextThemed = ContextThemeWrapper(context, themeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
// To fix status bar color
|
// To fix status bar color
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
val window = requireActivity().window
|
val window = requireActivity().window
|
||||||
val defaultColor = Color.BLACK
|
val defaultColor = Color.BLACK
|
||||||
|
val windowInset = WindowInsetsControllerCompat(window, window.decorView)
|
||||||
try {
|
try {
|
||||||
val taStatusBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.statusBarColor))
|
val taStatusBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.statusBarColor))
|
||||||
window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor
|
window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor
|
||||||
taStatusBarColor?.recycle()
|
taStatusBarColor?.recycle()
|
||||||
} catch (e: Exception) {}
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to retrieve theme : status bar color", e)
|
||||||
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
try {
|
try {
|
||||||
val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar))
|
val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar))
|
||||||
if (taWindowStatusLight?.getBoolean(0, false) == true) {
|
windowInset.isAppearanceLightStatusBars = taWindowStatusLight
|
||||||
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
|
?.getBoolean(0, false) == true
|
||||||
}
|
|
||||||
taWindowStatusLight?.recycle()
|
taWindowStatusLight?.recycle()
|
||||||
} catch (e: Exception) {}
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to retrieve theme : window light status bar", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor))
|
val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor))
|
||||||
window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor
|
window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor
|
||||||
taNavigationBarColor?.recycle()
|
taNavigationBarColor?.recycle()
|
||||||
} catch (e: Exception) {}
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to retrieve theme : navigation bar color", e)
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||||
|
try {
|
||||||
|
val taWindowLightNavigationBar = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightNavigationBar))
|
||||||
|
windowInset.isAppearanceLightNavigationBars = taWindowLightNavigationBar
|
||||||
|
?.getBoolean(0, false) == true
|
||||||
|
taWindowLightNavigationBar?.recycle()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to retrieve theme : navigation light navigation bar", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return super.onCreateView(inflater, container, savedInstanceState)
|
return super.onCreateView(inflater, container, savedInstanceState)
|
||||||
}
|
}
|
||||||
@@ -76,4 +92,8 @@ abstract class StylishFragment : Fragment() {
|
|||||||
contextThemed = null
|
contextThemed = null
|
||||||
super.onDetach()
|
super.onDetach()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = StylishFragment::class.java.simpleName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.view.strikeOut
|
||||||
|
|
||||||
|
class BreadcrumbAdapter(val context: Context)
|
||||||
|
: RecyclerView.Adapter<BreadcrumbAdapter.BreadcrumbGroupViewHolder>() {
|
||||||
|
|
||||||
|
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
|
var iconDrawableFactory: IconDrawableFactory? = null
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
private var mNodeBreadcrumb: MutableList<Node?> = mutableListOf()
|
||||||
|
var onItemClickListener: ((item: Node, position: Int)->Unit)? = null
|
||||||
|
var onLongItemClickListener: ((item: Node, position: Int)->Unit)? = null
|
||||||
|
|
||||||
|
private var mShowNumberEntries = false
|
||||||
|
private var mShowUUID = false
|
||||||
|
private var mIconColor: Int = 0
|
||||||
|
|
||||||
|
init {
|
||||||
|
mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
|
||||||
|
mShowUUID = PreferencesUtil.showUUID(context)
|
||||||
|
|
||||||
|
// Retrieve the textColor to tint the icon
|
||||||
|
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
||||||
|
mIconColor = taTextColor.getColor(0, Color.WHITE)
|
||||||
|
taTextColor.recycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
fun setNode(node: Node?) {
|
||||||
|
mNodeBreadcrumb.clear()
|
||||||
|
node?.let {
|
||||||
|
var currentNode = it
|
||||||
|
mNodeBreadcrumb.add(0, currentNode)
|
||||||
|
while (currentNode.containsParent()) {
|
||||||
|
currentNode.parent?.let { parent ->
|
||||||
|
currentNode = parent
|
||||||
|
mNodeBreadcrumb.add(0, currentNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
return when (position) {
|
||||||
|
mNodeBreadcrumb.size - 1 -> 0
|
||||||
|
else -> 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BreadcrumbGroupViewHolder {
|
||||||
|
return BreadcrumbGroupViewHolder(inflater.inflate(
|
||||||
|
when (viewType) {
|
||||||
|
0 -> R.layout.item_group
|
||||||
|
else -> R.layout.item_breadcrumb
|
||||||
|
}, parent, false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: BreadcrumbGroupViewHolder, position: Int) {
|
||||||
|
val node = mNodeBreadcrumb[position]
|
||||||
|
|
||||||
|
holder.groupNameView.apply {
|
||||||
|
text = node?.title ?: ""
|
||||||
|
strikeOut(node?.isCurrentlyExpires ?: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.itemView.apply {
|
||||||
|
setOnClickListener {
|
||||||
|
node?.let {
|
||||||
|
onItemClickListener?.invoke(it, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setOnLongClickListener {
|
||||||
|
node?.let {
|
||||||
|
onLongItemClickListener?.invoke(it, position)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node?.type == Type.GROUP) {
|
||||||
|
(node as Group).let { group ->
|
||||||
|
|
||||||
|
holder.groupIconView?.let { imageView ->
|
||||||
|
iconDrawableFactory?.assignDatabaseIcon(
|
||||||
|
imageView,
|
||||||
|
group.icon,
|
||||||
|
mIconColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.groupNumbersView?.apply {
|
||||||
|
if (mShowNumberEntries) {
|
||||||
|
group.refreshNumberOfChildEntries(Group.ChildFilter.getDefaults(context))
|
||||||
|
text = group.numberOfChildEntries.toString()
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.groupMetaView?.apply {
|
||||||
|
val meta = group.nodeId.toVisualString()
|
||||||
|
visibility = if (meta != null
|
||||||
|
&& !group.isVirtual
|
||||||
|
&& mShowUUID
|
||||||
|
) {
|
||||||
|
text = meta
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return mNodeBreadcrumb.size
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class BreadcrumbGroupViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
var groupIconView: ImageView? = itemView.findViewById(R.id.group_icon)
|
||||||
|
var groupNumbersView: TextView? = itemView.findViewById(R.id.group_numbers)
|
||||||
|
var groupNameView: TextView = itemView.findViewById(R.id.group_name)
|
||||||
|
var groupMetaView: TextView? = itemView.findViewById(R.id.group_meta)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
|
import com.kunzisoft.keepass.activities.fragments.PassphraseGeneratorFragment
|
||||||
|
import com.kunzisoft.keepass.activities.fragments.PasswordGeneratorFragment
|
||||||
|
import com.kunzisoft.keepass.activities.fragments.KeyGeneratorFragment
|
||||||
|
|
||||||
|
class KeyGeneratorPagerAdapter(fragment: Fragment)
|
||||||
|
: FragmentStateAdapter(fragment) {
|
||||||
|
|
||||||
|
private val passwordGeneratorFragment = PasswordGeneratorFragment()
|
||||||
|
private val passphraseGeneratorFragment = PassphraseGeneratorFragment()
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return KeyGeneratorFragment.KeyGeneratorTab.values().size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createFragment(position: Int): Fragment {
|
||||||
|
return when (KeyGeneratorFragment.KeyGeneratorTab.getKeyGeneratorTabByPosition(position)) {
|
||||||
|
KeyGeneratorFragment.KeyGeneratorTab.PASSWORD -> passwordGeneratorFragment
|
||||||
|
KeyGeneratorFragment.KeyGeneratorTab.PASSPHRASE -> passphraseGeneratorFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,13 +26,13 @@ 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.ProgressBar
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SortedList
|
import androidx.recyclerview.widget.SortedList
|
||||||
import androidx.recyclerview.widget.SortedListAdapterCallback
|
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||||
|
import com.google.android.material.progressindicator.CircularProgressIndicator
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
@@ -41,9 +41,11 @@ import com.kunzisoft.keepass.database.element.SortNodeEnum
|
|||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.otp.OtpType
|
import com.kunzisoft.keepass.otp.OtpType
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
import com.kunzisoft.keepass.view.setTextSize
|
import com.kunzisoft.keepass.view.setTextSize
|
||||||
import com.kunzisoft.keepass.view.strikeOut
|
import com.kunzisoft.keepass.view.strikeOut
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -52,9 +54,9 @@ import java.util.*
|
|||||||
* 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
|
||||||
*/
|
*/
|
||||||
class NodeAdapter (private val context: Context,
|
class NodesAdapter (private val context: Context,
|
||||||
private val database: Database)
|
private val database: Database)
|
||||||
: RecyclerView.Adapter<NodeAdapter.NodeViewHolder>() {
|
: RecyclerView.Adapter<NodesAdapter.NodeViewHolder>() {
|
||||||
|
|
||||||
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
|
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
|
||||||
private val mNodeSortedListCallback: NodeSortedListCallback
|
private val mNodeSortedListCallback: NodeSortedListCallback
|
||||||
@@ -64,26 +66,36 @@ class NodeAdapter (private val context: Context,
|
|||||||
private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type
|
private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type
|
||||||
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
||||||
private var mPrefSizeMultiplier: Float = 0F
|
private var mPrefSizeMultiplier: Float = 0F
|
||||||
private var mSubtextDefaultDimension: Float = 0F
|
private var mTextDefaultDimension: Float = 0F
|
||||||
private var mInfoTextDefaultDimension: Float = 0F
|
private var mSubTextDefaultDimension: Float = 0F
|
||||||
|
private var mMetaTextDefaultDimension: Float = 0F
|
||||||
|
private var mOtpTokenTextDefaultDimension: Float = 0F
|
||||||
private var mNumberChildrenTextDefaultDimension: Float = 0F
|
private var mNumberChildrenTextDefaultDimension: Float = 0F
|
||||||
private var mIconDefaultDimension: Float = 0F
|
private var mIconDefaultDimension: Float = 0F
|
||||||
|
|
||||||
|
private var mShowEntryColors: Boolean = true
|
||||||
private var mShowUserNames: Boolean = true
|
private var mShowUserNames: Boolean = true
|
||||||
private var mShowNumberEntries: Boolean = true
|
private var mShowNumberEntries: Boolean = true
|
||||||
private var mShowOTP: Boolean = false
|
private var mShowOTP: Boolean = false
|
||||||
private var mShowUUID: Boolean = false
|
private var mShowUUID: Boolean = false
|
||||||
private var mEntryFilters = arrayOf<Group.ChildFilter>()
|
private var mEntryFilters = arrayOf<Group.ChildFilter>()
|
||||||
|
private var mOldVirtualGroup = false
|
||||||
|
private var mVirtualGroup = false
|
||||||
|
|
||||||
private var mActionNodesList = LinkedList<Node>()
|
private var mActionNodesList = LinkedList<Node>()
|
||||||
private var mNodeClickCallback: NodeClickCallback? = null
|
private var mNodeClickCallback: NodeClickCallback? = null
|
||||||
|
private var mClipboardHelper = ClipboardHelper(context)
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private val mContentSelectionColor: Int
|
private val mTextColorPrimary: Int
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private val mIconGroupColor: Int
|
private val mTextColor: Int
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private val mIconEntryColor: Int
|
private val mTextColorSecondary: Int
|
||||||
|
@ColorInt
|
||||||
|
private val mColorAccentLight: Int
|
||||||
|
@ColorInt
|
||||||
|
private val mColorOnAccentColor: Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the adapter contains or not any element
|
* Determine if the adapter contains or not any element
|
||||||
@@ -100,16 +112,26 @@ class NodeAdapter (private val context: Context,
|
|||||||
this.mNodeSortedListCallback = NodeSortedListCallback()
|
this.mNodeSortedListCallback = NodeSortedListCallback()
|
||||||
this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback)
|
this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback)
|
||||||
|
|
||||||
// Color of content selection
|
|
||||||
this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white)
|
|
||||||
// Retrieve the color to tint the icon
|
// Retrieve the color to tint the icon
|
||||||
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
||||||
this.mIconGroupColor = taTextColorPrimary.getColor(0, Color.BLACK)
|
this.mTextColorPrimary = taTextColorPrimary.getColor(0, Color.BLACK)
|
||||||
taTextColorPrimary.recycle()
|
taTextColorPrimary.recycle()
|
||||||
// In two times to fix bug compilation
|
// To get text color
|
||||||
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
this.mIconEntryColor = taTextColor.getColor(0, Color.BLACK)
|
this.mTextColor = taTextColor.getColor(0, Color.BLACK)
|
||||||
taTextColor.recycle()
|
taTextColor.recycle()
|
||||||
|
// To get text color secondary
|
||||||
|
val taTextColorSecondary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorSecondary))
|
||||||
|
this.mTextColorSecondary = taTextColorSecondary.getColor(0, Color.BLACK)
|
||||||
|
taTextColorSecondary.recycle()
|
||||||
|
// To get background color for selection
|
||||||
|
val taColorAccentLight = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccentLight))
|
||||||
|
this.mColorAccentLight = taColorAccentLight.getColor(0, Color.GRAY)
|
||||||
|
taColorAccentLight.recycle()
|
||||||
|
// To get text color for selection
|
||||||
|
val taColorOnAccentColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnAccentColor))
|
||||||
|
this.mColorOnAccentColor = taColorOnAccentColor.getColor(0, Color.WHITE)
|
||||||
|
taColorOnAccentColor.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assignPreferences() {
|
private fun assignPreferences() {
|
||||||
@@ -124,6 +146,7 @@ class NodeAdapter (private val context: Context,
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
this.mShowEntryColors = PreferencesUtil.showEntryColors(context)
|
||||||
this.mShowUserNames = PreferencesUtil.showUsernamesListEntries(context)
|
this.mShowUserNames = PreferencesUtil.showUsernamesListEntries(context)
|
||||||
this.mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
|
this.mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
|
||||||
this.mShowOTP = PreferencesUtil.showOTPToken(context)
|
this.mShowOTP = PreferencesUtil.showOTPToken(context)
|
||||||
@@ -139,6 +162,8 @@ class NodeAdapter (private val context: Context,
|
|||||||
* 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: Group) {
|
fun rebuildList(group: Group) {
|
||||||
|
mOldVirtualGroup = mVirtualGroup
|
||||||
|
mVirtualGroup = group.isVirtual
|
||||||
assignPreferences()
|
assignPreferences()
|
||||||
mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters))
|
mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters))
|
||||||
}
|
}
|
||||||
@@ -149,14 +174,19 @@ class NodeAdapter (private val context: Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
|
override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean {
|
||||||
|
if (mOldVirtualGroup != mVirtualGroup)
|
||||||
|
return false
|
||||||
var typeContentTheSame = true
|
var typeContentTheSame = true
|
||||||
if (oldItem is Entry && newItem is Entry) {
|
if (oldItem is Entry && newItem is Entry) {
|
||||||
typeContentTheSame = oldItem.getVisualTitle() == newItem.getVisualTitle()
|
typeContentTheSame = oldItem.getVisualTitle() == newItem.getVisualTitle()
|
||||||
&& oldItem.username == newItem.username
|
&& oldItem.username == newItem.username
|
||||||
|
&& oldItem.backgroundColor == newItem.backgroundColor
|
||||||
|
&& oldItem.foregroundColor == newItem.foregroundColor
|
||||||
&& oldItem.getOtpElement() == newItem.getOtpElement()
|
&& oldItem.getOtpElement() == newItem.getOtpElement()
|
||||||
&& oldItem.containsAttachment() == newItem.containsAttachment()
|
&& oldItem.containsAttachment() == newItem.containsAttachment()
|
||||||
} else if (oldItem is Group && newItem is Group) {
|
} else if (oldItem is Group && newItem is Group) {
|
||||||
typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries
|
typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries
|
||||||
|
&& oldItem.notes == newItem.notes
|
||||||
}
|
}
|
||||||
return typeContentTheSame
|
return typeContentTheSame
|
||||||
&& oldItem.nodeId == newItem.nodeId
|
&& oldItem.nodeId == newItem.nodeId
|
||||||
@@ -299,8 +329,10 @@ class NodeAdapter (private val context: Context,
|
|||||||
mInflater.inflate(R.layout.item_list_nodes_entry, parent, false)
|
mInflater.inflate(R.layout.item_list_nodes_entry, parent, false)
|
||||||
}
|
}
|
||||||
val nodeViewHolder = NodeViewHolder(view)
|
val nodeViewHolder = NodeViewHolder(view)
|
||||||
mInfoTextDefaultDimension = nodeViewHolder.text.textSize
|
mTextDefaultDimension = nodeViewHolder.text.textSize
|
||||||
mSubtextDefaultDimension = nodeViewHolder.subText.textSize
|
mSubTextDefaultDimension = nodeViewHolder.subText?.textSize ?: mSubTextDefaultDimension
|
||||||
|
mMetaTextDefaultDimension = nodeViewHolder.meta.textSize
|
||||||
|
mOtpTokenTextDefaultDimension = nodeViewHolder.otpToken?.textSize ?: mOtpTokenTextDefaultDimension
|
||||||
nodeViewHolder.numberChildren?.let {
|
nodeViewHolder.numberChildren?.let {
|
||||||
mNumberChildrenTextDefaultDimension = it.textSize
|
mNumberChildrenTextDefaultDimension = it.textSize
|
||||||
}
|
}
|
||||||
@@ -311,41 +343,43 @@ class NodeAdapter (private val context: Context,
|
|||||||
val subNode = mNodeSortedList.get(position)
|
val subNode = mNodeSortedList.get(position)
|
||||||
|
|
||||||
// Node selection
|
// Node selection
|
||||||
holder.container.isSelected = mActionNodesList.contains(subNode)
|
holder.container.apply {
|
||||||
|
isSelected = mActionNodesList.contains(subNode)
|
||||||
// Assign image
|
|
||||||
val iconColor = if (holder.container.isSelected)
|
|
||||||
mContentSelectionColor
|
|
||||||
else when (subNode.type) {
|
|
||||||
Type.GROUP -> mIconGroupColor
|
|
||||||
Type.ENTRY -> mIconEntryColor
|
|
||||||
}
|
|
||||||
holder.imageIdentifier?.setColorFilter(iconColor)
|
|
||||||
holder.icon.apply {
|
|
||||||
database.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor)
|
|
||||||
// Relative size of the icon
|
|
||||||
layoutParams?.apply {
|
|
||||||
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
|
|
||||||
width = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign text
|
// Assign text
|
||||||
holder.text.apply {
|
holder.text.apply {
|
||||||
text = subNode.title
|
text = subNode.title
|
||||||
setTextSize(mTextSizeUnit, mInfoTextDefaultDimension, mPrefSizeMultiplier)
|
setTextSize(mTextSizeUnit, mTextDefaultDimension, mPrefSizeMultiplier)
|
||||||
strikeOut(subNode.isCurrentlyExpires)
|
strikeOut(subNode.isCurrentlyExpires)
|
||||||
}
|
}
|
||||||
// Add subText with username
|
|
||||||
holder.subText.apply {
|
|
||||||
text = ""
|
|
||||||
strikeOut(subNode.isCurrentlyExpires)
|
|
||||||
visibility = View.GONE
|
|
||||||
}
|
|
||||||
// Add meta text to show UUID
|
// Add meta text to show UUID
|
||||||
holder.meta.apply {
|
holder.meta.apply {
|
||||||
text = subNode.nodeId.toString()
|
val nodeId = subNode.nodeId?.toVisualString()
|
||||||
visibility = if (mShowUUID) View.VISIBLE else View.GONE
|
if (mShowUUID && nodeId != null) {
|
||||||
|
text = nodeId
|
||||||
|
setTextSize(mTextSizeUnit, mMetaTextDefaultDimension, mPrefSizeMultiplier)
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add path to virtual group
|
||||||
|
if (mVirtualGroup) {
|
||||||
|
holder.path?.apply {
|
||||||
|
text = subNode.getPathString()
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
holder.path?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign icon colors
|
||||||
|
var iconColor = if (holder.container.isSelected)
|
||||||
|
mColorOnAccentColor
|
||||||
|
else when (subNode.type) {
|
||||||
|
Type.GROUP -> mTextColorPrimary
|
||||||
|
Type.ENTRY -> mTextColor
|
||||||
}
|
}
|
||||||
|
|
||||||
// Specific elements for entry
|
// Specific elements for entry
|
||||||
@@ -354,12 +388,16 @@ class NodeAdapter (private val context: Context,
|
|||||||
database.startManageEntry(entry)
|
database.startManageEntry(entry)
|
||||||
|
|
||||||
holder.text.text = entry.getVisualTitle()
|
holder.text.text = entry.getVisualTitle()
|
||||||
holder.subText.apply {
|
// Add subText with username
|
||||||
|
holder.subText?.apply {
|
||||||
val username = entry.username
|
val username = entry.username
|
||||||
if (mShowUserNames && username.isNotEmpty()) {
|
if (mShowUserNames && username.isNotEmpty()) {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
text = username
|
text = username
|
||||||
setTextSize(mTextSizeUnit, mSubtextDefaultDimension, mPrefSizeMultiplier)
|
setTextSize(mTextSizeUnit, mSubTextDefaultDimension, mPrefSizeMultiplier)
|
||||||
|
strikeOut(subNode.isCurrentlyExpires)
|
||||||
|
} else {
|
||||||
|
visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,6 +423,44 @@ class NodeAdapter (private val context: Context,
|
|||||||
holder.attachmentIcon?.visibility =
|
holder.attachmentIcon?.visibility =
|
||||||
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
// Assign colors
|
||||||
|
val backgroundColor = if (mShowEntryColors) entry.backgroundColor else null
|
||||||
|
if (!holder.container.isSelected) {
|
||||||
|
if (backgroundColor != null) {
|
||||||
|
holder.container.setBackgroundColor(backgroundColor)
|
||||||
|
} else {
|
||||||
|
holder.container.setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
holder.container.setBackgroundColor(mColorAccentLight)
|
||||||
|
}
|
||||||
|
val foregroundColor = if (mShowEntryColors) entry.foregroundColor else null
|
||||||
|
if (!holder.container.isSelected) {
|
||||||
|
if (foregroundColor != null) {
|
||||||
|
holder.text.setTextColor(foregroundColor)
|
||||||
|
holder.subText?.setTextColor(foregroundColor)
|
||||||
|
holder.otpToken?.setTextColor(foregroundColor)
|
||||||
|
holder.otpProgress?.setIndicatorColor(foregroundColor)
|
||||||
|
holder.attachmentIcon?.setColorFilter(foregroundColor)
|
||||||
|
holder.meta.setTextColor(foregroundColor)
|
||||||
|
iconColor = foregroundColor
|
||||||
|
} else {
|
||||||
|
holder.text.setTextColor(mTextColor)
|
||||||
|
holder.subText?.setTextColor(mTextColorSecondary)
|
||||||
|
holder.otpToken?.setTextColor(mTextColorSecondary)
|
||||||
|
holder.otpProgress?.setIndicatorColor(mTextColorSecondary)
|
||||||
|
holder.attachmentIcon?.setColorFilter(mTextColorSecondary)
|
||||||
|
holder.meta.setTextColor(mTextColor)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
holder.text.setTextColor(mColorOnAccentColor)
|
||||||
|
holder.subText?.setTextColor(mColorOnAccentColor)
|
||||||
|
holder.otpToken?.setTextColor(mColorOnAccentColor)
|
||||||
|
holder.otpProgress?.setIndicatorColor(mColorOnAccentColor)
|
||||||
|
holder.attachmentIcon?.setColorFilter(mColorOnAccentColor)
|
||||||
|
holder.meta.setTextColor(mColorOnAccentColor)
|
||||||
|
}
|
||||||
|
|
||||||
database.stopManageEntry(entry)
|
database.stopManageEntry(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,6 +479,17 @@ class NodeAdapter (private val context: Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assign image
|
||||||
|
holder.imageIdentifier?.setColorFilter(iconColor)
|
||||||
|
holder.icon.apply {
|
||||||
|
database.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor)
|
||||||
|
// Relative size of the icon
|
||||||
|
layoutParams?.apply {
|
||||||
|
height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
|
||||||
|
width = (mIconDefaultDimension * mPrefSizeMultiplier).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Assign click
|
// Assign click
|
||||||
holder.container.setOnClickListener {
|
holder.container.setOnClickListener {
|
||||||
mNodeClickCallback?.onNodeClick(database, subNode)
|
mNodeClickCallback?.onNodeClick(database, subNode)
|
||||||
@@ -417,17 +504,32 @@ class NodeAdapter (private val context: Context,
|
|||||||
OtpType.HOTP -> {
|
OtpType.HOTP -> {
|
||||||
holder?.otpProgress?.apply {
|
holder?.otpProgress?.apply {
|
||||||
max = 100
|
max = 100
|
||||||
progress = 100
|
setProgressCompat(100, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OtpType.TOTP -> {
|
OtpType.TOTP -> {
|
||||||
holder?.otpProgress?.apply {
|
holder?.otpProgress?.apply {
|
||||||
max = otpElement.period
|
max = otpElement.period
|
||||||
progress = otpElement.secondsRemaining
|
setProgressCompat(otpElement.secondsRemaining, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
null -> {}
|
||||||
|
}
|
||||||
|
holder?.otpToken?.apply {
|
||||||
|
text = otpElement?.tokenString
|
||||||
|
setTextSize(mTextSizeUnit, mOtpTokenTextDefaultDimension, mPrefSizeMultiplier)
|
||||||
|
}
|
||||||
|
holder?.otpContainer?.setOnClickListener {
|
||||||
|
otpElement?.token?.let { token ->
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.copy_field,
|
||||||
|
TemplateField.getLocalizedName(context, TemplateField.LABEL_TOKEN)),
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
mClipboardHelper.copyToClipboard(token)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
holder?.otpToken?.text = otpElement?.token
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class OtpRunnable(val view: View?): Runnable {
|
class OtpRunnable(val view: View?): Runnable {
|
||||||
@@ -468,10 +570,11 @@ class NodeAdapter (private val context: Context,
|
|||||||
var imageIdentifier: ImageView? = itemView.findViewById(R.id.node_image_identifier)
|
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 meta: TextView = itemView.findViewById(R.id.node_meta)
|
var meta: TextView = itemView.findViewById(R.id.node_meta)
|
||||||
|
var path: TextView? = itemView.findViewById(R.id.node_path)
|
||||||
var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container)
|
var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container)
|
||||||
var otpProgress: ProgressBar? = itemView.findViewById(R.id.node_otp_progress)
|
var otpProgress: CircularProgressIndicator? = itemView.findViewById(R.id.node_otp_progress)
|
||||||
var otpToken: TextView? = itemView.findViewById(R.id.node_otp_token)
|
var otpToken: TextView? = itemView.findViewById(R.id.node_otp_token)
|
||||||
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
|
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
|
||||||
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
||||||
@@ -479,6 +582,6 @@ class NodeAdapter (private val context: Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = NodeAdapter::class.java.name
|
private val TAG = NodesAdapter::class.java.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.adapters
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.database.cursor.EntryCursorKDB
|
|
||||||
import com.kunzisoft.keepass.database.cursor.EntryCursorKDBX
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
|
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.view.strikeOut
|
|
||||||
|
|
||||||
class SearchEntryCursorAdapter(private val context: Context,
|
|
||||||
private val database: Database)
|
|
||||||
: androidx.cursoradapter.widget.CursorAdapter(context, null, FLAG_REGISTER_CONTENT_OBSERVER) {
|
|
||||||
|
|
||||||
private val cursorInflater: LayoutInflater? = context.getSystemService(
|
|
||||||
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
|
||||||
private var mDisplayUsername: Boolean = false
|
|
||||||
private var mOmitBackup: Boolean = true
|
|
||||||
private val iconColor: Int
|
|
||||||
|
|
||||||
init {
|
|
||||||
// Get the icon color
|
|
||||||
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
|
||||||
this.iconColor = taTextColor.getColor(0, Color.WHITE)
|
|
||||||
taTextColor.recycle()
|
|
||||||
|
|
||||||
reInit(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reInit(context: Context) {
|
|
||||||
this.mDisplayUsername = PreferencesUtil.showUsernamesListEntries(context)
|
|
||||||
this.mOmitBackup = PreferencesUtil.omitBackup(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
|
|
||||||
|
|
||||||
val view = cursorInflater!!.inflate(R.layout.item_search_entry, parent, false)
|
|
||||||
val viewHolder = ViewHolder()
|
|
||||||
viewHolder.imageViewIcon = view.findViewById(R.id.entry_icon)
|
|
||||||
viewHolder.textViewTitle = view.findViewById(R.id.entry_text)
|
|
||||||
viewHolder.textViewSubTitle = view.findViewById(R.id.entry_subtext)
|
|
||||||
view.tag = viewHolder
|
|
||||||
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun bindView(view: View, context: Context, cursor: Cursor) {
|
|
||||||
getEntryFrom(cursor)?.let { currentEntry ->
|
|
||||||
val viewHolder = view.tag as ViewHolder
|
|
||||||
|
|
||||||
// Assign image
|
|
||||||
viewHolder.imageViewIcon?.let { iconView ->
|
|
||||||
database.iconDrawableFactory.assignDatabaseIcon(iconView, currentEntry.icon, iconColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign title
|
|
||||||
viewHolder.textViewTitle?.apply {
|
|
||||||
text = currentEntry.getVisualTitle()
|
|
||||||
strikeOut(currentEntry.isCurrentlyExpires)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign subtitle
|
|
||||||
viewHolder.textViewSubTitle?.apply {
|
|
||||||
val entryUsername = currentEntry.username
|
|
||||||
text = if (mDisplayUsername && entryUsername.isNotEmpty()) {
|
|
||||||
String.format("(%s)", entryUsername)
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
visibility = if (text.isEmpty()) View.GONE else View.VISIBLE
|
|
||||||
strikeOut(currentEntry.isCurrentlyExpires)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getEntryFrom(cursor: Cursor): Entry? {
|
|
||||||
return database.createEntry()?.apply {
|
|
||||||
entryKDB?.let { entryKDB ->
|
|
||||||
(cursor as EntryCursorKDB).populateEntry(entryKDB,
|
|
||||||
{ standardIconId ->
|
|
||||||
database.getStandardIcon(standardIconId)
|
|
||||||
},
|
|
||||||
{ customIconId ->
|
|
||||||
database.getCustomIcon(customIconId)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
entryKDBX?.let { entryKDBX ->
|
|
||||||
(cursor as EntryCursorKDBX).populateEntry(entryKDBX,
|
|
||||||
{ standardIconId ->
|
|
||||||
database.getStandardIcon(standardIconId)
|
|
||||||
},
|
|
||||||
{ customIconId ->
|
|
||||||
database.getCustomIcon(customIconId)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun runQueryOnBackgroundThread(constraint: CharSequence): Cursor? {
|
|
||||||
return 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))) {
|
|
||||||
database.startManageEntry(entry)
|
|
||||||
entry.entryKDB?.let {
|
|
||||||
cursorKDB?.addEntry(it)
|
|
||||||
}
|
|
||||||
entry.entryKDBX?.let {
|
|
||||||
cursorKDBX?.addEntry(it)
|
|
||||||
}
|
|
||||||
database.stopManageEntry(entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cursorKDB ?: cursorKDBX
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getEntryFromPosition(position: Int): Entry? {
|
|
||||||
var pwEntry: Entry? = null
|
|
||||||
|
|
||||||
val cursor = this.cursor
|
|
||||||
if (cursor.moveToFirst() && cursor.move(position)) {
|
|
||||||
pwEntry = getEntryFrom(cursor)
|
|
||||||
}
|
|
||||||
return pwEntry
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ViewHolder {
|
|
||||||
var imageViewIcon: ImageView? = null
|
|
||||||
var textViewTitle: TextView? = null
|
|
||||||
var textViewSubTitle: TextView? = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Tags
|
||||||
|
|
||||||
|
class TagsAdapter(context: Context) : RecyclerView.Adapter<TagsAdapter.TagViewHolder>() {
|
||||||
|
|
||||||
|
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
|
private var mTags: Tags = Tags()
|
||||||
|
var onItemClickListener: OnItemClickListener? = null
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TagViewHolder {
|
||||||
|
val view = inflater.inflate(R.layout.item_tag, parent, false)
|
||||||
|
return TagViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: TagViewHolder, position: Int) {
|
||||||
|
val field = mTags.get(position)
|
||||||
|
holder.name.text = field
|
||||||
|
holder.bind(field, onItemClickListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return mTags.size()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTags(tags: Tags) {
|
||||||
|
mTags.setTags(tags)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
mTags.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OnItemClickListener {
|
||||||
|
fun onItemClick(item: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class TagViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
|
var name: TextView = itemView.findViewById(R.id.tag_name)
|
||||||
|
|
||||||
|
fun bind(item: String, listener: OnItemClickListener?) {
|
||||||
|
itemView.setOnClickListener { listener?.onItemClick(item) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.kunzisoft.keepass.database.element.Tags
|
||||||
|
import com.tokenautocomplete.FilteredArrayAdapter
|
||||||
|
|
||||||
|
class TagsProposalAdapter(context: Context, proposal: Tags?)
|
||||||
|
: FilteredArrayAdapter<String>(
|
||||||
|
context,
|
||||||
|
android.R.layout.simple_list_item_1,
|
||||||
|
(proposal ?: Tags()).toList()
|
||||||
|
) {
|
||||||
|
|
||||||
|
override fun keepObject(obj: String, mask: String?): Boolean {
|
||||||
|
if (mask == null)
|
||||||
|
return false
|
||||||
|
return obj.contains(mask, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,23 +9,23 @@ import android.widget.BaseAdapter
|
|||||||
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.element.Database
|
|
||||||
import com.kunzisoft.keepass.database.element.template.Template
|
import com.kunzisoft.keepass.database.element.template.Template
|
||||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
|
|
||||||
|
|
||||||
class TemplatesSelectorAdapter(private val context: Context,
|
class TemplatesSelectorAdapter(
|
||||||
private val iconDrawableFactory: IconDrawableFactory?,
|
context: Context,
|
||||||
private var templates: List<Template>): BaseAdapter() {
|
private var templates: List<Template>): BaseAdapter() {
|
||||||
|
|
||||||
|
var iconDrawableFactory: IconDrawableFactory? = null
|
||||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
private var mIconColor = Color.BLACK
|
private var mTextColor = Color.BLACK
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
mIconColor = taIconColor.getColor(0, Color.BLACK)
|
mTextColor = taTextColor.getColor(0, Color.BLACK)
|
||||||
taIconColor.recycle()
|
taTextColor.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
@@ -36,6 +36,7 @@ class TemplatesSelectorAdapter(private val context: Context,
|
|||||||
if (templateView == null) {
|
if (templateView == null) {
|
||||||
holder = TemplateSelectorViewHolder()
|
holder = TemplateSelectorViewHolder()
|
||||||
templateView = inflater.inflate(R.layout.item_template, parent, false)
|
templateView = inflater.inflate(R.layout.item_template, parent, false)
|
||||||
|
holder.background = templateView?.findViewById(R.id.template_background)
|
||||||
holder.icon = templateView?.findViewById(R.id.template_image)
|
holder.icon = templateView?.findViewById(R.id.template_image)
|
||||||
holder.name = templateView?.findViewById(R.id.template_name)
|
holder.name = templateView?.findViewById(R.id.template_name)
|
||||||
templateView?.tag = holder
|
templateView?.tag = holder
|
||||||
@@ -43,10 +44,15 @@ class TemplatesSelectorAdapter(private val context: Context,
|
|||||||
holder = templateView.tag as TemplateSelectorViewHolder
|
holder = templateView.tag as TemplateSelectorViewHolder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
holder.background?.setBackgroundColor(template.backgroundColor ?: Color.TRANSPARENT)
|
||||||
|
val textColor = template.foregroundColor ?: mTextColor
|
||||||
holder.icon?.let { icon ->
|
holder.icon?.let { icon ->
|
||||||
iconDrawableFactory?.assignDatabaseIcon(icon, template.icon, mIconColor)
|
iconDrawableFactory?.assignDatabaseIcon(icon, template.icon, textColor)
|
||||||
|
}
|
||||||
|
holder.name?.apply {
|
||||||
|
setTextColor(textColor)
|
||||||
|
text = TemplateField.getLocalizedName(context, template.title)
|
||||||
}
|
}
|
||||||
holder.name?.text = TemplateField.getLocalizedName(context, template.title)
|
|
||||||
|
|
||||||
return templateView!!
|
return templateView!!
|
||||||
}
|
}
|
||||||
@@ -64,6 +70,7 @@ class TemplatesSelectorAdapter(private val context: Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
inner class TemplateSelectorViewHolder {
|
inner class TemplateSelectorViewHolder {
|
||||||
|
var background: View? = null
|
||||||
var icon: ImageView? = null
|
var icon: ImageView? = null
|
||||||
var name: TextView? = null
|
var name: TextView? = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ package com.kunzisoft.keepass.app.database
|
|||||||
import android.content.*
|
import android.content.*
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||||
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
||||||
@@ -125,15 +127,41 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getCipherDatabase(databaseUri: Uri,
|
fun getCipherDatabase(databaseUri: Uri,
|
||||||
cipherDatabaseResultListener: (CipherDatabaseEntity?) -> Unit) {
|
cipherDatabaseResultListener: (CipherEncryptDatabase?) -> Unit) {
|
||||||
if (useTempDao) {
|
if (useTempDao) {
|
||||||
serviceActionTask {
|
serviceActionTask {
|
||||||
cipherDatabaseResultListener.invoke(mBinder?.getCipherDatabase(databaseUri))
|
var cipherDatabase: CipherEncryptDatabase? = null
|
||||||
|
mBinder?.getCipherDatabase(databaseUri)?.let { cipherDatabaseEntity ->
|
||||||
|
cipherDatabase = CipherEncryptDatabase().apply {
|
||||||
|
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
|
||||||
|
this.encryptedValue = Base64.decode(
|
||||||
|
cipherDatabaseEntity.encryptedValue,
|
||||||
|
Base64.NO_WRAP
|
||||||
|
)
|
||||||
|
this.specParameters = Base64.decode(
|
||||||
|
cipherDatabaseEntity.specParameters,
|
||||||
|
Base64.NO_WRAP
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cipherDatabaseResultListener.invoke(cipherDatabase)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
|
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())?.let { cipherDatabaseEntity ->
|
||||||
|
CipherEncryptDatabase().apply {
|
||||||
|
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
|
||||||
|
this.encryptedValue = Base64.decode(
|
||||||
|
cipherDatabaseEntity.encryptedValue,
|
||||||
|
Base64.NO_WRAP
|
||||||
|
)
|
||||||
|
this.specParameters = Base64.decode(
|
||||||
|
cipherDatabaseEntity.specParameters,
|
||||||
|
Base64.NO_WRAP
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cipherDatabaseResultListener.invoke(it)
|
cipherDatabaseResultListener.invoke(it)
|
||||||
@@ -149,18 +177,27 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity,
|
fun addOrUpdateCipherDatabase(cipherEncryptDatabase: CipherEncryptDatabase,
|
||||||
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
cipherDatabaseResultListener: (() -> Unit)? = null) {
|
||||||
if (useTempDao) {
|
cipherEncryptDatabase.databaseUri?.let { databaseUri ->
|
||||||
// The only case to create service (not needed to get an info)
|
|
||||||
serviceActionTask(true) {
|
val cipherDatabaseEntity = CipherDatabaseEntity(
|
||||||
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
|
databaseUri.toString(),
|
||||||
cipherDatabaseResultListener?.invoke()
|
Base64.encodeToString(cipherEncryptDatabase.encryptedValue, Base64.NO_WRAP),
|
||||||
}
|
Base64.encodeToString(cipherEncryptDatabase.specParameters, Base64.NO_WRAP),
|
||||||
} else {
|
)
|
||||||
IOActionTask(
|
|
||||||
|
if (useTempDao) {
|
||||||
|
// The only case to create service (not needed to get an info)
|
||||||
|
serviceActionTask(true) {
|
||||||
|
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
|
||||||
|
cipherDatabaseResultListener?.invoke()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
IOActionTask(
|
||||||
{
|
{
|
||||||
val cipherDatabaseRetrieve = cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
|
val cipherDatabaseRetrieve =
|
||||||
|
cipherDatabaseDao.getByDatabaseUri(cipherDatabaseEntity.databaseUri)
|
||||||
// Update values if element not yet in the database
|
// Update values if element not yet in the database
|
||||||
if (cipherDatabaseRetrieve == null) {
|
if (cipherDatabaseRetrieve == null) {
|
||||||
cipherDatabaseDao.add(cipherDatabaseEntity)
|
cipherDatabaseDao.add(cipherDatabaseEntity)
|
||||||
@@ -171,7 +208,8 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
{
|
{
|
||||||
cipherDatabaseResultListener?.invoke()
|
cipherDatabaseResultListener?.invoke()
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import kotlinx.coroutines.*
|
|||||||
*/
|
*/
|
||||||
class IOActionTask<T>(
|
class IOActionTask<T>(
|
||||||
private val action: () -> T ,
|
private val action: () -> T ,
|
||||||
private val afterActionDatabaseListener: ((T?) -> Unit)? = null) {
|
private val afterActionListener: ((T?) -> Unit)? = null) {
|
||||||
|
|
||||||
private val mainScope = CoroutineScope(Dispatchers.Main)
|
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ class IOActionTask<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
afterActionDatabaseListener?.invoke(asyncResult.await())
|
afterActionListener?.invoke(asyncResult.await())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ import android.app.assist.AssistStructure
|
|||||||
import android.view.inputmethod.InlineSuggestionsRequest
|
import android.view.inputmethod.InlineSuggestionsRequest
|
||||||
|
|
||||||
data class AutofillComponent(val assistStructure: AssistStructure,
|
data class AutofillComponent(val assistStructure: AssistStructure,
|
||||||
val inlineSuggestionsRequest: InlineSuggestionsRequest?)
|
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?)
|
||||||
@@ -25,7 +25,6 @@ import android.app.PendingIntent
|
|||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentSender
|
|
||||||
import android.graphics.BlendMode
|
import android.graphics.BlendMode
|
||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -35,11 +34,13 @@ import android.service.autofill.InlinePresentation
|
|||||||
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.view.inputmethod.InlineSuggestionsRequest
|
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import android.widget.inline.InlinePresentationSpec
|
import android.widget.inline.InlinePresentationSpec
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.autofill.inline.UiVersions
|
import androidx.autofill.inline.UiVersions
|
||||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
@@ -49,21 +50,19 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
|
||||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import kotlin.collections.ArrayList
|
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
object AutofillHelper {
|
object AutofillHelper {
|
||||||
|
|
||||||
private const val AUTOFILL_RESPONSE_REQUEST_CODE = 8165
|
|
||||||
|
|
||||||
private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
|
private const val EXTRA_ASSIST_STRUCTURE = AutofillManager.EXTRA_ASSIST_STRUCTURE
|
||||||
const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
|
private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
|
||||||
|
|
||||||
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
|
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
|
||||||
intent?.getParcelableExtra<AssistStructure?>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
|
intent?.getParcelableExtra<AssistStructure?>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
|
||||||
@@ -112,7 +111,7 @@ object AutofillHelper {
|
|||||||
database: Database,
|
database: Database,
|
||||||
entryInfo: EntryInfo,
|
entryInfo: EntryInfo,
|
||||||
struct: StructureParser.Result,
|
struct: StructureParser.Result,
|
||||||
inlinePresentation: InlinePresentation?): Dataset? {
|
additionalBuild: ((build: Dataset.Builder) -> Unit)? = null): Dataset? {
|
||||||
val title = makeEntryTitle(entryInfo)
|
val title = makeEntryTitle(entryInfo)
|
||||||
val views = newRemoteViews(context, database, title, entryInfo.icon)
|
val views = newRemoteViews(context, database, title, entryInfo.icon)
|
||||||
val builder = Dataset.Builder(views)
|
val builder = Dataset.Builder(views)
|
||||||
@@ -201,11 +200,7 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
additionalBuild?.invoke(builder)
|
||||||
inlinePresentation?.let {
|
|
||||||
builder.setInlinePresentation(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
builder.build()
|
builder.build()
|
||||||
@@ -236,40 +231,51 @@ object AutofillHelper {
|
|||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
private fun buildInlinePresentationForEntry(context: Context,
|
private fun buildInlinePresentationForEntry(context: Context,
|
||||||
database: Database,
|
database: Database,
|
||||||
inlineSuggestionsRequest: InlineSuggestionsRequest,
|
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
|
||||||
positionItem: Int,
|
positionItem: Int,
|
||||||
entryInfo: EntryInfo): InlinePresentation? {
|
entryInfo: EntryInfo): InlinePresentation? {
|
||||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
compatInlineSuggestionsRequest.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||||
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||||
|
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
||||||
|
|
||||||
if (positionItem <= maxSuggestion - 1
|
if (positionItem <= maxSuggestion - 1
|
||||||
&& inlinePresentationSpecs.size > positionItem) {
|
&& inlinePresentationSpecs.size > positionItem
|
||||||
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
|
) {
|
||||||
|
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
|
||||||
|
|
||||||
// Make sure that the IME spec claims support for v1 UI template.
|
// Make sure that the IME spec claims support for v1 UI template.
|
||||||
val imeStyle = inlinePresentationSpec.style
|
val imeStyle = inlinePresentationSpec.style
|
||||||
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
|
if (!UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1))
|
||||||
return null
|
return null
|
||||||
|
|
||||||
// Build the content for IME UI
|
// Build the content for IME UI
|
||||||
val pendingIntent = PendingIntent.getActivity(context,
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
0,
|
0,
|
||||||
Intent(context, AutofillSettingsActivity::class.java),
|
Intent(context, AutofillSettingsActivity::class.java),
|
||||||
0)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
return InlinePresentation(
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return InlinePresentation(
|
||||||
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
|
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
|
||||||
setContentDescription(context.getString(R.string.autofill_sign_in_prompt))
|
setContentDescription(context.getString(R.string.autofill_sign_in_prompt))
|
||||||
setTitle(entryInfo.title)
|
setTitle(entryInfo.title)
|
||||||
setSubtitle(entryInfo.username)
|
setSubtitle(entryInfo.username)
|
||||||
setStartIcon(Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
setStartIcon(
|
||||||
setTintBlendMode(BlendMode.DST)
|
Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
||||||
})
|
setTintBlendMode(BlendMode.DST)
|
||||||
|
})
|
||||||
buildIconFromEntry(context, database, entryInfo)?.let { icon ->
|
buildIconFromEntry(context, database, entryInfo)?.let { icon ->
|
||||||
setEndIcon(icon.apply {
|
setEndIcon(icon.apply {
|
||||||
setTintBlendMode(BlendMode.DST)
|
setTintBlendMode(BlendMode.DST)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}.build().slice, inlinePresentationSpec, false)
|
}.build().slice, inlinePresentationSpec, false
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -299,7 +305,7 @@ object AutofillHelper {
|
|||||||
database: Database,
|
database: Database,
|
||||||
entriesInfo: List<EntryInfo>,
|
entriesInfo: List<EntryInfo>,
|
||||||
parseResult: StructureParser.Result,
|
parseResult: StructureParser.Result,
|
||||||
inlineSuggestionsRequest: InlineSuggestionsRequest?): FillResponse? {
|
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? {
|
||||||
val responseBuilder = FillResponse.Builder()
|
val responseBuilder = FillResponse.Builder()
|
||||||
// Add Header
|
// Add Header
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
@@ -320,7 +326,7 @@ object AutofillHelper {
|
|||||||
// Add inline suggestion for new IME and dataset
|
// Add inline suggestion for new IME and dataset
|
||||||
var numberInlineSuggestions = 0
|
var numberInlineSuggestions = 0
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
inlineSuggestionsRequest?.let {
|
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||||
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
|
numberInlineSuggestions = minOf(inlineSuggestionsRequest.maxSuggestionCount, entriesInfo.size)
|
||||||
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
||||||
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
|
if (entriesInfo.size >= inlineSuggestionsRequest.maxSuggestionCount) {
|
||||||
@@ -332,14 +338,19 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
entriesInfo.forEachIndexed { _, entry ->
|
entriesInfo.forEachIndexed { _, entry ->
|
||||||
val inlinePresentation = if (numberInlineSuggestions > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (numberInlineSuggestions > 0
|
||||||
inlineSuggestionsRequest?.let {
|
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
buildInlinePresentationForEntry(context, database, inlineSuggestionsRequest, numberInlineSuggestions--, entry)
|
&& compatInlineSuggestionsRequest != null) {
|
||||||
}
|
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult) { builder ->
|
||||||
|
buildInlinePresentationForEntry(context, database,
|
||||||
|
compatInlineSuggestionsRequest, numberInlineSuggestions--, entry
|
||||||
|
)?.let { inlinePresentation ->
|
||||||
|
builder.setInlinePresentation(inlinePresentation)
|
||||||
|
}
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
null
|
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult))
|
||||||
}
|
}
|
||||||
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult, inlinePresentation))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
||||||
@@ -351,14 +362,14 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
|
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
|
||||||
val pendingIntent = AutofillLauncherActivity.getPendingIntentForSelection(context,
|
val pendingIntent = AutofillLauncherActivity.getPendingIntentForSelection(context,
|
||||||
searchInfo, inlineSuggestionsRequest)
|
searchInfo, compatInlineSuggestionsRequest)
|
||||||
|
|
||||||
parseResult.allAutofillIds().let { autofillIds ->
|
parseResult.allAutofillIds().let { autofillIds ->
|
||||||
autofillIds.forEach { id ->
|
autofillIds.forEach { id ->
|
||||||
val builder = Dataset.Builder(manualSelectionView)
|
val builder = Dataset.Builder(manualSelectionView)
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
inlineSuggestionsRequest?.let {
|
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||||
val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
||||||
val inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent)
|
val inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent)
|
||||||
inlinePresentation?.let {
|
inlinePresentation?.let {
|
||||||
@@ -403,11 +414,11 @@ object AutofillHelper {
|
|||||||
StructureParser(structure).parse()?.let { result ->
|
StructureParser(structure).parse()?.let { result ->
|
||||||
// New Response
|
// New Response
|
||||||
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
val inlineSuggestionsRequest = activity.intent?.getParcelableExtra<InlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtra<CompatInlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
||||||
if (inlineSuggestionsRequest != null) {
|
if (compatInlineSuggestionsRequest != null) {
|
||||||
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
buildResponse(activity, database, entriesInfo, result, inlineSuggestionsRequest)
|
buildResponse(activity, database, entriesInfo, result, compatInlineSuggestionsRequest)
|
||||||
} else {
|
} else {
|
||||||
buildResponse(activity, database, entriesInfo, result, null)
|
buildResponse(activity, database, entriesInfo, result, null)
|
||||||
}
|
}
|
||||||
@@ -427,37 +438,44 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun buildActivityResultLauncher(activity: AppCompatActivity,
|
||||||
|
lockDatabase: Boolean = false): ActivityResultLauncher<Intent> {
|
||||||
|
return activity.registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) {
|
||||||
|
// Utility method to loop and close each activity with return data
|
||||||
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
|
activity.setResult(it.resultCode, it.data)
|
||||||
|
}
|
||||||
|
if (it.resultCode == Activity.RESULT_CANCELED) {
|
||||||
|
activity.setResult(Activity.RESULT_CANCELED)
|
||||||
|
}
|
||||||
|
activity.finish()
|
||||||
|
|
||||||
|
if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(activity)) {
|
||||||
|
// Close the database
|
||||||
|
activity.sendBroadcast(Intent(LOCK_ACTION))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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,
|
fun startActivityForAutofillResult(activity: AppCompatActivity,
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
autofillComponent: AutofillComponent,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo?) {
|
searchInfo: SearchInfo?) {
|
||||||
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||||
intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
|
intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
|
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
|
||||||
autofillComponent.inlineSuggestionsRequest?.let {
|
autofillComponent.compatInlineSuggestionsRequest?.let {
|
||||||
intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
|
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
|
||||||
activity.startActivityForResult(intent, AUTOFILL_RESPONSE_REQUEST_CODE)
|
activityResultLauncher?.launch(intent)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility method to loop and close each activity with return data
|
|
||||||
*/
|
|
||||||
fun onActivityResultSetResultAndFinish(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
if (requestCode == AUTOFILL_RESPONSE_REQUEST_CODE) {
|
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
|
||||||
activity.setResult(resultCode, data)
|
|
||||||
}
|
|
||||||
if (resultCode == Activity.RESULT_CANCELED) {
|
|
||||||
activity.setResult(Activity.RESULT_CANCELED)
|
|
||||||
}
|
|
||||||
activity.finish()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.autofill
|
||||||
|
|
||||||
|
import android.annotation.TargetApi
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.service.autofill.FillRequest
|
||||||
|
import android.view.inputmethod.InlineSuggestionsRequest
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class only to prevent java.lang.NoClassDefFoundError for old Android version and new lib compilation
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
class CompatInlineSuggestionsRequest : Parcelable {
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.R)
|
||||||
|
var inlineSuggestionsRequest: InlineSuggestionsRequest? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
constructor(fillRequest: FillRequest) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
this.inlineSuggestionsRequest = fillRequest.inlineSuggestionsRequest
|
||||||
|
} else {
|
||||||
|
this.inlineSuggestionsRequest = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
|
constructor(inlineSuggestionsRequest: InlineSuggestionsRequest?) {
|
||||||
|
this.inlineSuggestionsRequest = inlineSuggestionsRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(parcel: Parcel) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
this.inlineSuggestionsRequest =
|
||||||
|
parcel.readParcelable(FillRequest::class.java.classLoader)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.inlineSuggestionsRequest = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
parcel.writeParcelable(inlineSuggestionsRequest, flags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object CREATOR : Parcelable.Creator<CompatInlineSuggestionsRequest> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): CompatInlineSuggestionsRequest {
|
||||||
|
return CompatInlineSuggestionsRequest(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<CompatInlineSuggestionsRequest?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -109,7 +109,7 @@ class KeeAutofillService : AutofillService() {
|
|||||||
searchInfo.webDomain = webDomainWithoutSubDomain
|
searchInfo.webDomain = webDomainWithoutSubDomain
|
||||||
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
&& autofillInlineSuggestionsEnabled) {
|
&& autofillInlineSuggestionsEnabled) {
|
||||||
request.inlineSuggestionsRequest
|
CompatInlineSuggestionsRequest(request)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -127,7 +127,7 @@ class KeeAutofillService : AutofillService() {
|
|||||||
private fun launchSelection(database: Database?,
|
private fun launchSelection(database: Database?,
|
||||||
searchInfo: SearchInfo,
|
searchInfo: SearchInfo,
|
||||||
parseResult: StructureParser.Result,
|
parseResult: StructureParser.Result,
|
||||||
inlineSuggestionsRequest: InlineSuggestionsRequest?,
|
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||||
callback: FillCallback) {
|
callback: FillCallback) {
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
database,
|
database,
|
||||||
@@ -155,7 +155,7 @@ class KeeAutofillService : AutofillService() {
|
|||||||
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
||||||
database: Database?,
|
database: Database?,
|
||||||
searchInfo: SearchInfo,
|
searchInfo: SearchInfo,
|
||||||
inlineSuggestionsRequest: InlineSuggestionsRequest?,
|
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||||
callback: FillCallback) {
|
callback: FillCallback) {
|
||||||
parseResult.allAutofillIds().let { autofillIds ->
|
parseResult.allAutofillIds().let { autofillIds ->
|
||||||
if (autofillIds.isNotEmpty()) {
|
if (autofillIds.isNotEmpty()) {
|
||||||
@@ -249,7 +249,7 @@ class KeeAutofillService : AutofillService() {
|
|||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
&& autofillInlineSuggestionsEnabled) {
|
&& autofillInlineSuggestionsEnabled) {
|
||||||
var inlinePresentation: InlinePresentation? = null
|
var inlinePresentation: InlinePresentation? = null
|
||||||
inlineSuggestionsRequest?.let {
|
inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||||
if (inlineSuggestionsRequest.maxSuggestionCount > 0
|
if (inlineSuggestionsRequest.maxSuggestionCount > 0
|
||||||
&& inlinePresentationSpecs.size > 0) {
|
&& inlinePresentationSpecs.size > 0) {
|
||||||
@@ -262,9 +262,13 @@ class KeeAutofillService : AutofillService() {
|
|||||||
inlinePresentation = InlinePresentation(
|
inlinePresentation = InlinePresentation(
|
||||||
InlineSuggestionUi.newContentBuilder(
|
InlineSuggestionUi.newContentBuilder(
|
||||||
PendingIntent.getActivity(this,
|
PendingIntent.getActivity(this,
|
||||||
0,
|
0,
|
||||||
Intent(this, AutofillSettingsActivity::class.java),
|
Intent(this, AutofillSettingsActivity::class.java),
|
||||||
0)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
})
|
||||||
).apply {
|
).apply {
|
||||||
setContentDescription(getString(R.string.autofill_sign_in_prompt))
|
setContentDescription(getString(R.string.autofill_sign_in_prompt))
|
||||||
setTitle(getString(R.string.autofill_sign_in_prompt))
|
setTitle(getString(R.string.autofill_sign_in_prompt))
|
||||||
@@ -277,8 +281,9 @@ class KeeAutofillService : AutofillService() {
|
|||||||
}
|
}
|
||||||
// Build response
|
// Build response
|
||||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
|
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
|
||||||
|
} else {
|
||||||
|
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
|
||||||
}
|
}
|
||||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
|
|
||||||
callback.onSuccess(responseBuilder.build())
|
callback.onSuccess(responseBuilder.build())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,8 +53,10 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
applicationId = windowNode.title.toString().split("/")[0]
|
applicationId = windowNode.title.toString().split("/")[0]
|
||||||
Log.d(TAG, "Autofill applicationId: $applicationId")
|
Log.d(TAG, "Autofill applicationId: $applicationId")
|
||||||
|
|
||||||
if (parseViewNode(windowNode.rootViewNode))
|
if (applicationId?.contains("PopupWindow:") == false) {
|
||||||
break@mainLoop
|
if (parseViewNode(windowNode.rootViewNode))
|
||||||
|
break@mainLoop
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// If not explicit username field found, add the field just before password field.
|
// If not explicit username field found, add the field just before password field.
|
||||||
if (usernameId == null && passwordId != null && usernameIdCandidate != null) {
|
if (usernameId == null && passwordId != null && usernameIdCandidate != null) {
|
||||||
@@ -270,12 +272,12 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean {
|
private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean {
|
||||||
val autofillId = node.autofillId
|
val autofillId = node.autofillId
|
||||||
val nodHtml = node.htmlInfo
|
val nodHtml = node.htmlInfo
|
||||||
when (nodHtml?.tag?.toLowerCase(Locale.ENGLISH)) {
|
when (nodHtml?.tag?.lowercase(Locale.ENGLISH)) {
|
||||||
"input" -> {
|
"input" -> {
|
||||||
nodHtml.attributes?.forEach { pairAttribute ->
|
nodHtml.attributes?.forEach { pairAttribute ->
|
||||||
when (pairAttribute.first.toLowerCase(Locale.ENGLISH)) {
|
when (pairAttribute.first.lowercase(Locale.ENGLISH)) {
|
||||||
"type" -> {
|
"type" -> {
|
||||||
when (pairAttribute.second.toLowerCase(Locale.ENGLISH)) {
|
when (pairAttribute.second.lowercase(Locale.ENGLISH)) {
|
||||||
"tel", "email" -> {
|
"tel", "email" -> {
|
||||||
result?.usernameId = autofillId
|
result?.usernameId = autofillId
|
||||||
result?.usernameValue = node.autofillValue
|
result?.usernameValue = node.autofillValue
|
||||||
@@ -375,9 +377,11 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
when {
|
when {
|
||||||
inputIsVariationType(inputType,
|
inputIsVariationType(inputType,
|
||||||
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
|
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
|
||||||
usernameIdCandidate = autofillId
|
if (usernameIdCandidate == null) {
|
||||||
usernameValueCandidate = node.autofillValue
|
usernameIdCandidate = autofillId
|
||||||
Log.d(TAG, "Autofill username candidate android number type: ${showHexInputType(inputType)}")
|
usernameValueCandidate = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill username candidate android number type: ${showHexInputType(inputType)}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
inputIsVariationType(inputType,
|
inputIsVariationType(inputType,
|
||||||
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {
|
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.biometric
|
package com.kunzisoft.keepass.biometric
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -27,9 +28,11 @@ import android.os.Bundle
|
|||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.*
|
import android.view.*
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.biometric.BiometricPrompt
|
import androidx.biometric.BiometricPrompt
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.getkeepsafe.taptargetview.TapTargetView
|
import com.getkeepsafe.taptargetview.TapTargetView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
@@ -37,8 +40,14 @@ import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
|||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||||
import com.kunzisoft.keepass.database.exception.IODatabaseException
|
import com.kunzisoft.keepass.database.exception.IODatabaseException
|
||||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||||
|
import com.kunzisoft.keepass.model.CipherDecryptDatabase
|
||||||
|
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||||
|
import com.kunzisoft.keepass.model.CredentialStorage
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
||||||
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
|
import com.kunzisoft.keepass.view.showByFading
|
||||||
|
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -56,12 +65,18 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
var databaseFileUri: Uri? = null
|
var databaseFileUri: Uri? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
// TODO Retrieve credential storage from app database
|
||||||
|
var credentialDatabaseStorage: CredentialStorage = CredentialStorage.DEFAULT
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manage setting to auto open biometric prompt
|
* Manage setting to auto open biometric prompt
|
||||||
*/
|
*/
|
||||||
private var mAutoOpenPrompt: Boolean = false
|
private var mAutoOpenPrompt: Boolean
|
||||||
get() {
|
get() {
|
||||||
return field && mAutoOpenPromptEnabled
|
return mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt && mAutoOpenPromptEnabled
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = value
|
||||||
}
|
}
|
||||||
|
|
||||||
// Variable to check if the prompt can be open (if the right activity is currently shown)
|
// Variable to check if the prompt can be open (if the right activity is currently shown)
|
||||||
@@ -72,6 +87,8 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
|
|
||||||
private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null
|
private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null
|
||||||
|
|
||||||
|
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by activityViewModels()
|
||||||
|
|
||||||
// Only to fix multiple fingerprint menu #332
|
// Only to fix multiple fingerprint menu #332
|
||||||
private var mAllowAdvancedUnlockMenu = false
|
private var mAllowAdvancedUnlockMenu = false
|
||||||
private var mAddBiometricMenuInProgress = false
|
private var mAddBiometricMenuInProgress = false
|
||||||
@@ -79,6 +96,15 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
// Only keep connection when we request a device credential activity
|
// Only keep connection when we request a device credential activity
|
||||||
private var keepConnection = false
|
private var keepConnection = false
|
||||||
|
|
||||||
|
private var mDeviceCredentialResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
|
||||||
|
// To wait resume
|
||||||
|
if (keepConnection) {
|
||||||
|
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = result.resultCode == Activity.RESULT_OK
|
||||||
|
}
|
||||||
|
keepConnection = false
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
|
|
||||||
@@ -97,10 +123,21 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
retainInstance = true
|
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
|
|
||||||
cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext)
|
cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext)
|
||||||
|
|
||||||
|
mAdvancedUnlockViewModel.onInitAdvancedUnlockModeRequested.observe(this) {
|
||||||
|
initAdvancedUnlockMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
mAdvancedUnlockViewModel.onUnlockAvailabilityCheckRequested.observe(this) {
|
||||||
|
checkUnlockAvailability()
|
||||||
|
}
|
||||||
|
|
||||||
|
mAdvancedUnlockViewModel.onDatabaseFileLoaded.observe(this) {
|
||||||
|
onDatabaseLoaded(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
@@ -114,17 +151,6 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
return rootView
|
return rootView
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class ActivityResult(var requestCode: Int, var resultCode: Int, var data: Intent?)
|
|
||||||
private var activityResult: ActivityResult? = null
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
// To wait resume
|
|
||||||
if (keepConnection) {
|
|
||||||
activityResult = ActivityResult(requestCode, resultCode, data)
|
|
||||||
}
|
|
||||||
keepConnection = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
context?.let {
|
context?.let {
|
||||||
@@ -154,32 +180,38 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadDatabase(databaseUri: Uri?, autoOpenPrompt: Boolean) {
|
private fun onDatabaseLoaded(databaseUri: Uri?) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
// To get device credential unlock result, only if same database uri
|
// To get device credential unlock result, only if same database uri
|
||||||
if (databaseUri != null
|
if (databaseUri != null
|
||||||
&& mAdvancedUnlockEnabled) {
|
&& mAdvancedUnlockEnabled) {
|
||||||
activityResult?.let {
|
val deviceCredentialAuthSucceeded = mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded
|
||||||
|
deviceCredentialAuthSucceeded?.let {
|
||||||
if (databaseUri == databaseFileUri) {
|
if (databaseUri == databaseFileUri) {
|
||||||
advancedUnlockManager?.onActivityResult(it.requestCode, it.resultCode)
|
if (deviceCredentialAuthSucceeded == true) {
|
||||||
|
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationSucceeded()
|
||||||
|
} else {
|
||||||
|
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationFailed()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
disconnect()
|
disconnect()
|
||||||
}
|
}
|
||||||
} ?: run {
|
} ?: run {
|
||||||
this.mAutoOpenPrompt = autoOpenPrompt
|
if (databaseUri != databaseFileUri) {
|
||||||
connect(databaseUri)
|
connect(databaseUri)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
disconnect()
|
disconnect()
|
||||||
}
|
}
|
||||||
activityResult = null
|
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check unlock availability and change the current mode depending of device's state
|
* Check unlock availability and change the current mode depending of device's state
|
||||||
*/
|
*/
|
||||||
fun checkUnlockAvailability() {
|
private fun checkUnlockAvailability() {
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
allowOpenBiometricPrompt = true
|
allowOpenBiometricPrompt = true
|
||||||
@@ -317,7 +349,8 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
if (cryptoPrompt.isDeviceCredentialOperation)
|
if (cryptoPrompt.isDeviceCredentialOperation)
|
||||||
keepConnection = true
|
keepConnection = true
|
||||||
try {
|
try {
|
||||||
advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt)
|
advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt,
|
||||||
|
mDeviceCredentialResultLauncher)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to open advanced unlock prompt", e)
|
Log.e(TAG, "Unable to open advanced unlock prompt", e)
|
||||||
setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
|
setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
|
||||||
@@ -369,8 +402,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
} ?: throw Exception("AdvancedUnlockManager not initialized")
|
} ?: throw Exception("AdvancedUnlockManager not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
private fun initAdvancedUnlockMode() {
|
||||||
fun initAdvancedUnlockMode() {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
mAllowAdvancedUnlockMenu = false
|
mAllowAdvancedUnlockMenu = false
|
||||||
try {
|
try {
|
||||||
@@ -444,6 +476,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
fun deleteEncryptedDatabaseKey() {
|
fun deleteEncryptedDatabaseKey() {
|
||||||
|
mAllowAdvancedUnlockMenu = false
|
||||||
advancedUnlockManager?.closeBiometricPrompt()
|
advancedUnlockManager?.closeBiometricPrompt()
|
||||||
databaseFileUri?.let { databaseUri ->
|
databaseFileUri?.let { databaseUri ->
|
||||||
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
|
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
|
||||||
@@ -452,6 +485,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
} ?: checkUnlockAvailability()
|
} ?: checkUnlockAvailability()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
|
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
|
||||||
@@ -503,24 +537,43 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {
|
override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {
|
||||||
databaseFileUri?.let { databaseUri ->
|
databaseFileUri?.let { databaseUri ->
|
||||||
mBuilderListener?.onCredentialEncrypted(databaseUri, encryptedValue, ivSpec)
|
mBuilderListener?.onCredentialEncrypted(
|
||||||
|
CipherEncryptDatabase().apply {
|
||||||
|
this.databaseUri = databaseUri
|
||||||
|
this.credentialStorage = credentialDatabaseStorage
|
||||||
|
this.encryptedValue = encryptedValue
|
||||||
|
this.specParameters = ivSpec
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleDecryptedResult(decryptedValue: String) {
|
override fun handleDecryptedResult(decryptedValue: ByteArray) {
|
||||||
// Load database directly with password retrieve
|
// Load database directly with password retrieve
|
||||||
databaseFileUri?.let {
|
databaseFileUri?.let { databaseUri ->
|
||||||
mBuilderListener?.onCredentialDecrypted(it, decryptedValue)
|
mBuilderListener?.onCredentialDecrypted(
|
||||||
|
CipherDecryptDatabase().apply {
|
||||||
|
this.databaseUri = databaseUri
|
||||||
|
this.credentialStorage = credentialDatabaseStorage
|
||||||
|
this.decryptedValue = decryptedValue
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
override fun onUnrecoverableKeyException(e: Exception) {
|
||||||
|
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
|
||||||
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
override fun onInvalidKeyException(e: Exception) {
|
override fun onInvalidKeyException(e: Exception) {
|
||||||
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
|
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
override fun onGenericException(e: Exception) {
|
override fun onGenericException(e: Exception) {
|
||||||
val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: ""
|
val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: ""
|
||||||
setAdvancedUnlockedMessageView(errorMessage)
|
setAdvancedUnlockedMessageView(errorMessage)
|
||||||
@@ -528,10 +581,13 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
|
|
||||||
private fun showViews(show: Boolean) {
|
private fun showViews(show: Boolean) {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
mAdvancedUnlockInfoView?.visibility = if (show)
|
if (show) {
|
||||||
View.VISIBLE
|
if (mAdvancedUnlockInfoView?.visibility != View.VISIBLE)
|
||||||
|
mAdvancedUnlockInfoView?.showByFading()
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
View.GONE
|
if (mAdvancedUnlockInfoView?.visibility == View.VISIBLE)
|
||||||
|
mAdvancedUnlockInfoView?.hideByFading()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -550,32 +606,13 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
private fun setAdvancedUnlockedMessageView(text: CharSequence) {
|
private fun setAdvancedUnlockedMessageView(text: CharSequence) {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
mAdvancedUnlockInfoView?.message = text
|
mAdvancedUnlockInfoView?.message = text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun performEducation(passwordActivityEducation: PasswordActivityEducation,
|
|
||||||
readOnlyEducationPerformed: Boolean,
|
|
||||||
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
|
|
||||||
onOuterViewClick: ((TapTargetView?) -> Unit)? = null) {
|
|
||||||
try {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
|
||||||
&& !readOnlyEducationPerformed) {
|
|
||||||
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(requireContext())
|
|
||||||
PreferencesUtil.isAdvancedUnlockEnable(requireContext())
|
|
||||||
&& (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
|
||||||
&& mAdvancedUnlockInfoView != null && mAdvancedUnlockInfoView?.visibility == View.VISIBLE
|
|
||||||
&& mAdvancedUnlockInfoView?.unlockIconImageView != null
|
|
||||||
&& passwordActivityEducation.checkAndPerformedBiometricEducation(mAdvancedUnlockInfoView!!.unlockIconImageView!!,
|
|
||||||
onEducationViewClick,
|
|
||||||
onOuterViewClick)
|
|
||||||
}
|
|
||||||
} catch (ignored: Exception) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class Mode {
|
enum class Mode {
|
||||||
BIOMETRIC_UNAVAILABLE,
|
BIOMETRIC_UNAVAILABLE,
|
||||||
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
|
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
|
||||||
@@ -587,10 +624,10 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface BuilderListener {
|
interface BuilderListener {
|
||||||
fun retrieveCredentialForEncryption(): String
|
fun retrieveCredentialForEncryption(): ByteArray
|
||||||
fun conditionToStoreCredential(): Boolean
|
fun conditionToStoreCredential(): Boolean
|
||||||
fun onCredentialEncrypted(databaseUri: Uri, encryptedCredential: String, ivSpec: String)
|
fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase)
|
||||||
fun onCredentialDecrypted(databaseUri: Uri, decryptedCredential: String)
|
fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
|
|||||||
@@ -19,15 +19,17 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.biometric
|
package com.kunzisoft.keepass.biometric
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.KeyguardManager
|
import android.app.KeyguardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.security.keystore.KeyGenParameterSpec
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
import android.security.keystore.KeyPermanentlyInvalidatedException
|
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||||
import android.security.keystore.KeyProperties
|
import android.security.keystore.KeyProperties
|
||||||
import android.util.Base64
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.biometric.BiometricManager.Authenticators.*
|
import androidx.biometric.BiometricManager.Authenticators.*
|
||||||
@@ -35,6 +37,7 @@ import androidx.biometric.BiometricPrompt
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
import java.security.UnrecoverableKeyException
|
import java.security.UnrecoverableKeyException
|
||||||
@@ -121,7 +124,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSecretKey(): SecretKey? {
|
@Synchronized private fun getSecretKey(): SecretKey? {
|
||||||
if (!isKeyManagerInitialized) {
|
if (!isKeyManagerInitialized) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -136,18 +139,24 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
// and the constrains (purposes) in the constructor of the Builder
|
// and the constrains (purposes) in the constructor of the Builder
|
||||||
keyGenerator?.init(
|
keyGenerator?.init(
|
||||||
KeyGenParameterSpec.Builder(
|
KeyGenParameterSpec.Builder(
|
||||||
ADVANCED_UNLOCK_KEYSTORE_KEY,
|
ADVANCED_UNLOCK_KEYSTORE_KEY,
|
||||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||||
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
|
.setBlockModes(ADVANCED_UNLOCK_BLOCKS_MODES)
|
||||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
|
.setEncryptionPaddings(ADVANCED_UNLOCK_ENCRYPTION_PADDING)
|
||||||
|
.apply {
|
||||||
// Require the user to authenticate with a fingerprint to authorize every use
|
// Require the user to authenticate with a fingerprint to authorize every use
|
||||||
// of the key, don't use it for device credential because it's the user authentication
|
// of the key, don't use it for device credential because it's the user authentication
|
||||||
.apply {
|
if (biometricUnlockEnable) {
|
||||||
if (biometricUnlockEnable) {
|
setUserAuthenticationRequired(true)
|
||||||
setUserAuthenticationRequired(true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.build())
|
// To store in the security chip
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||||
|
&& retrieveContext().packageManager.hasSystemFeature(
|
||||||
|
PackageManager.FEATURE_STRONGBOX_KEYSTORE)) {
|
||||||
|
setIsStrongBoxBacked(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.build())
|
||||||
keyGenerator?.generateKey()
|
keyGenerator?.generateKey()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -164,8 +173,12 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initEncryptData(actionIfCypherInit
|
@Synchronized fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,) {
|
||||||
: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
|
initEncryptData(actionIfCypherInit, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized private fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
|
||||||
|
firstLaunch: Boolean) {
|
||||||
if (!isKeyManagerInitialized) {
|
if (!isKeyManagerInitialized) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -185,28 +198,30 @@ class AdvancedUnlockManager(private var retrieveContext: () -> 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)
|
||||||
advancedUnlockCallback?.onInvalidKeyException(unrecoverableKeyException)
|
advancedUnlockCallback?.onUnrecoverableKeyException(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)
|
||||||
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
if (firstLaunch) {
|
||||||
|
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
|
||||||
|
initEncryptData(actionIfCypherInit, false)
|
||||||
|
} else {
|
||||||
|
advancedUnlockCallback?.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)
|
||||||
advancedUnlockCallback?.onGenericException(e)
|
advancedUnlockCallback?.onGenericException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun encryptData(value: String) {
|
@Synchronized fun encryptData(value: ByteArray) {
|
||||||
if (!isKeyManagerInitialized) {
|
if (!isKeyManagerInitialized) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val encrypted = cipher?.doFinal(value.toByteArray())
|
val encrypted = cipher?.doFinal(value) ?: byteArrayOf()
|
||||||
val encryptedBase64 = Base64.encodeToString(encrypted, Base64.NO_WRAP)
|
|
||||||
|
|
||||||
// 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)
|
advancedUnlockCallback?.handleEncryptedResult(encrypted, spec.iv)
|
||||||
advancedUnlockCallback?.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)
|
||||||
@@ -214,16 +229,20 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initDecryptData(ivSpecValue: String, actionIfCypherInit
|
@Synchronized fun initDecryptData(ivSpecValue: ByteArray,
|
||||||
: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
|
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
|
||||||
|
initDecryptData(ivSpecValue, actionIfCypherInit, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized private fun initDecryptData(ivSpecValue: ByteArray,
|
||||||
|
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
|
||||||
|
firstLaunch: Boolean = true) {
|
||||||
if (!isKeyManagerInitialized) {
|
if (!isKeyManagerInitialized) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// important to restore spec here that was used for decryption
|
// important to restore spec here that was used for decryption
|
||||||
val iv = Base64.decode(ivSpecValue, Base64.NO_WRAP)
|
val spec = IvParameterSpec(ivSpecValue)
|
||||||
val spec = IvParameterSpec(iv)
|
|
||||||
|
|
||||||
getSecretKey()?.let { secretKey ->
|
getSecretKey()?.let { secretKey ->
|
||||||
cipher?.let { cipher ->
|
cipher?.let { cipher ->
|
||||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
|
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
|
||||||
@@ -239,25 +258,34 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
}
|
}
|
||||||
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
||||||
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
|
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
|
||||||
deleteKeystoreKey()
|
if (firstLaunch) {
|
||||||
|
deleteKeystoreKey()
|
||||||
|
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
|
||||||
|
} else {
|
||||||
|
advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException)
|
||||||
|
}
|
||||||
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
||||||
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
|
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
|
||||||
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
if (firstLaunch) {
|
||||||
|
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
|
||||||
|
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
|
||||||
|
} else {
|
||||||
|
advancedUnlockCallback?.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)
|
||||||
advancedUnlockCallback?.onGenericException(e)
|
advancedUnlockCallback?.onGenericException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decryptData(encryptedValue: String) {
|
@Synchronized fun decryptData(encryptedValue: ByteArray) {
|
||||||
if (!isKeyManagerInitialized) {
|
if (!isKeyManagerInitialized) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// actual decryption here
|
// actual decryption here
|
||||||
val encrypted = Base64.decode(encryptedValue, Base64.NO_WRAP)
|
cipher?.doFinal(encryptedValue)?.let { decrypted ->
|
||||||
cipher?.doFinal(encrypted)?.let { decrypted ->
|
advancedUnlockCallback?.handleDecryptedResult(decrypted)
|
||||||
advancedUnlockCallback?.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)
|
||||||
@@ -268,7 +296,7 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteKeystoreKey() {
|
@Synchronized fun deleteKeystoreKey() {
|
||||||
try {
|
try {
|
||||||
keyStore?.load(null)
|
keyStore?.load(null)
|
||||||
keyStore?.deleteEntry(ADVANCED_UNLOCK_KEYSTORE_KEY)
|
keyStore?.deleteEntry(ADVANCED_UNLOCK_KEYSTORE_KEY)
|
||||||
@@ -278,9 +306,9 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Synchronized fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt,
|
||||||
@Synchronized
|
deviceCredentialResultLauncher: ActivityResultLauncher<Intent>
|
||||||
fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
|
) {
|
||||||
// Init advanced unlock prompt
|
// Init advanced unlock prompt
|
||||||
if (biometricPrompt == null) {
|
if (biometricPrompt == null) {
|
||||||
biometricPrompt = BiometricPrompt(retrieveContext(),
|
biometricPrompt = BiometricPrompt(retrieveContext(),
|
||||||
@@ -311,28 +339,19 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
}
|
}
|
||||||
else if (cryptoPrompt.isDeviceCredentialOperation) {
|
else if (cryptoPrompt.isDeviceCredentialOperation) {
|
||||||
val keyGuardManager = ContextCompat.getSystemService(retrieveContext(), KeyguardManager::class.java)
|
val keyGuardManager = ContextCompat.getSystemService(retrieveContext(), KeyguardManager::class.java)
|
||||||
retrieveContext().startActivityForResult(
|
@Suppress("DEPRECATION")
|
||||||
keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription),
|
deviceCredentialResultLauncher.launch(
|
||||||
REQUEST_DEVICE_CREDENTIAL)
|
keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized fun closeBiometricPrompt() {
|
||||||
fun onActivityResult(requestCode: Int, resultCode: Int) {
|
|
||||||
if (requestCode == REQUEST_DEVICE_CREDENTIAL) {
|
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
|
||||||
advancedUnlockCallback?.onAuthenticationSucceeded()
|
|
||||||
} else {
|
|
||||||
advancedUnlockCallback?.onAuthenticationFailed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun closeBiometricPrompt() {
|
|
||||||
biometricPrompt?.cancelAuthentication()
|
biometricPrompt?.cancelAuthentication()
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AdvancedUnlockErrorCallback {
|
interface AdvancedUnlockErrorCallback {
|
||||||
|
fun onUnrecoverableKeyException(e: Exception)
|
||||||
fun onInvalidKeyException(e: Exception)
|
fun onInvalidKeyException(e: Exception)
|
||||||
fun onGenericException(e: Exception)
|
fun onGenericException(e: Exception)
|
||||||
}
|
}
|
||||||
@@ -341,8 +360,8 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
fun onAuthenticationSucceeded()
|
fun onAuthenticationSucceeded()
|
||||||
fun onAuthenticationFailed()
|
fun onAuthenticationFailed()
|
||||||
fun onAuthenticationError(errorCode: Int, errString: CharSequence)
|
fun onAuthenticationError(errorCode: Int, errString: CharSequence)
|
||||||
fun handleEncryptedResult(encryptedValue: String, ivSpec: String)
|
fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray)
|
||||||
fun handleDecryptedResult(decryptedValue: String)
|
fun handleDecryptedResult(decryptedValue: ByteArray)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -355,8 +374,6 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
|
private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
|
||||||
private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
||||||
|
|
||||||
private const val REQUEST_DEVICE_CREDENTIAL = 556
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
fun canAuthenticate(context: Context): Int {
|
fun canAuthenticate(context: Context): Int {
|
||||||
return try {
|
return try {
|
||||||
@@ -445,9 +462,13 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
|
|
||||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {}
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {}
|
||||||
|
|
||||||
override fun handleEncryptedResult(encryptedValue: String, ivSpec: String) {}
|
override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {}
|
||||||
|
|
||||||
override fun handleDecryptedResult(decryptedValue: String) {}
|
override fun handleDecryptedResult(decryptedValue: ByteArray) {}
|
||||||
|
|
||||||
|
override fun onUnrecoverableKeyException(e: Exception) {
|
||||||
|
advancedCallback.onUnrecoverableKeyException(e)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onInvalidKeyException(e: Exception) {
|
override fun onInvalidKeyException(e: Exception) {
|
||||||
advancedCallback.onInvalidKeyException(e)
|
advancedCallback.onInvalidKeyException(e)
|
||||||
@@ -460,6 +481,33 @@ class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity)
|
|||||||
deleteKeystoreKey()
|
deleteKeystoreKey()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deleteAllEntryKeysInKeystoreForBiometric(activity: FragmentActivity) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
deleteEntryKeyInKeystoreForBiometric(
|
||||||
|
activity,
|
||||||
|
object : AdvancedUnlockErrorCallback {
|
||||||
|
fun showException(e: Exception) {
|
||||||
|
Toast.makeText(activity,
|
||||||
|
activity.getString(R.string.advanced_unlock_scanning_error, e.localizedMessage),
|
||||||
|
Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUnrecoverableKeyException(e: Exception) {
|
||||||
|
showException(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInvalidKeyException(e: Exception) {
|
||||||
|
showException(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onGenericException(e: Exception) {
|
||||||
|
showException(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ import com.kunzisoft.keepass.database.element.Database
|
|||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
open class AssignPasswordInDatabaseRunnable (
|
open class AssignMainCredentialInDatabaseRunnable (
|
||||||
context: Context,
|
context: Context,
|
||||||
database: Database,
|
database: Database,
|
||||||
protected val mDatabaseUri: Uri,
|
protected val mDatabaseUri: Uri,
|
||||||
@@ -43,7 +43,7 @@ open class AssignPasswordInDatabaseRunnable (
|
|||||||
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
|
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
|
||||||
|
|
||||||
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri)
|
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri)
|
||||||
database.retrieveMasterKey(mMainCredential.masterPassword, uriInputStream)
|
database.assignMasterKey(mMainCredential.masterPassword, uriInputStream)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
erase(mBackupKey)
|
erase(mBackupKey)
|
||||||
setError(e)
|
setError(e)
|
||||||
@@ -35,7 +35,7 @@ class CreateDatabaseRunnable(context: Context,
|
|||||||
private val templateGroupName: String?,
|
private val templateGroupName: String?,
|
||||||
mainCredential: MainCredential,
|
mainCredential: MainCredential,
|
||||||
private val createDatabaseResult: ((Result) -> Unit)?)
|
private val createDatabaseResult: ((Result) -> Unit)?)
|
||||||
: AssignPasswordInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) {
|
: AssignMainCredentialInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) {
|
||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ import android.os.Bundle
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
|
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
|
||||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
@@ -42,6 +42,7 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
|||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
|
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
@@ -53,6 +54,7 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
|||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MERGE_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK
|
||||||
@@ -82,7 +84,6 @@ import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
|
|||||||
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class to connect an activity or a service to the DatabaseTaskNotificationService,
|
* Utility class to connect an activity or a service to the DatabaseTaskNotificationService,
|
||||||
@@ -247,7 +248,7 @@ class DatabaseTaskProvider {
|
|||||||
private fun bindService() {
|
private fun bindService() {
|
||||||
initServiceConnection()
|
initServiceConnection()
|
||||||
serviceConnection?.let {
|
serviceConnection?.let {
|
||||||
context.bindService(intentDatabaseTask, it, BIND_AUTO_CREATE or BIND_NOT_FOREGROUND or BIND_ABOVE_CLIENT)
|
context.bindService(intentDatabaseTask, it, BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,18 +343,27 @@ class DatabaseTaskProvider {
|
|||||||
fun startDatabaseLoad(databaseUri: Uri,
|
fun startDatabaseLoad(databaseUri: Uri,
|
||||||
mainCredential: MainCredential,
|
mainCredential: MainCredential,
|
||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
cipherEntity: CipherDatabaseEntity?,
|
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||||
fixDuplicateUuid: Boolean) {
|
fixDuplicateUuid: Boolean) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||||
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
|
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
|
||||||
putParcelable(DatabaseTaskNotificationService.CIPHER_ENTITY_KEY, cipherEntity)
|
putParcelable(DatabaseTaskNotificationService.CIPHER_DATABASE_KEY, cipherEncryptDatabase)
|
||||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_LOAD_TASK)
|
, ACTION_DATABASE_LOAD_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startDatabaseMerge(fromDatabaseUri: Uri? = null,
|
||||||
|
mainCredential: MainCredential? = null) {
|
||||||
|
start(Bundle().apply {
|
||||||
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri)
|
||||||
|
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||||
|
}
|
||||||
|
, ACTION_DATABASE_MERGE_TASK)
|
||||||
|
}
|
||||||
|
|
||||||
fun startDatabaseReload(fixDuplicateUuid: Boolean) {
|
fun startDatabaseReload(fixDuplicateUuid: Boolean) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||||
@@ -361,6 +371,19 @@ class DatabaseTaskProvider {
|
|||||||
, ACTION_DATABASE_RELOAD_TASK)
|
, ACTION_DATABASE_RELOAD_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun askToStartDatabaseReload(conditionToAsk: Boolean, approved: () -> Unit) {
|
||||||
|
if (conditionToAsk) {
|
||||||
|
AlertDialog.Builder(context)
|
||||||
|
.setMessage(R.string.warning_database_info_reloaded)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
approved.invoke()
|
||||||
|
}.create().show()
|
||||||
|
} else {
|
||||||
|
approved.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun startDatabaseAssignPassword(databaseUri: Uri,
|
fun startDatabaseAssignPassword(databaseUri: Uri,
|
||||||
mainCredential: MainCredential) {
|
mainCredential: MainCredential) {
|
||||||
|
|
||||||
@@ -671,9 +694,10 @@ class DatabaseTaskProvider {
|
|||||||
/**
|
/**
|
||||||
* Save Database without parameter
|
* Save Database without parameter
|
||||||
*/
|
*/
|
||||||
fun startDatabaseSave(save: Boolean) {
|
fun startDatabaseSave(save: Boolean, saveToUri: Uri? = null) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, saveToUri)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_SAVE)
|
, ACTION_DATABASE_SAVE)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,12 +22,11 @@ package com.kunzisoft.keepass.database.action
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
|
||||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||||
|
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
@@ -39,7 +38,7 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
private val mUri: Uri,
|
private val mUri: Uri,
|
||||||
private val mMainCredential: MainCredential,
|
private val mMainCredential: MainCredential,
|
||||||
private val mReadonly: Boolean,
|
private val mReadonly: Boolean,
|
||||||
private val mCipherEntity: CipherDatabaseEntity?,
|
private val mCipherEncryptDatabase: CipherEncryptDatabase?,
|
||||||
private val mFixDuplicateUUID: Boolean,
|
private val mFixDuplicateUUID: Boolean,
|
||||||
private val progressTaskUpdater: ProgressTaskUpdater?,
|
private val progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||||
@@ -60,7 +59,6 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
{ memoryWanted ->
|
{ memoryWanted ->
|
||||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||||
},
|
},
|
||||||
LoadedKey.generateNewCipherKey(),
|
|
||||||
mFixDuplicateUUID,
|
mFixDuplicateUUID,
|
||||||
progressTaskUpdater)
|
progressTaskUpdater)
|
||||||
}
|
}
|
||||||
@@ -77,9 +75,9 @@ class LoadDatabaseRunnable(private val context: Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Register the biometric
|
// Register the biometric
|
||||||
mCipherEntity?.let { cipherDatabaseEntity ->
|
mCipherEncryptDatabase?.let { cipherDatabase ->
|
||||||
CipherDatabaseAction.getInstance(context)
|
CipherDatabaseAction.getInstance(context)
|
||||||
.addOrUpdateCipherDatabase(cipherDatabaseEntity) // return value not called
|
.addOrUpdateCipherDatabase(cipherDatabase) // return value not called
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the current time to init the lock timer
|
// Register the current time to init the lock timer
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.database.action
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
|
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
|
|
||||||
|
class MergeDatabaseRunnable(private val context: Context,
|
||||||
|
private val mDatabase: Database,
|
||||||
|
private val mDatabaseToMergeUri: Uri?,
|
||||||
|
private val mDatabaseToMergeMainCredential: MainCredential?,
|
||||||
|
private val progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
|
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||||
|
: ActionRunnable() {
|
||||||
|
|
||||||
|
override fun onStartRun() {
|
||||||
|
mDatabase.wasReloaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActionRun() {
|
||||||
|
try {
|
||||||
|
mDatabase.mergeData(mDatabaseToMergeUri,
|
||||||
|
mDatabaseToMergeMainCredential,
|
||||||
|
context.contentResolver,
|
||||||
|
{ memoryWanted ->
|
||||||
|
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||||
|
},
|
||||||
|
progressTaskUpdater
|
||||||
|
)
|
||||||
|
} catch (e: LoadDatabaseException) {
|
||||||
|
setError(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.isSuccess) {
|
||||||
|
// Register the current time to init the lock timer
|
||||||
|
PreferencesUtil.saveCurrentTime(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFinishRun() {
|
||||||
|
mLoadDatabaseResult?.invoke(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.database.action
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.element.binary.LoadedKey
|
|
||||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
@@ -35,23 +34,18 @@ class ReloadDatabaseRunnable(private val context: Context,
|
|||||||
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||||
: ActionRunnable() {
|
: ActionRunnable() {
|
||||||
|
|
||||||
private var tempCipherKey: LoadedKey? = null
|
|
||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
tempCipherKey = mDatabase.binaryCache.loadedCipherKey
|
|
||||||
// Clear before we load
|
// Clear before we load
|
||||||
mDatabase.clear(UriUtil.getBinaryDir(context))
|
mDatabase.clearIndexesAndBinaries(UriUtil.getBinaryDir(context))
|
||||||
mDatabase.wasReloaded = true
|
mDatabase.wasReloaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionRun() {
|
override fun onActionRun() {
|
||||||
try {
|
try {
|
||||||
mDatabase.reloadData(context.contentResolver,
|
mDatabase.reloadData(context.contentResolver,
|
||||||
UriUtil.getBinaryDir(context),
|
|
||||||
{ memoryWanted ->
|
{ memoryWanted ->
|
||||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||||
},
|
},
|
||||||
tempCipherKey ?: LoadedKey.generateNewCipherKey(),
|
|
||||||
progressTaskUpdater)
|
progressTaskUpdater)
|
||||||
} catch (e: LoadDatabaseException) {
|
} catch (e: LoadDatabaseException) {
|
||||||
setError(e)
|
setError(e)
|
||||||
@@ -61,7 +55,6 @@ class ReloadDatabaseRunnable(private val context: Context,
|
|||||||
// Register the current time to init the lock timer
|
// Register the current time to init the lock timer
|
||||||
PreferencesUtil.saveCurrentTime(context)
|
PreferencesUtil.saveCurrentTime(context)
|
||||||
} else {
|
} else {
|
||||||
tempCipherKey = null
|
|
||||||
mDatabase.clearAndClose(context)
|
mDatabase.clearAndClose(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||