Compare commits
1914 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
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 | ||
|
|
cc3204453e | ||
|
|
5ef8d3b7b9 | ||
|
|
2a9de97a19 | ||
|
|
9cecfed417 | ||
|
|
319715918a | ||
|
|
a3bf6e8b6d | ||
|
|
c4062658ce | ||
|
|
01a5de413e | ||
|
|
e4c22b1f29 | ||
|
|
fd05670dbc | ||
|
|
1ac094bfae | ||
|
|
b10e60126f | ||
|
|
ef1f27f421 | ||
|
|
0ed208675c | ||
|
|
00f7a0a194 | ||
|
|
935d4f4a64 | ||
|
|
dc4d88260d | ||
|
|
18934601da | ||
|
|
4ea811aeda | ||
|
|
f8fdecdc8f | ||
|
|
5467c61137 | ||
|
|
9c72b4cc56 | ||
|
|
9102217bc3 | ||
|
|
0e8fd7b2c4 | ||
|
|
fdf052cddb | ||
|
|
9a8d50ba6f | ||
|
|
136c97c312 | ||
|
|
bf00b88ef3 | ||
|
|
bafd1ea549 | ||
|
|
982618511b | ||
|
|
a4ad7ca3b1 | ||
|
|
99d71b57a4 | ||
|
|
1b2d8502e0 | ||
|
|
53e4ea9334 | ||
|
|
3ce704155c | ||
|
|
a06ea8fe55 | ||
|
|
31eb0fb48a | ||
|
|
d6a012e85f | ||
|
|
11c1cc7c72 | ||
|
|
6b7acb7bd5 | ||
|
|
bdebf19d7b | ||
|
|
cb1973ffb5 | ||
|
|
c6e2342ab4 | ||
|
|
2447599364 | ||
|
|
6a2cda74f1 | ||
|
|
8385d55d69 | ||
|
|
85e3464a15 | ||
|
|
6680039de7 | ||
|
|
9935826877 | ||
|
|
b977792168 | ||
|
|
2595cf87d8 | ||
|
|
f4342f1448 | ||
|
|
84c26b7c40 | ||
|
|
1cd7940a17 | ||
|
|
9514032f25 | ||
|
|
c7d6da2373 | ||
|
|
41b822fb6c | ||
|
|
69bf098c84 | ||
|
|
b4283ed4dc | ||
|
|
0fa0cac9e6 | ||
|
|
c71ef24052 | ||
|
|
cf0f665b14 | ||
|
|
2034e3ab78 | ||
|
|
89cfeec1b3 | ||
|
|
d8ae212df0 | ||
|
|
39b817bc69 | ||
|
|
09d79d52ae | ||
|
|
5c4b98d0e9 | ||
|
|
e5d6fc0604 | ||
|
|
5e656ebfba | ||
|
|
58fb75e55d | ||
|
|
e01621e658 | ||
|
|
b4aee17f53 | ||
|
|
dc70918648 | ||
|
|
69772edfa3 | ||
|
|
dd224cab05 | ||
|
|
b62873129e | ||
|
|
5052a1f564 | ||
|
|
2c36163e7a | ||
|
|
1bf912d6f0 | ||
|
|
d290259075 | ||
|
|
1689672faf | ||
|
|
8196e05679 | ||
|
|
fe0235da43 | ||
|
|
0895a73546 | ||
|
|
f06821e35b | ||
|
|
9cce5f645f | ||
|
|
c1a46408e9 | ||
|
|
21c3ccd637 | ||
|
|
3b9b034d80 | ||
|
|
348a5c3eb7 | ||
|
|
e3adaba3b3 | ||
|
|
4c5be658c3 | ||
|
|
b6517a449b | ||
|
|
ae0b8db0b0 | ||
|
|
56f0f8a299 | ||
|
|
c9af786b79 | ||
|
|
f34f615b80 | ||
|
|
aef2ef8479 | ||
|
|
7afbc9f5a4 | ||
|
|
df51b62041 | ||
|
|
045abc54fb | ||
|
|
9b2d9683eb | ||
|
|
3b0dd4a36c | ||
|
|
5e15f82313 | ||
|
|
d841c25bd3 | ||
|
|
8d3f1fe179 | ||
|
|
130ec130cc | ||
|
|
5e7a95eac0 | ||
|
|
a8cb49d12d | ||
|
|
c179ac626a | ||
|
|
041583bf96 | ||
|
|
ed710335b3 | ||
|
|
b556581a87 | ||
|
|
77a1b7918c | ||
|
|
45149e1b28 | ||
|
|
932338a25a | ||
|
|
925509e5a0 | ||
|
|
25646fbad7 | ||
|
|
e1733512c4 | ||
|
|
8379ffe1ce | ||
|
|
c77537ecee | ||
|
|
2192d97c69 | ||
|
|
a0dc76bda8 | ||
|
|
7fe177edc6 | ||
|
|
1f5e6f1e17 | ||
|
|
bf0aa295b0 | ||
|
|
649dffc3e0 | ||
|
|
a0f5ed66e2 | ||
|
|
7df3b95c22 | ||
|
|
0756474d40 | ||
|
|
60747db945 | ||
|
|
afcfad162e | ||
|
|
63f15bdc9e | ||
|
|
3b826869e9 | ||
|
|
af0256add0 | ||
|
|
b8d8cba12c | ||
|
|
616e9a0ec2 | ||
|
|
366434cbd7 | ||
|
|
f6d4046af6 | ||
|
|
82932f002e | ||
|
|
7593a05953 | ||
|
|
3026a9e3e4 | ||
|
|
362939eab9 | ||
|
|
61d52731a5 | ||
|
|
6aecc6521c | ||
|
|
ef5829593e | ||
|
|
4a8f67093f | ||
|
|
9cbe0664f6 | ||
|
|
965d6e4e8e | ||
|
|
eefdeb0bb7 | ||
|
|
a904a51293 | ||
|
|
cce377d70d | ||
|
|
5721bca5a3 | ||
|
|
bcd5b024f0 | ||
|
|
571f257c17 | ||
|
|
3451135800 | ||
|
|
f426a78a94 | ||
|
|
3d65236e63 | ||
|
|
7b51b5005a | ||
|
|
3d9cf16960 | ||
|
|
35def53666 | ||
|
|
5c46a89ddc | ||
|
|
4e429025bf | ||
|
|
95fae11eee | ||
|
|
9a22a9fb8b | ||
|
|
f60e2e2ca6 | ||
|
|
deb9101335 | ||
|
|
407f93ac43 | ||
|
|
78c39edceb | ||
|
|
c8445fb711 | ||
|
|
7c0e7347c8 | ||
|
|
12f37d0931 | ||
|
|
9a5086d9ba | ||
|
|
3222c7e677 | ||
|
|
632e0648d4 | ||
|
|
e3198031e3 | ||
|
|
0ced9c8e26 | ||
|
|
65f4a708cd | ||
|
|
36e7b00d9a | ||
|
|
8b2c48f5ca | ||
|
|
9f7a0d4f17 | ||
|
|
fa5ae17621 | ||
|
|
7a2536c559 | ||
|
|
96d2edb641 | ||
|
|
8a2bd23c32 | ||
|
|
d3b935ea7f | ||
|
|
e53bc3b048 | ||
|
|
5f1cfc9dda | ||
|
|
43207b316f | ||
|
|
96ed4c419a | ||
|
|
840a2253e2 | ||
|
|
18db9b0a77 | ||
|
|
6c7a5292a4 | ||
|
|
bef1c74226 | ||
|
|
176ec8bace | ||
|
|
c62064002f | ||
|
|
45b7800a68 | ||
|
|
fa761ac69b | ||
|
|
cc11e98aa6 | ||
|
|
8f1c71137a | ||
|
|
8fdf2dcb7a | ||
|
|
d4cd5b73bd | ||
|
|
77975aed2a | ||
|
|
a7700ce27e | ||
|
|
726ff1a126 | ||
|
|
e24269c452 | ||
|
|
686f4656ec | ||
|
|
55fe10d2dc | ||
|
|
422984ac41 | ||
|
|
706d117d80 | ||
|
|
13f8df4e0d | ||
|
|
263d433193 | ||
|
|
c15c11f3b1 | ||
|
|
524c8ccfc5 | ||
|
|
902392ea30 | ||
|
|
bef179187f | ||
|
|
ea7221c39a | ||
|
|
edaf9f6296 | ||
|
|
0d83725b77 | ||
|
|
6ce31305c6 | ||
|
|
90935c033d | ||
|
|
b4c3f831a7 | ||
|
|
f0e25e8198 | ||
|
|
d800082621 | ||
|
|
653d3da718 | ||
|
|
0f39409386 | ||
|
|
ccae0d1a57 | ||
|
|
257992d314 | ||
|
|
5eb843b63d | ||
|
|
3929b478a7 | ||
|
|
18734ed822 | ||
|
|
876e749b31 | ||
|
|
32b8c505d9 | ||
|
|
37a0dce7c5 | ||
|
|
2332f36b56 | ||
|
|
21cc9cc026 | ||
|
|
db4d76502e | ||
|
|
1578ea7590 | ||
|
|
7405de01fe | ||
|
|
77dc5943e5 | ||
|
|
f8a2748ede | ||
|
|
a99ca00bb3 | ||
|
|
6eb80eea2f | ||
|
|
82828f7f82 | ||
|
|
23c9a5963a | ||
|
|
7595f113ec | ||
|
|
e9e5a4ee0d | ||
|
|
1947fc3e83 | ||
|
|
8844689482 | ||
|
|
0d2ba54c10 | ||
|
|
a4bb5137ea | ||
|
|
b8aea1f97a | ||
|
|
120e1893bd | ||
|
|
836df52a50 | ||
|
|
00498aaeac | ||
|
|
f4f47cff75 | ||
|
|
40dc3d45fc | ||
|
|
b89d2a6da1 | ||
|
|
55a4af9f00 | ||
|
|
719d45e75e | ||
|
|
8703684740 | ||
|
|
e95e7218f6 | ||
|
|
e9d4711978 | ||
|
|
9309506e97 | ||
|
|
00d2a80e95 | ||
|
|
c1e62b7d90 | ||
|
|
84775d36dc | ||
|
|
fc4eb11fd8 | ||
|
|
ce70ce6c76 | ||
|
|
ffd404ec1b | ||
|
|
6f943db012 | ||
|
|
12342ac426 | ||
|
|
489ddc3f56 | ||
|
|
02a266cbea | ||
|
|
bdc9facd41 | ||
|
|
7d679aac0b | ||
|
|
ae1719e795 | ||
|
|
6cea05e9f4 | ||
|
|
cda84d4e64 | ||
|
|
b3f63a85d5 | ||
|
|
196903cb12 | ||
|
|
e6f082adfd | ||
|
|
bb2f641073 | ||
|
|
de6312d317 | ||
|
|
887b0f3119 | ||
|
|
f13c8d7884 | ||
|
|
a10fdb260d | ||
|
|
0f8b1790f3 | ||
|
|
531e345dd9 | ||
|
|
c1942759d4 | ||
|
|
eca9e573de | ||
|
|
61376f8d68 | ||
|
|
292e2c60e2 | ||
|
|
2ffaf81109 | ||
|
|
da37381678 | ||
|
|
3236bf6122 | ||
|
|
a352ae6922 | ||
|
|
f3631a6a09 | ||
|
|
0d376bd5b7 | ||
|
|
fd57bd0378 | ||
|
|
5a11882d1b | ||
|
|
0f041da548 | ||
|
|
00a23119c5 | ||
|
|
cdeb49473b | ||
|
|
b55b3731a5 | ||
|
|
4afbb688ba | ||
|
|
f289a921f1 | ||
|
|
f2165bc4c1 | ||
|
|
73b20bfe4a | ||
|
|
fbf2006e3f | ||
|
|
358b701396 | ||
|
|
d3caae3a2d | ||
|
|
300062d3ac | ||
|
|
bb0aaad383 | ||
|
|
e9aa8609f1 | ||
|
|
37cbb18626 | ||
|
|
3dad6daa2e | ||
|
|
f49485e161 | ||
|
|
f3f268742f | ||
|
|
99e76f2254 | ||
|
|
a7801e376b | ||
|
|
90000aa1fb | ||
|
|
a2a26cd058 | ||
|
|
e3cbefc5b6 | ||
|
|
c6ceb25ee8 | ||
|
|
022777888c | ||
|
|
ad5e644362 | ||
|
|
bf6cb04fe8 | ||
|
|
dbde23fb7b | ||
|
|
c9cb469d65 | ||
|
|
2e37c20e55 | ||
|
|
ade665d228 | ||
|
|
a62f0cfd3b | ||
|
|
8b867f78fe | ||
|
|
d6a43fd8e5 | ||
|
|
9887b8142d | ||
|
|
4d92d6dc2b | ||
|
|
d8cd84ed9e | ||
|
|
6bc740e881 | ||
|
|
db348cc368 | ||
|
|
6ebf59d7ff | ||
|
|
d35e31d128 | ||
|
|
b9b6d3d2cb | ||
|
|
728b111ac9 | ||
|
|
5afbfbfd43 | ||
|
|
9e5ce589ae | ||
|
|
f486150a0f | ||
|
|
075ee815f0 | ||
|
|
d321283b13 | ||
|
|
0ec72bb013 | ||
|
|
cdfbcd873c | ||
|
|
8be382fa7e | ||
|
|
82f5ab1446 | ||
|
|
c97f8c31ce | ||
|
|
92e5b5e9c3 | ||
|
|
a70fca493d | ||
|
|
42c8f0c345 | ||
|
|
d3d5a1745d | ||
|
|
8392d8b684 | ||
|
|
1abd13c6e0 | ||
|
|
e126b66e19 | ||
|
|
47449d93db | ||
|
|
13002f96f1 | ||
|
|
1985a035be | ||
|
|
cdfc7e4158 | ||
|
|
5050052710 | ||
|
|
48e39d2ffa | ||
|
|
a8d053e82a | ||
|
|
2154945c3c | ||
|
|
8d83a0a86a | ||
|
|
c196cdf405 | ||
|
|
b6a5f43176 | ||
|
|
008ded4a5c | ||
|
|
d476574d05 | ||
|
|
371b3813d4 | ||
|
|
e4c3c224d9 | ||
|
|
69bc697568 | ||
|
|
fe08d034bb | ||
|
|
18f4714410 | ||
|
|
1b6c416893 | ||
|
|
6153a28b4b | ||
|
|
9574cf16fb | ||
|
|
d309a67416 | ||
|
|
fb865af088 | ||
|
|
c1e7039357 | ||
|
|
0fd3b37641 | ||
|
|
cea91f7b2f | ||
|
|
3959896832 | ||
|
|
d55dccdeb1 | ||
|
|
c46c286b51 | ||
|
|
36b37b3b8f | ||
|
|
ce9931d8a3 | ||
|
|
66f5ff35d3 | ||
|
|
aa15d261f3 | ||
|
|
00a32463c7 | ||
|
|
dd60ff8b74 | ||
|
|
4588611cbf | ||
|
|
80c0152d46 | ||
|
|
fac727cd3d | ||
|
|
9c30183068 | ||
|
|
9c28d5c5c5 | ||
|
|
3c55e3a3f0 | ||
|
|
ba6e3b801d | ||
|
|
4064ab47ac | ||
|
|
b6005fcb56 | ||
|
|
71de526366 | ||
|
|
a3e5d8448b | ||
|
|
f65b6e5484 | ||
|
|
05d0d9e501 | ||
|
|
d107beadf2 | ||
|
|
9c2fd26579 | ||
|
|
d796ea6324 | ||
|
|
8386c9e729 | ||
|
|
00ca4524b5 | ||
|
|
f4d4853319 | ||
|
|
00678cc9ca | ||
|
|
2845486a3f | ||
|
|
a815e6447d | ||
|
|
5b71a30ee9 | ||
|
|
ac2d94420b | ||
|
|
1460c1364a | ||
|
|
37f38fe988 | ||
|
|
cf025b9135 | ||
|
|
283ff7a280 | ||
|
|
e668f016b4 | ||
|
|
256c2c955a | ||
|
|
d560c3e8de | ||
|
|
4f8e8e6669 | ||
|
|
7cdc2e0915 | ||
|
|
74b236b317 | ||
|
|
e344cafd9b | ||
|
|
4b0d16cad1 | ||
|
|
da4d8629bd | ||
|
|
68ae3b79ab | ||
|
|
78e0336c1b | ||
|
|
89ffeaf03b | ||
|
|
e439f4d643 | ||
|
|
8b779a0fca | ||
|
|
4b71dc8445 | ||
|
|
780875d5f2 | ||
|
|
7356d4b0e2 | ||
|
|
654dea6b7e | ||
|
|
1204374637 | ||
|
|
880fde2148 | ||
|
|
d776d76100 | ||
|
|
39d95105e1 | ||
|
|
2a7af826a8 | ||
|
|
9d2fd53073 | ||
|
|
4c99923467 | ||
|
|
edc5985a7e | ||
|
|
96fc79103b | ||
|
|
76879f3a73 | ||
|
|
38dac3803b | ||
|
|
c0d4ad2042 | ||
|
|
c669d5657a | ||
|
|
98c44fa578 | ||
|
|
328629fe88 | ||
|
|
9754535055 | ||
|
|
26f701d890 | ||
|
|
f3df8024e6 | ||
|
|
bc9fdfc7a4 | ||
|
|
e1e37989e4 | ||
|
|
dc67cb1807 | ||
|
|
2e8a2457bc | ||
|
|
6d4398c6fd | ||
|
|
9f67ad872d | ||
|
|
aba396f274 | ||
|
|
4315f34398 | ||
|
|
47b456e1ee | ||
|
|
529810b2dc | ||
|
|
6f67b8e788 | ||
|
|
a9c9d12444 | ||
|
|
f8428cec61 | ||
|
|
ac46bce807 | ||
|
|
23e899042d | ||
|
|
80c4a3c06d | ||
|
|
d2a31601ba | ||
|
|
7ddb4f3486 | ||
|
|
e56da87e0e | ||
|
|
2e4ebecf67 | ||
|
|
1b4ccaed91 | ||
|
|
1e2d41c7fb | ||
|
|
1b2ead054a | ||
|
|
468c1b95b7 | ||
|
|
60d8eff71f | ||
|
|
8baae8b801 | ||
|
|
839375fcf1 | ||
|
|
9f16f26347 | ||
|
|
69b4cacab4 | ||
|
|
b74b5040b1 | ||
|
|
a28decc854 | ||
|
|
ed82c36628 | ||
|
|
3536629dd9 | ||
|
|
c87696696e | ||
|
|
cb59cef1b8 | ||
|
|
b5ba03df4d | ||
|
|
d9b600466c | ||
|
|
0d37a59a5c | ||
|
|
4edf2f8cd1 | ||
|
|
c60dfdf0d7 | ||
|
|
006afc6841 | ||
|
|
f70879581d | ||
|
|
7a2d2b0376 | ||
|
|
b5368fa239 | ||
|
|
db1d71af9f | ||
|
|
6b03ef35a6 | ||
|
|
2afd02d86f | ||
|
|
b32c00f455 | ||
|
|
fbe9fb41ed | ||
|
|
cc0a7f7d76 | ||
|
|
db33fc60b9 | ||
|
|
6feaee4f86 | ||
|
|
1a27a31a32 | ||
|
|
5b611e71d5 | ||
|
|
6de88bfe11 | ||
|
|
6d7236249f | ||
|
|
69fbaba8a6 | ||
|
|
6d88737505 | ||
|
|
9869cfc736 | ||
|
|
8505326a68 | ||
|
|
3a4af88384 | ||
|
|
5b2e7d0f70 | ||
|
|
ddeea6bee3 | ||
|
|
0cfe3a7634 | ||
|
|
727463e4d1 | ||
|
|
d42abfdc56 | ||
|
|
e01ea1df4c | ||
|
|
078bfac5f5 | ||
|
|
111b07b9e6 | ||
|
|
dfbc89addc | ||
|
|
bf44da9a14 | ||
|
|
d75d13965b | ||
|
|
8aedebdc94 | ||
|
|
9388c4bb0d | ||
|
|
77d4f601af | ||
|
|
7fae590848 | ||
|
|
bc41558a26 | ||
|
|
f6651face4 | ||
|
|
345f00f7f2 | ||
|
|
e876d02118 | ||
|
|
5b7018f71b | ||
|
|
f45b3fc50a | ||
|
|
01196be30d | ||
|
|
0a2999bffb | ||
|
|
8f097096e7 | ||
|
|
cd97fc046a | ||
|
|
eeb10f31a6 | ||
|
|
9df5e116e8 | ||
|
|
1228a03d39 | ||
|
|
a5e1b3096e | ||
|
|
b41ae67128 | ||
|
|
ddfbe20125 | ||
|
|
0bfe9291dd | ||
|
|
622b2e1edc | ||
|
|
4a40719534 | ||
|
|
384993d363 | ||
|
|
01b7d28154 | ||
|
|
d7c4f5577f | ||
|
|
a69d23ca64 | ||
|
|
e2f8b7a6e3 | ||
|
|
171a0b012f | ||
|
|
5c04b15433 | ||
|
|
6397feffff | ||
|
|
e73b9b7f1c | ||
|
|
2d528db054 | ||
|
|
80c4ba6723 | ||
|
|
0d82e40c67 | ||
|
|
b75d6d02fa | ||
|
|
76d4542716 | ||
|
|
87955de849 | ||
|
|
6df60cf5da | ||
|
|
3c23a314f0 | ||
|
|
8fda6b04a4 | ||
|
|
9fa98e6b76 | ||
|
|
deb685f39b | ||
|
|
d7851d3a18 | ||
|
|
44946fc54a | ||
|
|
a033d10adc | ||
|
|
5a3e599fe0 | ||
|
|
a9f645f389 | ||
|
|
d662f0903a | ||
|
|
beaa947eb7 | ||
|
|
48006b64d6 | ||
|
|
8f195ba66f | ||
|
|
123288e745 | ||
|
|
5866e95d49 | ||
|
|
e79f395424 | ||
|
|
999ca87fec | ||
|
|
1217266d88 | ||
|
|
bb262198be | ||
|
|
11aae77caf | ||
|
|
8212cede6e | ||
|
|
a3c51884f4 | ||
|
|
b8890aca7f | ||
|
|
014b0cce14 | ||
|
|
6d860c5cb7 | ||
|
|
d8be832858 | ||
|
|
afcb9fcf41 | ||
|
|
3c7ae0aaf0 | ||
|
|
6b7f93dbfe | ||
|
|
c40b255022 | ||
|
|
1742d265f3 | ||
|
|
3240e0bcae | ||
|
|
ff185f6505 | ||
|
|
346b517c9d | ||
|
|
80f00aba0a | ||
|
|
949905f6e2 | ||
|
|
b9e26fecfd | ||
|
|
232682f4a8 | ||
|
|
de3b690d60 | ||
|
|
de69a78a98 | ||
|
|
1c341c34a3 | ||
|
|
33beb57e9d | ||
|
|
66eeadca0b | ||
|
|
a10d1c98a8 | ||
|
|
59ead4986f | ||
|
|
09f6c18189 | ||
|
|
a5cd6d5ac0 | ||
|
|
0f3ad7c8b1 | ||
|
|
0487dea7fc | ||
|
|
a6803bf0e3 | ||
|
|
8cac1ee284 | ||
|
|
196620e1bd | ||
|
|
43d6c76873 | ||
|
|
b864c39a0d | ||
|
|
818b975111 | ||
|
|
d5fbc8393f | ||
|
|
df9a71a63d | ||
|
|
7b5e9d2344 | ||
|
|
7fc2d95886 | ||
|
|
2ba8702787 | ||
|
|
78d3b369bb | ||
|
|
bb3620680b | ||
|
|
d4a45655ca | ||
|
|
c9c739fd52 | ||
|
|
2b359cc592 | ||
|
|
151b7a323d | ||
|
|
1063dc2b63 | ||
|
|
f9f59a6eb1 | ||
|
|
73156cc337 | ||
|
|
7d53607f49 | ||
|
|
7539945465 | ||
|
|
51df8e7bb1 | ||
|
|
17029ce67c | ||
|
|
8cedc313cf | ||
|
|
5afe3acac1 | ||
|
|
9887b58b71 | ||
|
|
ec8363ba6a | ||
|
|
fcfb71f13b | ||
|
|
3a12e431ff | ||
|
|
bc4ed8e123 | ||
|
|
445e9540a5 | ||
|
|
bbc2a2a9dd | ||
|
|
5117bc78b6 | ||
|
|
10bf149a07 | ||
|
|
0a976bd012 | ||
|
|
df31c43e59 | ||
|
|
c5d30b9b23 | ||
|
|
2e18beff27 | ||
|
|
25e0cec2cc | ||
|
|
16cc4c5c97 | ||
|
|
e5cfb6b7eb | ||
|
|
a882ba07e9 | ||
|
|
801f3f99aa | ||
|
|
2338b9b57d | ||
|
|
8ba396c693 | ||
|
|
1164022765 | ||
|
|
b0c5519da5 | ||
|
|
f5073238d8 | ||
|
|
3ffa89bfaf | ||
|
|
26d8b2fa22 | ||
|
|
6b17502694 | ||
|
|
a69d57a4f4 | ||
|
|
430bc6150f | ||
|
|
b888615e0d | ||
|
|
9fae343668 | ||
|
|
db467889b0 | ||
|
|
153b8d1f37 | ||
|
|
87858762d4 | ||
|
|
50d3282a65 | ||
|
|
aee0500b38 | ||
|
|
f2ef6eb94e | ||
|
|
151a5a7e73 | ||
|
|
6a088c58de | ||
|
|
23e7bf9f89 | ||
|
|
59f24206ad | ||
|
|
e45ef019c0 | ||
|
|
2830d3c8fa | ||
|
|
088816dfab | ||
|
|
453a29b81c | ||
|
|
e5cb160aa4 | ||
|
|
844588a0d4 | ||
|
|
cfcfd47705 | ||
|
|
f416c0ec7d | ||
|
|
78f707c07c | ||
|
|
d28a59a2fe | ||
|
|
edf3525a3f | ||
|
|
2b7fe35305 | ||
|
|
d5819ea4d0 | ||
|
|
1099126def | ||
|
|
4ba3a797e3 | ||
|
|
51b9bb88e5 | ||
|
|
fa376148bd | ||
|
|
452b9677da | ||
|
|
02a779f9a2 | ||
|
|
ea60645247 | ||
|
|
b444a13285 | ||
|
|
0c6b2a13eb | ||
|
|
e10bdc1169 | ||
|
|
7512cffca3 | ||
|
|
203440e9b8 | ||
|
|
145030e854 | ||
|
|
2b81dfb100 | ||
|
|
c96ace5281 | ||
|
|
520c6b60be | ||
|
|
492382d552 | ||
|
|
7ca55dd531 | ||
|
|
ce4ba73fc4 | ||
|
|
5622d92cbb | ||
|
|
8de1c5fd36 | ||
|
|
6c84fea8dc | ||
|
|
0b94070086 | ||
|
|
2f209182f5 | ||
|
|
d7bc572f3e | ||
|
|
4985b49194 | ||
|
|
a792df2021 | ||
|
|
a42ec74723 | ||
|
|
de3dbe3b36 | ||
|
|
874fdb7da0 | ||
|
|
3bf0de3888 | ||
|
|
d4020c5e0f | ||
|
|
6175fc00ad | ||
|
|
76e996f429 | ||
|
|
fd222b73ce | ||
|
|
a8cc0b1edf | ||
|
|
ea2f3545a6 | ||
|
|
0cf136712a | ||
|
|
34a453873a | ||
|
|
0acac3b096 | ||
|
|
37141410e0 | ||
|
|
69b0e276e3 | ||
|
|
5872376f50 | ||
|
|
075f72d9f6 | ||
|
|
4441ec1b14 | ||
|
|
0735cc1a54 | ||
|
|
dea2ad6904 | ||
|
|
0f0b6b4a8a | ||
|
|
174e562dcb | ||
|
|
f080750545 | ||
|
|
621415fe51 | ||
|
|
428c2818a5 | ||
|
|
5aa1c70999 | ||
|
|
3508f47842 | ||
|
|
62d4993e6d | ||
|
|
ede6070e43 | ||
|
|
a61b1d4337 | ||
|
|
631d946dcf | ||
|
|
8945334f37 | ||
|
|
af2df11a56 | ||
|
|
6dc0c42b1e | ||
|
|
0328293746 | ||
|
|
ad406947cf | ||
|
|
6bb4c1171f | ||
|
|
0f8c71a9df | ||
|
|
faa39190fc | ||
|
|
7c0b925c96 | ||
|
|
1b8c453fd0 | ||
|
|
8cc8f595bd | ||
|
|
cb0b6e010d | ||
|
|
23dc7be1ab | ||
|
|
4b14ad07d2 | ||
|
|
9a5a8ae23a | ||
|
|
02b27e235c | ||
|
|
8a4bf7896f | ||
|
|
208ea29643 | ||
|
|
7c52ec731a | ||
|
|
c6ee38e435 | ||
|
|
65253cc5b9 | ||
|
|
d1a1a23cbc | ||
|
|
e8bb3a5ba7 | ||
|
|
22b8f82770 | ||
|
|
5874c5b9cb | ||
|
|
fad09b2cd5 | ||
|
|
cfb08afd7d | ||
|
|
16d939c601 | ||
|
|
af072648c1 | ||
|
|
45d2609494 | ||
|
|
f5ea65f18c | ||
|
|
e9fc6bed23 | ||
|
|
ccc8e4664d | ||
|
|
651ef04137 | ||
|
|
063aba333c | ||
|
|
a3a517ff89 | ||
|
|
40da29b681 | ||
|
|
684d81c895 | ||
|
|
c6ddd3b238 | ||
|
|
3186413bee | ||
|
|
aae1c4cf1c | ||
|
|
e64f264f12 | ||
|
|
08cf747f52 | ||
|
|
383437a3c7 | ||
|
|
c7cba3f50b | ||
|
|
7feb499d50 | ||
|
|
2e6c25b651 | ||
|
|
192903e8d7 | ||
|
|
6f72ade4d4 | ||
|
|
7e3fc0fa59 | ||
|
|
4ea896b57c | ||
|
|
073ccb9b52 | ||
|
|
f32c944d31 | ||
|
|
acd1e3bdfc | ||
|
|
774cbdf0fe | ||
|
|
f5fd527590 | ||
|
|
7ac9a7e94a | ||
|
|
5735f7a945 | ||
|
|
f8f423b5c1 | ||
|
|
81ba7f0721 | ||
|
|
e6be8c23fb | ||
|
|
9cc1764a18 | ||
|
|
6a77adc313 | ||
|
|
e532572d5a | ||
|
|
dcae49c5f8 | ||
|
|
7321c01e8c | ||
|
|
a85b9998c3 | ||
|
|
30d2ce43d1 | ||
|
|
d46edfc9b7 | ||
|
|
79d11138e6 | ||
|
|
f5cd019b6c | ||
|
|
42c4de56fd | ||
|
|
44b3c28a2a | ||
|
|
e5184a1568 | ||
|
|
76d60ded4c | ||
|
|
d2e7e925f7 | ||
|
|
6357a30acb | ||
|
|
4b1fdd0e38 | ||
|
|
227fc060b9 | ||
|
|
32e5aba906 | ||
|
|
55013bb220 | ||
|
|
5544b20d7f | ||
|
|
d6c7f9c68b | ||
|
|
8b5004e500 | ||
|
|
d6cf11b87d | ||
|
|
d4c3a3be6b | ||
|
|
e724b188ef | ||
|
|
ebf92b1103 | ||
|
|
42e2a49af6 | ||
|
|
be7cd3275a | ||
|
|
966df11beb | ||
|
|
fad852f00d | ||
|
|
9c3e6eb823 | ||
|
|
8b88f72efc | ||
|
|
e0aab6cfbf | ||
|
|
29a2e60e05 | ||
|
|
12df74b3a7 | ||
|
|
22d943c9e2 | ||
|
|
5839f51f44 | ||
|
|
4ecb8d4483 | ||
|
|
a08035551a | ||
|
|
c2460d7262 | ||
|
|
4776eac07e | ||
|
|
4952d107dd | ||
|
|
b5d6ee9dee | ||
|
|
e7a30c6024 | ||
|
|
6578e52ec5 | ||
|
|
97508beb5c | ||
|
|
e063b0d6fc | ||
|
|
bb65dc0e81 | ||
|
|
09e00ec119 | ||
|
|
985f8fad3b | ||
|
|
a9accc8c42 | ||
|
|
41316d2bd3 | ||
|
|
2c172eb8d3 | ||
|
|
d49827f9f8 | ||
|
|
b2aafda2b1 | ||
|
|
b4c50e0262 | ||
|
|
fbe51c12c1 | ||
|
|
991959416b | ||
|
|
41f0e61f60 | ||
|
|
eab8cd101f | ||
|
|
97765d798c | ||
|
|
a7f76248ac | ||
|
|
8de670fcf2 | ||
|
|
744823cce4 | ||
|
|
d95a3e00aa | ||
|
|
ccc190f7b0 | ||
|
|
3e6cd98cb9 | ||
|
|
0e3b8fdbb6 | ||
|
|
5aa3f79616 | ||
|
|
42427d0690 | ||
|
|
1a7b32e6d1 | ||
|
|
83555bfdc5 | ||
|
|
03990c1dd9 | ||
|
|
b361be5cb0 | ||
|
|
d02f6d1e67 | ||
|
|
1e56c34e2f | ||
|
|
2a2f8dcecd | ||
|
|
b609ed3ad4 | ||
|
|
80521f8ec2 | ||
|
|
3a5df6a893 | ||
|
|
0e60c4f910 | ||
|
|
f5f2d3c883 | ||
|
|
3c830bfaf2 | ||
|
|
98caf9b5bf | ||
|
|
cfbb8fab1b | ||
|
|
3069e5e566 | ||
|
|
ac050a09e8 | ||
|
|
78406ccdbf | ||
|
|
2efea1bb00 | ||
|
|
0157a160f0 | ||
|
|
eb4084a6a4 | ||
|
|
63f5e5416f | ||
|
|
38e0433e8f | ||
|
|
fc5ffc5f62 | ||
|
|
460e3558a9 | ||
|
|
b06f223124 | ||
|
|
aabfb2adfd | ||
|
|
f9c47c9035 | ||
|
|
e9485ebf56 | ||
|
|
fcd7fd2889 | ||
|
|
279f4a347a | ||
|
|
b29fe23403 | ||
|
|
1972a551e9 | ||
|
|
7e2ffd2ec4 | ||
|
|
06793ae13e | ||
|
|
26e961d356 | ||
|
|
da956d3bd5 | ||
|
|
318a72a123 | ||
|
|
5c4b4864d2 | ||
|
|
4ab31fe21a | ||
|
|
c22a213635 | ||
|
|
aa166a0104 | ||
|
|
2a36626731 | ||
|
|
2f2360fd48 | ||
|
|
e466643229 | ||
|
|
a9044c3dc4 | ||
|
|
2d28cc21a0 | ||
|
|
ebb0e7e118 | ||
|
|
8205454858 | ||
|
|
e909112ab8 | ||
|
|
469923855a | ||
|
|
f2aca08886 | ||
|
|
9900f8fecb | ||
|
|
73224887b9 | ||
|
|
e31574015b | ||
|
|
ef26251469 | ||
|
|
798bce2759 | ||
|
|
88fee5f6de | ||
|
|
937695a1e5 | ||
|
|
a43b580d67 | ||
|
|
b8d0bff22b | ||
|
|
c180d38394 | ||
|
|
10f7d955ff | ||
|
|
143651099a | ||
|
|
63bb12269f | ||
|
|
00ee80184e | ||
|
|
60ba058515 | ||
|
|
1cf8131b6c | ||
|
|
1b38bd59ef | ||
|
|
2e409c3246 | ||
|
|
63fbca8029 | ||
|
|
b37966f79c | ||
|
|
bac5b0de5b | ||
|
|
5dac161553 | ||
|
|
528c167a88 | ||
|
|
cfe01aa996 | ||
|
|
0f53c975cc | ||
|
|
b7b99c77c8 | ||
|
|
1eea5412a5 | ||
|
|
4240465930 | ||
|
|
3dfbf7d2ad | ||
|
|
440490a4bb | ||
|
|
746382811b | ||
|
|
967a54dd50 | ||
|
|
289d9a2531 | ||
|
|
c56b4964fe | ||
|
|
79dbb942f9 | ||
|
|
a5bb5635d3 | ||
|
|
3efe43c0fe | ||
|
|
5fb5299d34 | ||
|
|
b988882251 | ||
|
|
85e82e3fb9 | ||
|
|
35cfe261d2 | ||
|
|
fe4faf9ebc | ||
|
|
751392d656 | ||
|
|
2e5ce5e94f | ||
|
|
535eeb2594 | ||
|
|
8f5e0e93ee | ||
|
|
6f17c5dcac | ||
|
|
4e7c7ba8ce | ||
|
|
6bd5b2345c | ||
|
|
63832ef8fd | ||
|
|
3719bf3593 | ||
|
|
7bca41ca72 | ||
|
|
3f6a9c3af5 | ||
|
|
309380bdd5 | ||
|
|
f135bdb905 | ||
|
|
2b926fd157 | ||
|
|
e911eea69c | ||
|
|
9d182b8299 | ||
|
|
61366e000f | ||
|
|
21cf49f4f8 | ||
|
|
30f0de83d3 | ||
|
|
42278a4b66 | ||
|
|
8752f92cea | ||
|
|
064c468e62 | ||
|
|
ef78fb749c | ||
|
|
a5d1db392b | ||
|
|
6234fc2ca3 | ||
|
|
c9cf90cdc9 | ||
|
|
5b033975b6 | ||
|
|
5663a153f7 | ||
|
|
4b73c45e65 | ||
|
|
ac248d8b73 | ||
|
|
726b0d0fa3 | ||
|
|
2d40164549 | ||
|
|
5203152f78 | ||
|
|
b064bb74cd | ||
|
|
843d8e8e77 | ||
|
|
c5f95b243d | ||
|
|
06f1f4c8ad | ||
|
|
9847f834c2 | ||
|
|
b744d58e6c | ||
|
|
c95358b344 | ||
|
|
2f15d6c9f2 | ||
|
|
d13aa047d5 | ||
|
|
7590d18c67 | ||
|
|
a689116c97 | ||
|
|
3bf7459f05 | ||
|
|
c70faaedd1 | ||
|
|
cb2417fbe4 | ||
|
|
65dd996f2e | ||
|
|
01790a6f31 | ||
|
|
8b84bb893d | ||
|
|
8cb99847c5 | ||
|
|
b3e01277d4 | ||
|
|
b10d407659 | ||
|
|
b7c5a5d238 | ||
|
|
90d1ce63e8 | ||
|
|
9e30b4e5f7 | ||
|
|
e541a8c629 | ||
|
|
0afe25c922 | ||
|
|
bca133430f | ||
|
|
9c925518a7 | ||
|
|
18e79b99e7 | ||
|
|
4e02846df9 | ||
|
|
2268b78bba | ||
|
|
ea8acd0677 | ||
|
|
9e931dd03f | ||
|
|
19e3aabca4 | ||
|
|
ae697d82d5 | ||
|
|
33382273c3 | ||
|
|
7d8466d77a | ||
|
|
5ca08a00d2 | ||
|
|
df3942697e | ||
|
|
5700ca5bcf | ||
|
|
54b2419d64 | ||
|
|
d8b1c94b78 | ||
|
|
2d1ffc23b9 | ||
|
|
e6b33d60c3 | ||
|
|
3fd06890d7 | ||
|
|
4af4ad7663 | ||
|
|
6ca8501e28 | ||
|
|
432b385f60 | ||
|
|
6cebdefa4a | ||
|
|
bc665eb83d | ||
|
|
cb187300fe | ||
|
|
da761614bd | ||
|
|
f34e007ecd | ||
|
|
3b6ad080b4 | ||
|
|
9919e90ba5 | ||
|
|
f4af44925b | ||
|
|
4bb366b568 | ||
|
|
7e7ab4ce19 | ||
|
|
4d833d25ce | ||
|
|
a9c508ecd9 | ||
|
|
ef4dbb8fdb | ||
|
|
9eb66face5 | ||
|
|
3fd13f3e3b | ||
|
|
319c9cad4b | ||
|
|
c12297c98d | ||
|
|
7c38361844 | ||
|
|
559554a975 | ||
|
|
7e2ffa2124 | ||
|
|
66dbac4bb2 | ||
|
|
8b6a843a85 | ||
|
|
976cff2751 | ||
|
|
f7c30fa8eb | ||
|
|
7757c8218b | ||
|
|
2928b7daa3 | ||
|
|
3a55dea276 | ||
|
|
2a25213d66 | ||
|
|
035ffd8135 | ||
|
|
b040487f1f | ||
|
|
6fc821aecf | ||
|
|
cdceb1fb6f | ||
|
|
07d185913d | ||
|
|
f2a245a9c8 | ||
|
|
33338f4759 | ||
|
|
f7a4370b29 | ||
|
|
d619e089c0 | ||
|
|
3c50348a79 | ||
|
|
167ea3b82b | ||
|
|
9eda3e62f7 | ||
|
|
99c4319b51 | ||
|
|
790b25db65 | ||
|
|
45da17adb8 | ||
|
|
58d10672ea |
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]
|
||||||
|
|||||||
191
CHANGELOG
@@ -1,3 +1,194 @@
|
|||||||
|
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)
|
||||||
|
* Add / Manage dynamic templates #191
|
||||||
|
* Manually select RecycleBin group and Templates group #191
|
||||||
|
* Setting to display OTP Token in list #655
|
||||||
|
* Fix timeout in dialogs #716
|
||||||
|
* Check URI permissions #626
|
||||||
|
* Better autofill implementation #943 #946 #984 #1070 (Thx @uduerholz)
|
||||||
|
* Improvements #680 #1035 #1043 #942 #1021 #1027 #1046 #1082 #1083 (Thx @chenxiaolong)
|
||||||
|
|
||||||
|
KeePassDX(2.10.5)
|
||||||
|
* Increase the saving speed of database #1028
|
||||||
|
* Fix advanced unlocking by device credential #1029
|
||||||
|
|
||||||
|
KeePassDX(2.10.4)
|
||||||
|
* Hot fix to increase the opening speed of database #1028
|
||||||
|
|
||||||
|
KeePassDX(2.10.3)
|
||||||
|
* Improve Magikeyboard options description #1022 #1023 (Thx @djibux)
|
||||||
|
* Fix database opened without notification (database is now closed when screen is killed in background #1025)
|
||||||
|
* Fix biometric prompt #1018
|
||||||
|
|
||||||
|
KeePassDX(2.10.2)
|
||||||
|
* Fix search fields references #987
|
||||||
|
* Fix Auto-Types with same key #997
|
||||||
|
|
||||||
|
KeePassDX(2.10.1)
|
||||||
|
* Fix parcelable with custom data #986
|
||||||
|
|
||||||
|
KeePassDX(2.10.0)
|
||||||
|
* Manage new database format 4.1 #956
|
||||||
|
* Fix show button consistency #980
|
||||||
|
* Fix persistent notification #979
|
||||||
|
|
||||||
|
KeePassDX(2.9.20)
|
||||||
|
* Fix search with non-latin chars #971
|
||||||
|
* Fix action mode with search #972 (rollback ignore accents #945)
|
||||||
|
* Fix timeout with 0s #974
|
||||||
|
|
||||||
|
KeePassDX(2.9.19)
|
||||||
|
* Fix search slowdown #964
|
||||||
|
* Fix closing notification after lock request #965
|
||||||
|
* Better temp advanced unlocking code implementation #965
|
||||||
|
* Fix OTP token generation #967
|
||||||
|
|
||||||
|
KeePassDX(2.9.18)
|
||||||
|
* Move groups #658
|
||||||
|
* Improve autofill recognition #960
|
||||||
|
* Remove diacritical marks in search string #945
|
||||||
|
* Fix search in references #962
|
||||||
|
* Fix themes in Libre version
|
||||||
|
|
||||||
|
KeePassDX(2.9.17)
|
||||||
|
* Import / Export app properties #839
|
||||||
|
* Force twofish padding compatibility #955
|
||||||
|
* Better timeout preference #579
|
||||||
|
|
||||||
|
KeePassDX(2.9.16)
|
||||||
|
* Fix small bugs #948
|
||||||
|
|
||||||
|
KeePassDX(2.9.15)
|
||||||
|
* Fix themes #935 #926
|
||||||
|
* Decrease default clipboard time #934
|
||||||
|
* Better opening performance #929 #933
|
||||||
|
* Fix memory usage setting #941
|
||||||
|
|
||||||
|
KeePassDX(2.9.14)
|
||||||
|
* Add custom icons #96
|
||||||
|
* Dark Themes #532 #714
|
||||||
|
* Fix binary deduplication #715
|
||||||
|
* Fix IconId #901
|
||||||
|
* Resize image stream dynamically to prevent slowdown #919
|
||||||
|
* Small changes #795 #900 #903 #909 #914
|
||||||
|
|
||||||
|
KeePassDX(2.9.13)
|
||||||
|
* Binary image viewer #473 #749
|
||||||
|
* Fix TOTP plugin settings #878
|
||||||
|
* Allow Emoji #796
|
||||||
|
* Scroll and better UI in entry edition screen #876
|
||||||
|
* Better UI #876
|
||||||
|
* Fix themes and add Purple Dark #889
|
||||||
|
* Allow OTP with many padding #585
|
||||||
|
* Add notes in groups #734
|
||||||
|
|
||||||
|
KeePassDX(2.9.12)
|
||||||
|
* Fix OTP token type #863
|
||||||
|
* Fix auto open biometric prompt #862
|
||||||
|
* Fix back appearance setting #865
|
||||||
|
* Fix orientation change in settings #872
|
||||||
|
* Change memory unit to MiB #851
|
||||||
|
* Small changes #642
|
||||||
|
|
||||||
|
KeePassDX(2.9.11)
|
||||||
|
* Add Keyfile XML version 2 (fix hex) #844
|
||||||
|
* Fix hex Keyfile #861
|
||||||
|
|
||||||
KeePassDX(2.9.10)
|
KeePassDX(2.9.10)
|
||||||
* Try to fix autofill #852
|
* Try to fix autofill #852
|
||||||
* Fix database change dialog displayed too often #853
|
* Fix database change dialog displayed too often #853
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
106
app/build.gradle
@@ -1,23 +1,23 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-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.3.6528147'
|
ndkVersion "21.4.7075529"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 14
|
minSdkVersion 15
|
||||||
targetSdkVersion 30
|
targetSdkVersion 31
|
||||||
versionCode = 54
|
versionCode = 111
|
||||||
versionName = "2.9.10"
|
versionName = "3.4.2"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
testInstrumentationRunner = "android.test.InstrumentationTestRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
|
buildConfigField "String[]", "ICON_PACKS", "{\"classic\",\"material\"}"
|
||||||
manifestPlaceholders = [ googleAndroidBackupAPIKey:"unused" ]
|
manifestPlaceholders = [ googleAndroidBackupAPIKey:"unused" ]
|
||||||
@@ -30,12 +30,6 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
path "src/main/jni/CMakeLists.txt"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled = false
|
minifyEnabled = false
|
||||||
@@ -49,28 +43,32 @@ 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", "{\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
buildConfigField "String[]", "STYLES_DISABLED",
|
||||||
|
"{\"KeepassDXStyle_Red\"," +
|
||||||
|
"\"KeepassDXStyle_Red_Night\"," +
|
||||||
|
"\"KeepassDXStyle_Reply\"," +
|
||||||
|
"\"KeepassDXStyle_Reply_Night\"," +
|
||||||
|
"\"KeepassDXStyle_Purple\"," +
|
||||||
|
"\"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", "{\"KeepassDXStyle_Blue\",\"KeepassDXStyle_Red\",\"KeepassDXStyle_Purple\"}"
|
buildConfigField "String[]", "STYLES_DISABLED",
|
||||||
|
"{\"KeepassDXStyle_Simple\"," +
|
||||||
|
"\"KeepassDXStyle_Simple_Night\"," +
|
||||||
|
"\"KeepassDXStyle_Blue\"," +
|
||||||
|
"\"KeepassDXStyle_Blue_Night\"," +
|
||||||
|
"\"KeepassDXStyle_Red\"," +
|
||||||
|
"\"KeepassDXStyle_Red_Night\"," +
|
||||||
|
"\"KeepassDXStyle_Reply\"," +
|
||||||
|
"\"KeepassDXStyle_Reply_Night\"," +
|
||||||
|
"\"KeepassDXStyle_Purple\"," +
|
||||||
|
"\"KeepassDXStyle_Purple_Dark\"}"
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||||
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
|
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
|
||||||
}
|
}
|
||||||
@@ -78,10 +76,13 @@ 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'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests.includeAndroidResources = true
|
||||||
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
@@ -92,42 +93,49 @@ 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-beta01'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'androidx.biometric:biometric:1.1.0-rc01'
|
implementation 'androidx.biometric:biometric:1.1.0'
|
||||||
|
implementation 'androidx.media:media:1.5.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: To upgrade with style, bug in edit text
|
implementation "com.google.android.material:material:$android_material_version"
|
||||||
implementation 'com.google.android.material:material:1.0.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-rc01"
|
implementation "androidx.autofill:autofill:1.1.0"
|
||||||
// Crypto
|
|
||||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.65.01'
|
|
||||||
// Time
|
// Time
|
||||||
implementation 'joda-time:joda-time:2.10.6'
|
implementation 'joda-time:joda-time:2.10.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 Collections
|
// Apache Commons
|
||||||
implementation 'commons-collections:commons-collections:3.2.2'
|
implementation 'commons-io:commons-io:2.8.0'
|
||||||
// Apache Commons Codec
|
|
||||||
implementation 'commons-codec:commons-codec:1.15'
|
implementation 'commons-codec:commons-codec:1.15'
|
||||||
|
// Password generator
|
||||||
|
implementation 'me.gosimple:nbvcxz:1.5.0'
|
||||||
|
// Encrypt lib
|
||||||
|
implementation project(path: ':crypto')
|
||||||
// Icon pack
|
// Icon pack
|
||||||
implementation project(path: ':icon-pack-classic')
|
implementation project(path: ':icon-pack-classic')
|
||||||
implementation project(path: ':icon-pack-material')
|
implementation project(path: ':icon-pack-material')
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
androidTestImplementation 'junit:junit:4.13'
|
androidTestImplementation "androidx.test:runner:$android_test_version"
|
||||||
|
androidTestImplementation "androidx.test:rules:$android_test_version"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
app/src/androidTest/assets/test_image.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
133
app/src/androidTest/assets/test_text.txt
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
Basic Latin
|
||||||
|
! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~
|
||||||
|
Latin-1 Supplement
|
||||||
|
¡ ¢ £ ¤ ¥ ¦ § ¨ © ª « ¬ ® ¯ ° ± ² ³ ´ µ ¶ · ¸ ¹ º » ¼ ½ ¾ ¿ À Á Â Ã Ä Å Æ Ç È É Ê Ë Ì Í Î Ï Ð Ñ Ò Ó Ô Õ Ö × Ø Ù Ú Û Ü Ý Þ ß à á â ã ä å æ ç è é ê ë ì í î ï ð ñ ò ó ô õ ö ÷ ø ù ú û ü ý þ ÿ
|
||||||
|
Latin Extended-A
|
||||||
|
Ā ā Ă ă Ą ą Ć ć Ĉ ĉ Ċ ċ Č č Ď ď Đ đ Ē ē Ĕ ĕ Ė ė Ę ę Ě ě Ĝ ĝ Ğ ğ Ġ ġ Ģ ģ Ĥ ĥ Ħ ħ Ĩ ĩ Ī ī Ĭ ĭ Į į İ ı IJ ij Ĵ ĵ Ķ ķ ĸ Ĺ ĺ Ļ ļ Ľ ľ Ŀ ŀ Ł ł Ń ń Ņ ņ Ň ň ʼn Ŋ ŋ Ō ō Ŏ ŏ Ő ő Œ œ Ŕ ŕ Ŗ ŗ Ř ř Ś ś Ŝ ŝ Ş ş Š š Ţ ţ Ť ť Ŧ ŧ Ũ ũ Ū ū Ŭ ŭ Ů ů Ű ű Ų ų Ŵ ŵ Ŷ ŷ Ÿ Ź ź Ż ż Ž ž ſ
|
||||||
|
Latin Extended-B
|
||||||
|
ƀ Ɓ Ƃ ƃ Ƅ ƅ Ɔ Ƈ ƈ Ɖ Ɗ Ƌ ƌ ƍ Ǝ Ə Ɛ Ƒ ƒ Ɠ Ɣ ƕ Ɩ Ɨ Ƙ ƙ ƚ ƛ Ɯ Ɲ ƞ Ɵ Ơ ơ Ƣ ƣ Ƥ ƥ Ʀ Ƨ ƨ Ʃ ƪ ƫ Ƭ ƭ Ʈ Ư ư Ʊ Ʋ Ƴ ƴ Ƶ ƶ Ʒ Ƹ ƹ ƺ ƻ Ƽ ƽ ƾ ƿ ǀ ǁ ǂ ǃ DŽ Dž dž LJ Lj lj NJ Nj nj Ǎ ǎ Ǐ ǐ Ǒ ǒ Ǔ ǔ Ǖ ǖ Ǘ ǘ Ǚ ǚ Ǜ ǜ ǝ Ǟ ǟ Ǡ ǡ Ǣ ǣ Ǥ ǥ Ǧ ǧ Ǩ ǩ Ǫ ǫ Ǭ ǭ Ǯ ǯ ǰ DZ Dz dz Ǵ ǵ Ǻ ǻ Ǽ ǽ Ǿ ǿ Ȁ ȁ Ȃ ȃ ...
|
||||||
|
IPA Extensions
|
||||||
|
ɐ ɑ ɒ ɓ ɔ ɕ ɖ ɗ ɘ ə ɚ ɛ ɜ ɝ ɞ ɟ ɠ ɡ ɢ ɣ ɤ ɥ ɦ ɧ ɨ ɩ ɪ ɫ ɬ ɭ ɮ ɯ ɰ ɱ ɲ ɳ ɴ ɵ ɶ ɷ ɸ ɹ ɺ ɻ ɼ ɽ ɾ ɿ ʀ ʁ ʂ ʃ ʄ ʅ ʆ ʇ ʈ ʉ ʊ ʋ ʌ ʍ ʎ ʏ ʐ ʑ ʒ ʓ ʔ ʕ ʖ ʗ ʘ ʙ ʚ ʛ ʜ ʝ ʞ ʟ ʠ ʡ ʢ ʣ ʤ ʥ ʦ ʧ ʨ
|
||||||
|
Spacing Modifier Letters
|
||||||
|
ʰ ʱ ʲ ʳ ʴ ʵ ʶ ʷ ʸ ʹ ʺ ʻ ʼ ʽ ʾ ʿ ˀ ˁ ˂ ˃ ˄ ˅ ˆ ˇ ˈ ˉ ˊ ˋ ˌ ˍ ˎ ˏ ː ˑ ˒ ˓ ˔ ˕ ˖ ˗ ˘ ˙ ˚ ˛ ˜ ˝ ˞ ˠ ˡ ˢ ˣ ˤ ˥ ˦ ˧ ˨ ˩
|
||||||
|
Combining Diacritical Marks
|
||||||
|
̀ ́ ̂ ̃ ̄ ̅ ̆ ̇ ̈ ̉ ̊ ̋ ̌ ̍ ̎ ̏ ̐ ̑ ̒ ̓ ̔ ̕ ̖ ̗ ̘ ̙ ̚ ̛ ̜ ̝ ̞ ̟ ̠ ̡ ̢ ̣ ̤ ̥ ̦ ̧ ̨ ̩ ̪ ̫ ̬ ̭ ̮ ̯ ̰ ̱ ̲ ̳ ̴ ̵ ̶ ̷ ̸ ̹ ̺ ̻ ̼ ̽ ̾ ̿ ̀ ́ ͂ ̓ ̈́ ͅ ͠ ͡
|
||||||
|
Greek
|
||||||
|
ʹ ͵ ͺ ; ΄ ΅ Ά · Έ Ή Ί Ό Ύ Ώ ΐ Α Β Γ Δ Ε Ζ Η Θ Ι Κ Λ Μ Ν Ξ Ο Π Ρ Σ Τ Υ Φ Χ Ψ Ω Ϊ Ϋ ά έ ή ί ΰ α β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ ς σ τ υ φ χ ψ ω ϊ ϋ ό ύ ώ ϐ ϑ ϒ ϓ ϔ ϕ ϖ Ϛ Ϝ Ϟ Ϡ Ϣ ϣ Ϥ ϥ Ϧ ϧ Ϩ ϩ Ϫ ϫ Ϭ ϭ Ϯ ϯ ϰ ϱ ϲ ϳ
|
||||||
|
Cyrillic
|
||||||
|
Ё Ђ Ѓ Є Ѕ І Ї Ј Љ Њ Ћ Ќ Ў Џ А Б В Г Д Е Ж З И Й К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ Ъ Ы Ь Э Ю Я а б в г д е ж з и й к л м н о п р с т у ф х ц ч ш щ ъ ы ь э ю я ё ђ ѓ є ѕ і ї ј љ њ ћ ќ ў џ Ѡ ѡ Ѣ ѣ Ѥ ѥ Ѧ ѧ Ѩ ѩ Ѫ ѫ Ѭ ѭ Ѯ ѯ Ѱ ѱ Ѳ ѳ Ѵ ѵ Ѷ ѷ Ѹ ѹ Ѻ ѻ Ѽ ѽ Ѿ ѿ Ҁ ҁ ҂ ҃ ...
|
||||||
|
Armenian
|
||||||
|
Ա Բ Գ Դ Ե Զ Է Ը Թ Ժ Ի Լ Խ Ծ Կ Հ Ձ Ղ Ճ Մ Յ Ն Շ Ո Չ Պ Ջ Ռ Ս Վ Տ Ր Ց Ւ Փ Ք Օ Ֆ ՙ ՚ ՛ ՜ ՝ ՞ ՟ ա բ գ դ ե զ է ը թ ժ ի լ խ ծ կ հ ձ ղ ճ մ յ ն շ ո չ պ ջ ռ ս վ տ ր ց ւ փ ք օ ֆ և ։
|
||||||
|
Hebrew
|
||||||
|
֑ ֒ ֓ ֔ ֕ ֖ ֗ ֘ ֙ ֚ ֛ ֜ ֝ ֞ ֟ ֠ ֡ ֣ ֤ ֥ ֦ ֧ ֨ ֩ ֪ ֫ ֬ ֭ ֮ ֯ ְ ֱ ֲ ֳ ִ ֵ ֶ ַ ָ ֹ ֻ ּ ֽ ־ ֿ ׀ ׁ ׂ ׃ ׄ א ב ג ד ה ו ז ח ט י ך כ ל ם מ ן נ ס ע ף פ ץ צ ק ר ש ת װ ױ ײ ׳ ״
|
||||||
|
Arabic
|
||||||
|
، ؛ ؟ ء آ أ ؤ إ ئ ا ب ة ت ث ج ح خ د ذ ر ز س ش ص ض ط ظ ع غ ـ ف ق ك ل م ن ه و ى ي ً ٌ ٍ َ ُ ِ ّ ْ ٠ ١ ٢ ٣ ٤ ٥ ٦ ٧ ٨ ٩ ٪ ٫ ٬ ٭ ٰ ٱ ٲ ٳ ٴ ٵ ٶ ٷ ٸ ٹ ٺ ٻ ټ ٽ پ ٿ ڀ ځ ڂ ڃ ڄ څ چ ڇ ڈ ډ ڊ ڋ ڌ ڍ ڎ ڏ ڐ ڑ ڒ ړ ڔ ڕ ږ ڗ ژ ڙ ښ ڛ ڜ ڝ ڞ ڟ ڠ ڡ ڢ ڣ ڤ ڥ ڦ ڧ ڨ ک ڪ ګ ڬ ڭ ڮ گ ڰ ڱ ...
|
||||||
|
Devanagari
|
||||||
|
ँ ं ः अ आ इ ई उ ऊ ऋ ऌ ऍ ऎ ए ऐ ऑ ऒ ओ औ क ख ग घ ङ च छ ज झ ञ ट ठ ड ढ ण त थ द ध न ऩ प फ ब भ म य र ऱ ल ळ ऴ व श ष स ह ़ ऽ ा ि ी ु ू ृ ॄ ॅ ॆ े ै ॉ ॊ ो ौ ् ॐ ॑ ॒ ॓ ॔ क़ ख़ ग़ ज़ ड़ ढ़ फ़ य़ ॠ ॡ ॢ ॣ । ॥ ० १ २ ३ ४ ५ ६ ७ ८ ९ ॰
|
||||||
|
Bengali
|
||||||
|
ঁ ং ঃ অ আ ই ঈ উ ঊ ঋ ঌ এ ঐ ও ঔ ক খ গ ঘ ঙ চ ছ জ ঝ ঞ ট ঠ ড ঢ ণ ত থ দ ধ ন প ফ ব ভ ম য র ল শ ষ স হ ় া ি ী ু ূ ৃ ৄ ে ৈ ো ৌ ্ ৗ ড় ঢ় য় ৠ ৡ ৢ ৣ ০ ১ ২ ৩ ৪ ৫ ৬ ৭ ৮ ৯ ৰ ৱ ৲ ৳ ৴ ৵ ৶ ৷ ৸ ৹ ৺
|
||||||
|
Gurmukhi
|
||||||
|
ਂ ਅ ਆ ਇ ਈ ਉ ਊ ਏ ਐ ਓ ਔ ਕ ਖ ਗ ਘ ਙ ਚ ਛ ਜ ਝ ਞ ਟ ਠ ਡ ਢ ਣ ਤ ਥ ਦ ਧ ਨ ਪ ਫ ਬ ਭ ਮ ਯ ਰ ਲ ਲ਼ ਵ ਸ਼ ਸ ਹ ਼ ਾ ਿ ੀ ੁ ੂ ੇ ੈ ੋ ੌ ੍ ਖ਼ ਗ਼ ਜ਼ ੜ ਫ਼ ੦ ੧ ੨ ੩ ੪ ੫ ੬ ੭ ੮ ੯ ੰ ੱ ੲ ੳ ੴ
|
||||||
|
Gujarati
|
||||||
|
ઁ ં ઃ અ આ ઇ ઈ ઉ ઊ ઋ ઍ એ ઐ ઑ ઓ ઔ ક ખ ગ ઘ ઙ ચ છ જ ઝ ઞ ટ ઠ ડ ઢ ણ ત થ દ ધ ન પ ફ બ ભ મ ય ર લ ળ વ શ ષ સ હ ઼ ઽ ા િ ી ુ ૂ ૃ ૄ ૅ ે ૈ ૉ ો ૌ ્ ૐ ૠ ૦ ૧ ૨ ૩ ૪ ૫ ૬ ૭ ૮ ૯
|
||||||
|
Oriya
|
||||||
|
ଁ ଂ ଃ ଅ ଆ ଇ ଈ ଉ ଊ ଋ ଌ ଏ ଐ ଓ ଔ କ ଖ ଗ ଘ ଙ ଚ ଛ ଜ ଝ ଞ ଟ ଠ ଡ ଢ ଣ ତ ଥ ଦ ଧ ନ ପ ଫ ବ ଭ ମ ଯ ର ଲ ଳ ଶ ଷ ସ ହ ଼ ଽ ା ି ୀ ୁ ୂ ୃ େ ୈ ୋ ୌ ୍ ୖ ୗ ଡ଼ ଢ଼ ୟ ୠ ୡ ୦ ୧ ୨ ୩ ୪ ୫ ୬ ୭ ୮ ୯ ୰
|
||||||
|
Tamil
|
||||||
|
ஂ ஃ அ ஆ இ ஈ உ ஊ எ ஏ ஐ ஒ ஓ ஔ க ங ச ஜ ஞ ட ண த ந ன ப ம ய ர ற ல ள ழ வ ஷ ஸ ஹ ா ி ீ ு ூ ெ ே ை ொ ோ ௌ ் ௗ ௧ ௨ ௩ ௪ ௫ ௬ ௭ ௮ ௯ ௰ ௱ ௲
|
||||||
|
Telugu
|
||||||
|
ఁ ం ః అ ఆ ఇ ఈ ఉ ఊ ఋ ఌ ఎ ఏ ఐ ఒ ఓ ఔ క ఖ గ ఘ ఙ చ ఛ జ ఝ ఞ ట ఠ డ ఢ ణ త థ ద ధ న ప ఫ బ భ మ య ర ఱ ల ళ వ శ ష స హ ా ి ీ ు ూ ృ ౄ ె ే ై ొ ో ౌ ్ ౕ ౖ ౠ ౡ ౦ ౧ ౨ ౩ ౪ ౫ ౬ ౭ ౮ ౯
|
||||||
|
Kannada
|
||||||
|
ಂ ಃ ಅ ಆ ಇ ಈ ಉ ಊ ಋ ಌ ಎ ಏ ಐ ಒ ಓ ಔ ಕ ಖ ಗ ಘ ಙ ಚ ಛ ಜ ಝ ಞ ಟ ಠ ಡ ಢ ಣ ತ ಥ ದ ಧ ನ ಪ ಫ ಬ ಭ ಮ ಯ ರ ಱ ಲ ಳ ವ ಶ ಷ ಸ ಹ ಾ ಿ ೀ ು ೂ ೃ ೄ ೆ ೇ ೈ ೊ ೋ ೌ ್ ೕ ೖ ೞ ೠ ೡ ೦ ೧ ೨ ೩ ೪ ೫ ೬ ೭ ೮ ೯
|
||||||
|
Malayalam
|
||||||
|
ം ഃ അ ആ ഇ ഈ ഉ ഊ ഋ ഌ എ ഏ ഐ ഒ ഓ ഔ ക ഖ ഗ ഘ ങ ച ഛ ജ ഝ ഞ ട ഠ ഡ ഢ ണ ത ഥ ദ ധ ന പ ഫ ബ ഭ മ യ ര റ ല ള ഴ വ ശ ഷ സ ഹ ാ ി ീ ു ൂ ൃ െ േ ൈ ൊ ോ ൌ ് ൗ ൠ ൡ ൦ ൧ ൨ ൩ ൪ ൫ ൬ ൭ ൮ ൯
|
||||||
|
Thai
|
||||||
|
ก ข ฃ ค ฅ ฆ ง จ ฉ ช ซ ฌ ญ ฎ ฏ ฐ ฑ ฒ ณ ด ต ถ ท ธ น บ ป ผ ฝ พ ฟ ภ ม ย ร ฤ ล ฦ ว ศ ษ ส ห ฬ อ ฮ ฯ ะ ั า ำ ิ ี ึ ื ุ ู ฺ ฿ เ แ โ ใ ไ ๅ ๆ ็ ่ ้ ๊ ๋ ์ ํ ๎ ๏ ๐ ๑ ๒ ๓ ๔ ๕ ๖ ๗ ๘ ๙ ๚ ๛
|
||||||
|
Lao
|
||||||
|
ກ ຂ ຄ ງ ຈ ຊ ຍ ດ ຕ ຖ ທ ນ ບ ປ ຜ ຝ ພ ຟ ມ ຢ ຣ ລ ວ ສ ຫ ອ ຮ ຯ ະ ັ າ ຳ ິ ີ ຶ ື ຸ ູ ົ ຼ ຽ ເ ແ ໂ ໃ ໄ ໆ ່ ້ ໊ ໋ ໌ ໍ ໐ ໑ ໒ ໓ ໔ ໕ ໖ ໗ ໘ ໙ ໜ ໝ
|
||||||
|
Tibetan
|
||||||
|
ༀ ༁ ༂ ༃ ༄ ༅ ༆ ༇ ༈ ༉ ༊ ་ ༌ ། ༎ ༏ ༐ ༑ ༒ ༓ ༔ ༕ ༖ ༗ ༘ ༙ ༚ ༛ ༜ ༝ ༞ ༟ ༠ ༡ ༢ ༣ ༤ ༥ ༦ ༧ ༨ ༩ ༪ ༫ ༬ ༭ ༮ ༯ ༰ ༱ ༲ ༳ ༴ ༵ ༶ ༷ ༸ ༹ ༺ ༻ ༼ ༽ ༾ ༿ ཀ ཁ ག གྷ ང ཅ ཆ ཇ ཉ ཊ ཋ ཌ ཌྷ ཎ ཏ ཐ ད དྷ ན པ ཕ བ བྷ མ ཙ ཚ ཛ ཛྷ ཝ ཞ ཟ འ ཡ ར ལ ཤ ཥ ས ཧ ཨ ཀྵ ཱ ི ཱི ུ ཱུ ྲྀ ཷ ླྀ ཹ ེ ཻ ོ ཽ ཾ ཿ ྀ ཱྀ ྂ ྃ ྄ ྅ ྆ ྇ ...
|
||||||
|
Georgian
|
||||||
|
Ⴀ Ⴁ Ⴂ Ⴃ Ⴄ Ⴅ Ⴆ Ⴇ Ⴈ Ⴉ Ⴊ Ⴋ Ⴌ Ⴍ Ⴎ Ⴏ Ⴐ Ⴑ Ⴒ Ⴓ Ⴔ Ⴕ Ⴖ Ⴗ Ⴘ Ⴙ Ⴚ Ⴛ Ⴜ Ⴝ Ⴞ Ⴟ Ⴠ Ⴡ Ⴢ Ⴣ Ⴤ Ⴥ ა ბ გ დ ე ვ ზ თ ი კ ლ მ ნ ო პ ჟ რ ს ტ უ ფ ქ ღ ყ შ ჩ ც ძ წ ჭ ხ ჯ ჰ ჱ ჲ ჳ ჴ ჵ ჶ ჻
|
||||||
|
Hangul Jamo
|
||||||
|
ᄀ ᄁ ᄂ ᄃ ᄄ ᄅ ᄆ ᄇ ᄈ ᄉ ᄊ ᄋ ᄌ ᄍ ᄎ ᄏ ᄐ ᄑ ᄒ ᄓ ᄔ ᄕ ᄖ ᄗ ᄘ ᄙ ᄚ ᄛ ᄜ ᄝ ᄞ ᄟ ᄠ ᄡ ᄢ ᄣ ᄤ ᄥ ᄦ ᄧ ᄨ ᄩ ᄪ ᄫ ᄬ ᄭ ᄮ ᄯ ᄰ ᄱ ᄲ ᄳ ᄴ ᄵ ᄶ ᄷ ᄸ ᄹ ᄺ ᄻ ᄼ ᄽ ᄾ ᄿ ᅀ ᅁ ᅂ ᅃ ᅄ ᅅ ᅆ ᅇ ᅈ ᅉ ᅊ ᅋ ᅌ ᅍ ᅎ ᅏ ᅐ ᅑ ᅒ ᅓ ᅔ ᅕ ᅖ ᅗ ᅘ ᅙ ᅟ ᅠ ᅡ ᅢ ᅣ ᅤ ᅥ ᅦ ᅧ ᅨ ᅩ ᅪ ᅫ ᅬ ᅭ ᅮ ᅯ ᅰ ᅱ ᅲ ᅳ ᅴ ᅵ ᅶ ᅷ ᅸ ᅹ ᅺ ᅻ ᅼ ᅽ ᅾ ᅿ ᆀ ᆁ ᆂ ᆃ ᆄ ...
|
||||||
|
Latin Extended Additional
|
||||||
|
Ḁ ḁ Ḃ ḃ Ḅ ḅ Ḇ ḇ Ḉ ḉ Ḋ ḋ Ḍ ḍ Ḏ ḏ Ḑ ḑ Ḓ ḓ Ḕ ḕ Ḗ ḗ Ḙ ḙ Ḛ ḛ Ḝ ḝ Ḟ ḟ Ḡ ḡ Ḣ ḣ Ḥ ḥ Ḧ ḧ Ḩ ḩ Ḫ ḫ Ḭ ḭ Ḯ ḯ Ḱ ḱ Ḳ ḳ Ḵ ḵ Ḷ ḷ Ḹ ḹ Ḻ ḻ Ḽ ḽ Ḿ ḿ Ṁ ṁ Ṃ ṃ Ṅ ṅ Ṇ ṇ Ṉ ṉ Ṋ ṋ Ṍ ṍ Ṏ ṏ Ṑ ṑ Ṓ ṓ Ṕ ṕ Ṗ ṗ Ṙ ṙ Ṛ ṛ Ṝ ṝ Ṟ ṟ Ṡ ṡ Ṣ ṣ Ṥ ṥ Ṧ ṧ Ṩ ṩ Ṫ ṫ Ṭ ṭ Ṯ ṯ Ṱ ṱ Ṳ ṳ Ṵ ṵ Ṷ ṷ Ṹ ṹ Ṻ ṻ Ṽ ṽ Ṿ ṿ ...
|
||||||
|
Greek Extended
|
||||||
|
ἀ ἁ ἂ ἃ ἄ ἅ ἆ ἇ Ἀ Ἁ Ἂ Ἃ Ἄ Ἅ Ἆ Ἇ ἐ ἑ ἒ ἓ ἔ ἕ Ἐ Ἑ Ἒ Ἓ Ἔ Ἕ ἠ ἡ ἢ ἣ ἤ ἥ ἦ ἧ Ἠ Ἡ Ἢ Ἣ Ἤ Ἥ Ἦ Ἧ ἰ ἱ ἲ ἳ ἴ ἵ ἶ ἷ Ἰ Ἱ Ἲ Ἳ Ἴ Ἵ Ἶ Ἷ ὀ ὁ ὂ ὃ ὄ ὅ Ὀ Ὁ Ὂ Ὃ Ὄ Ὅ ὐ ὑ ὒ ὓ ὔ ὕ ὖ ὗ Ὑ Ὓ Ὕ Ὗ ὠ ὡ ὢ ὣ ὤ ὥ ὦ ὧ Ὠ Ὡ Ὢ Ὣ Ὤ Ὥ Ὦ Ὧ ὰ ά ὲ έ ὴ ή ὶ ί ὸ ό ὺ ύ ὼ ώ ᾀ ᾁ ᾂ ᾃ ᾄ ᾅ ᾆ ᾇ ᾈ ᾉ ᾊ ᾋ ᾌ ᾍ ...
|
||||||
|
General Punctuation
|
||||||
|
‐ ‑ ‒ – — ― ‖ ‗ ‘ ’ ‚ ‛ “ ” „ ‟ † ‡ • ‣ ․ ‥ … ‧
‰ ‱ ′ ″ ‴ ‵ ‶ ‷ ‸ ‹ › ※ ‼ ‽ ‾ ‿ ⁀ ⁁ ⁂ ⁃ ⁄ ⁅ ⁆
|
||||||
|
Superscripts and Subscripts
|
||||||
|
⁰ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁺ ⁻ ⁼ ⁽ ⁾ ⁿ ₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₊ ₋ ₌ ₍ ₎
|
||||||
|
Currency Symbols
|
||||||
|
₠ ₡ ₢ ₣ ₤ ₥ ₦ ₧ ₨ ₩ ₪ ₫
|
||||||
|
Combining Marks for Symbols
|
||||||
|
⃐ ⃑ ⃒ ⃓ ⃔ ⃕ ⃖ ⃗ ⃘ ⃙ ⃚ ⃛ ⃜ ⃝ ⃞ ⃟ ⃠ ⃡
|
||||||
|
Letterlike Symbols
|
||||||
|
℀ ℁ ℂ ℃ ℄ ℅ ℆ ℇ ℈ ℉ ℊ ℋ ℌ ℍ ℎ ℏ ℐ ℑ ℒ ℓ ℔ ℕ № ℗ ℘ ℙ ℚ ℛ ℜ ℝ ℞ ℟ ℠ ℡ ™ ℣ ℤ ℥ Ω ℧ ℨ ℩ K Å ℬ ℭ ℮ ℯ ℰ ℱ Ⅎ ℳ ℴ ℵ ℶ ℷ ℸ
|
||||||
|
Number Forms
|
||||||
|
⅓ ⅔ ⅕ ⅖ ⅗ ⅘ ⅙ ⅚ ⅛ ⅜ ⅝ ⅞ ⅟ Ⅰ Ⅱ Ⅲ Ⅳ Ⅴ Ⅵ Ⅶ Ⅷ Ⅸ Ⅹ Ⅺ Ⅻ Ⅼ Ⅽ Ⅾ Ⅿ ⅰ ⅱ ⅲ ⅳ ⅴ ⅵ ⅶ ⅷ ⅸ ⅹ ⅺ ⅻ ⅼ ⅽ ⅾ ⅿ ↀ ↁ ↂ
|
||||||
|
Arrows
|
||||||
|
← ↑ → ↓ ↔ ↕ ↖ ↗ ↘ ↙ ↚ ↛ ↜ ↝ ↞ ↟ ↠ ↡ ↢ ↣ ↤ ↥ ↦ ↧ ↨ ↩ ↪ ↫ ↬ ↭ ↮ ↯ ↰ ↱ ↲ ↳ ↴ ↵ ↶ ↷ ↸ ↹ ↺ ↻ ↼ ↽ ↾ ↿ ⇀ ⇁ ⇂ ⇃ ⇄ ⇅ ⇆ ⇇ ⇈ ⇉ ⇊ ⇋ ⇌ ⇍ ⇎ ⇏ ⇐ ⇑ ⇒ ⇓ ⇔ ⇕ ⇖ ⇗ ⇘ ⇙ ⇚ ⇛ ⇜ ⇝ ⇞ ⇟ ⇠ ⇡ ⇢ ⇣ ⇤ ⇥ ⇦ ⇧ ⇨ ⇩ ⇪
|
||||||
|
Mathematical Operators
|
||||||
|
∀ ∁ ∂ ∃ ∄ ∅ ∆ ∇ ∈ ∉ ∊ ∋ ∌ ∍ ∎ ∏ ∐ ∑ − ∓ ∔ ∕ ∖ ∗ ∘ ∙ √ ∛ ∜ ∝ ∞ ∟ ∠ ∡ ∢ ∣ ∤ ∥ ∦ ∧ ∨ ∩ ∪ ∫ ∬ ∭ ∮ ∯ ∰ ∱ ∲ ∳ ∴ ∵ ∶ ∷ ∸ ∹ ∺ ∻ ∼ ∽ ∾ ∿ ≀ ≁ ≂ ≃ ≄ ≅ ≆ ≇ ≈ ≉ ≊ ≋ ≌ ≍ ≎ ≏ ≐ ≑ ≒ ≓ ≔ ≕ ≖ ≗ ≘ ≙ ≚ ≛ ≜ ≝ ≞ ≟ ≠ ≡ ≢ ≣ ≤ ≥ ≦ ≧ ≨ ≩ ≪ ≫ ≬ ≭ ≮ ≯ ≰ ≱ ≲ ≳ ≴ ≵ ≶ ≷ ≸ ≹ ≺ ≻ ≼ ≽ ≾ ≿ ...
|
||||||
|
Miscellaneous Technical
|
||||||
|
⌀ ⌂ ⌃ ⌄ ⌅ ⌆ ⌇ ⌈ ⌉ ⌊ ⌋ ⌌ ⌍ ⌎ ⌏ ⌐ ⌑ ⌒ ⌓ ⌔ ⌕ ⌖ ⌗ ⌘ ⌙ ⌚ ⌛ ⌜ ⌝ ⌞ ⌟ ⌠ ⌡ ⌢ ⌣ ⌤ ⌥ ⌦ ⌧ ⌨ 〈 〉 ⌫ ⌬ ⌭ ⌮ ⌯ ⌰ ⌱ ⌲ ⌳ ⌴ ⌵ ⌶ ⌷ ⌸ ⌹ ⌺ ⌻ ⌼ ⌽ ⌾ ⌿ ⍀ ⍁ ⍂ ⍃ ⍄ ⍅ ⍆ ⍇ ⍈ ⍉ ⍊ ⍋ ⍌ ⍍ ⍎ ⍏ ⍐ ⍑ ⍒ ⍓ ⍔ ⍕ ⍖ ⍗ ⍘ ⍙ ⍚ ⍛ ⍜ ⍝ ⍞ ⍟ ⍠ ⍡ ⍢ ⍣ ⍤ ⍥ ⍦ ⍧ ⍨ ⍩ ⍪ ⍫ ⍬ ⍭ ⍮ ⍯ ⍰ ⍱ ⍲ ⍳ ⍴ ⍵ ⍶ ⍷ ⍸ ⍹ ⍺
|
||||||
|
Control Pictures
|
||||||
|
␀ ␁ ␂ ␃ ␄ ␅ ␆ ␇ ␈ ␉ ␊ ␋ ␌ ␍ ␎ ␏ ␐ ␑ ␒ ␓ ␔ ␕ ␖ ␗ ␘ ␙ ␚ ␛ ␜ ␝ ␞ ␟ ␠ ␡ ␢ ␣ 
|
||||||
|
Optical Character Recognition
|
||||||
|
⑀ ⑁ ⑂ ⑃ ⑄ ⑅ ⑆ ⑇ ⑈ ⑉ ⑊
|
||||||
|
Enclosed Alphanumerics
|
||||||
|
① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ ⑫ ⑬ ⑭ ⑮ ⑯ ⑰ ⑱ ⑲ ⑳ ⑴ ⑵ ⑶ ⑷ ⑸ ⑹ ⑺ ⑻ ⑼ ⑽ ⑾ ⑿ ⒀ ⒁ ⒂ ⒃ ⒄ ⒅ ⒆ ⒇ ⒈ ⒉ ⒊ ⒋ ⒌ ⒍ ⒎ ⒏ ⒐ ⒑ ⒒ ⒓ ⒔ ⒕ ⒖ ⒗ ⒘ ⒙ ⒚ ⒛ ⒜ ⒝ ⒞ ⒟ ⒠ ⒡ ⒢ ⒣ ⒤ ⒥ ⒦ ⒧ ⒨ ⒩ ⒪ ⒫ ⒬ ⒭ ⒮ ⒯ ⒰ ⒱ ⒲ ⒳ ⒴ ⒵ Ⓐ Ⓑ Ⓒ Ⓓ Ⓔ Ⓕ Ⓖ Ⓗ Ⓘ Ⓙ Ⓚ Ⓛ Ⓜ Ⓝ Ⓞ Ⓟ Ⓠ Ⓡ Ⓢ Ⓣ Ⓤ Ⓥ Ⓦ Ⓧ Ⓨ Ⓩ ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ ⓖ ⓗ ⓘ ⓙ ⓚ ⓛ ⓜ ⓝ ⓞ ⓟ ...
|
||||||
|
Box Drawing
|
||||||
|
─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏ ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟ ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯ ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿ ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏ ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟ ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯ ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿
|
||||||
|
Block Elements
|
||||||
|
▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▉ ▊ ▋ ▌ ▍ ▎ ▏ ▐ ░ ▒ ▓ ▔ ▕
|
||||||
|
Geometric Shapes
|
||||||
|
■ □ ▢ ▣ ▤ ▥ ▦ ▧ ▨ ▩ ▪ ▫ ▬ ▭ ▮ ▯ ▰ ▱ ▲ △ ▴ ▵ ▶ ▷ ▸ ▹ ► ▻ ▼ ▽ ▾ ▿ ◀ ◁ ◂ ◃ ◄ ◅ ◆ ◇ ◈ ◉ ◊ ○ ◌ ◍ ◎ ● ◐ ◑ ◒ ◓ ◔ ◕ ◖ ◗ ◘ ◙ ◚ ◛ ◜ ◝ ◞ ◟ ◠ ◡ ◢ ◣ ◤ ◥ ◦ ◧ ◨ ◩ ◪ ◫ ◬ ◭ ◮ ◯
|
||||||
|
Miscellaneous Symbols
|
||||||
|
☀ ☁ ☂ ☃ ☄ ★ ☆ ☇ ☈ ☉ ☊ ☋ ☌ ☍ ☎ ☏ ☐ ☑ ☒ ☓ ☚ ☛ ☜ ☝ ☞ ☟ ☠ ☡ ☢ ☣ ☤ ☥ ☦ ☧ ☨ ☩ ☪ ☫ ☬ ☭ ☮ ☯ ☰ ☱ ☲ ☳ ☴ ☵ ☶ ☷ ☸ ☹ ☺ ☻ ☼ ☽ ☾ ☿ ♀ ♁ ♂ ♃ ♄ ♅ ♆ ♇ ♈ ♉ ♊ ♋ ♌ ♍ ♎ ♏ ♐ ♑ ♒ ♓ ♔ ♕ ♖ ♗ ♘ ♙ ♚ ♛ ♜ ♝ ♞ ♟ ♠ ♡ ♢ ♣ ♤ ♥ ♦ ♧ ♨ ♩ ♪ ♫ ♬ ♭ ♮ ♯
|
||||||
|
Dingbats
|
||||||
|
✁ ✂ ✃ ✄ ✆ ✇ ✈ ✉ ✌ ✍ ✎ ✏ ✐ ✑ ✒ ✓ ✔ ✕ ✖ ✗ ✘ ✙ ✚ ✛ ✜ ✝ ✞ ✟ ✠ ✡ ✢ ✣ ✤ ✥ ✦ ✧ ✩ ✪ ✫ ✬ ✭ ✮ ✯ ✰ ✱ ✲ ✳ ✴ ✵ ✶ ✷ ✸ ✹ ✺ ✻ ✼ ✽ ✾ ✿ ❀ ❁ ❂ ❃ ❄ ❅ ❆ ❇ ❈ ❉ ❊ ❋ ❍ ❏ ❐ ❑ ❒ ❖ ❘ ❙ ❚ ❛ ❜ ❝ ❞ ❡ ❢ ❣ ❤ ❥ ❦ ❧ ❶ ❷ ❸ ❹ ❺ ❻ ❼ ❽ ❾ ❿ ➀ ➁ ➂ ➃ ➄ ➅ ➆ ➇ ➈ ➉ ➊ ➋ ➌ ➍ ➎ ➏ ➐ ➑ ➒ ➓ ➔ ➘ ➙ ➚ ➛ ➜ ➝ ...
|
||||||
|
CJK Symbols and Punctuation
|
||||||
|
、 。 〃 〄 々 〆 〇 〈 〉 《 》 「 」 『 』 【 】 〒 〓 〔 〕 〖 〗 〘 〙 〚 〛 〜 〝 〞 〟 〠 〡 〢 〣 〤 〥 〦 〧 〨 〩 〪 〫 〬 〭 〮 〯 〰 〱 〲 〳 〴 〵 〶 〷 〿
|
||||||
|
Hiragana
|
||||||
|
ぁ あ ぃ い ぅ う ぇ え ぉ お か が き ぎ く ぐ け げ こ ご さ ざ し じ す ず せ ぜ そ ぞ た だ ち ぢ っ つ づ て で と ど な に ぬ ね の は ば ぱ ひ び ぴ ふ ぶ ぷ へ べ ぺ ほ ぼ ぽ ま み む め も ゃ や ゅ ゆ ょ よ ら り る れ ろ ゎ わ ゐ ゑ を ん ゔ ゙ ゚ ゛ ゜ ゝ ゞ
|
||||||
|
Katakana
|
||||||
|
ァ ア ィ イ ゥ ウ ェ エ ォ オ カ ガ キ ギ ク グ ケ ゲ コ ゴ サ ザ シ ジ ス ズ セ ゼ ソ ゾ タ ダ チ ヂ ッ ツ ヅ テ デ ト ド ナ ニ ヌ ネ ノ ハ バ パ ヒ ビ ピ フ ブ プ ヘ ベ ペ ホ ボ ポ マ ミ ム メ モ ャ ヤ ュ ユ ョ ヨ ラ リ ル レ ロ ヮ ワ ヰ ヱ ヲ ン ヴ ヵ ヶ ヷ ヸ ヹ ヺ ・ ー ヽ ヾ
|
||||||
|
Bopomofo
|
||||||
|
ㄅ ㄆ ㄇ ㄈ ㄉ ㄊ ㄋ ㄌ ㄍ ㄎ ㄏ ㄐ ㄑ ㄒ ㄓ ㄔ ㄕ ㄖ ㄗ ㄘ ㄙ ㄚ ㄛ ㄜ ㄝ ㄞ ㄟ ㄠ ㄡ ㄢ ㄣ ㄤ ㄥ ㄦ ㄧ ㄨ ㄩ ㄪ ㄫ ㄬ
|
||||||
|
Hangul Compatibility Jamo
|
||||||
|
ㄱ ㄲ ㄳ ㄴ ㄵ ㄶ ㄷ ㄸ ㄹ ㄺ ㄻ ㄼ ㄽ ㄾ ㄿ ㅀ ㅁ ㅂ ㅃ ㅄ ㅅ ㅆ ㅇ ㅈ ㅉ ㅊ ㅋ ㅌ ㅍ ㅎ ㅏ ㅐ ㅑ ㅒ ㅓ ㅔ ㅕ ㅖ ㅗ ㅘ ㅙ ㅚ ㅛ ㅜ ㅝ ㅞ ㅟ ㅠ ㅡ ㅢ ㅣ ㅤ ㅥ ㅦ ㅧ ㅨ ㅩ ㅪ ㅫ ㅬ ㅭ ㅮ ㅯ ㅰ ㅱ ㅲ ㅳ ㅴ ㅵ ㅶ ㅷ ㅸ ㅹ ㅺ ㅻ ㅼ ㅽ ㅾ ㅿ ㆀ ㆁ ㆂ ㆃ ㆄ ㆅ ㆆ ㆇ ㆈ ㆉ ㆊ ㆋ ㆌ ㆍ ㆎ
|
||||||
|
Kanbun
|
||||||
|
㆐ ㆑ ㆒ ㆓ ㆔ ㆕ ㆖ ㆗ ㆘ ㆙ ㆚ ㆛ ㆜ ㆝ ㆞ ㆟
|
||||||
|
Enclosed CJK Letters and Months
|
||||||
|
㈀ ㈁ ㈂ ㈃ ㈄ ㈅ ㈆ ㈇ ㈈ ㈉ ㈊ ㈋ ㈌ ㈍ ㈎ ㈏ ㈐ ㈑ ㈒ ㈓ ㈔ ㈕ ㈖ ㈗ ㈘ ㈙ ㈚ ㈛ ㈜ ㈠ ㈡ ㈢ ㈣ ㈤ ㈥ ㈦ ㈧ ㈨ ㈩ ㈪ ㈫ ㈬ ㈭ ㈮ ㈯ ㈰ ㈱ ㈲ ㈳ ㈴ ㈵ ㈶ ㈷ ㈸ ㈹ ㈺ ㈻ ㈼ ㈽ ㈾ ㈿ ㉀ ㉁ ㉂ ㉃ ㉠ ㉡ ㉢ ㉣ ㉤ ㉥ ㉦ ㉧ ㉨ ㉩ ㉪ ㉫ ㉬ ㉭ ㉮ ㉯ ㉰ ㉱ ㉲ ㉳ ㉴ ㉵ ㉶ ㉷ ㉸ ㉹ ㉺ ㉻ ㉿ ㊀ ㊁ ㊂ ㊃ ㊄ ㊅ ㊆ ㊇ ㊈ ㊉ ㊊ ㊋ ㊌ ㊍ ㊎ ㊏ ㊐ ㊑ ㊒ ㊓ ㊔ ㊕ ㊖ ㊗ ㊘ ㊙ ㊚ ㊛ ㊜ ㊝ ㊞ ㊟ ㊠ ㊡ ...
|
||||||
|
CJK Compatibility
|
||||||
|
㌀ ㌁ ㌂ ㌃ ㌄ ㌅ ㌆ ㌇ ㌈ ㌉ ㌊ ㌋ ㌌ ㌍ ㌎ ㌏ ㌐ ㌑ ㌒ ㌓ ㌔ ㌕ ㌖ ㌗ ㌘ ㌙ ㌚ ㌛ ㌜ ㌝ ㌞ ㌟ ㌠ ㌡ ㌢ ㌣ ㌤ ㌥ ㌦ ㌧ ㌨ ㌩ ㌪ ㌫ ㌬ ㌭ ㌮ ㌯ ㌰ ㌱ ㌲ ㌳ ㌴ ㌵ ㌶ ㌷ ㌸ ㌹ ㌺ ㌻ ㌼ ㌽ ㌾ ㌿ ㍀ ㍁ ㍂ ㍃ ㍄ ㍅ ㍆ ㍇ ㍈ ㍉ ㍊ ㍋ ㍌ ㍍ ㍎ ㍏ ㍐ ㍑ ㍒ ㍓ ㍔ ㍕ ㍖ ㍗ ㍘ ㍙ ㍚ ㍛ ㍜ ㍝ ㍞ ㍟ ㍠ ㍡ ㍢ ㍣ ㍤ ㍥ ㍦ ㍧ ㍨ ㍩ ㍪ ㍫ ㍬ ㍭ ㍮ ㍯ ㍰ ㍱ ㍲ ㍳ ㍴ ㍵ ㍶ ㍻ ㍼ ㍽ ㍾ ㍿ ㎀ ㎁ ㎂ ㎃ ...
|
||||||
|
CJK Unified Ideographs
|
||||||
|
一 丁 丂 七 丄 丅 丆 万 丈 三 上 下 丌 不 与 丏 丐 丑 丒 专 且 丕 世 丗 丘 丙 业 丛 东 丝 丞 丟 丠 両 丢 丣 两 严 並 丧 丨 丩 个 丫 丬 中 丮 丯 丰 丱 串 丳 临 丵 丶 丷 丸 丹 为 主 丼 丽 举 丿 乀 乁 乂 乃 乄 久 乆 乇 么 义 乊 之 乌 乍 乎 乏 乐 乑 乒 乓 乔 乕 乖 乗 乘 乙 乚 乛 乜 九 乞 也 习 乡 乢 乣 乤 乥 书 乧 乨 乩 乪 乫 乬 乭 乮 乯 买 乱 乲 乳 乴 乵 乶 乷 乸 乹 乺 乻 乼 乽 乾 乿 ...
|
||||||
|
Hangul Syllables
|
||||||
|
가 각 갂 갃 간 갅 갆 갇 갈 갉 갊 갋 갌 갍 갎 갏 감 갑 값 갓 갔 강 갖 갗 갘 같 갚 갛 개 객 갞 갟 갠 갡 갢 갣 갤 갥 갦 갧 갨 갩 갪 갫 갬 갭 갮 갯 갰 갱 갲 갳 갴 갵 갶 갷 갸 갹 갺 갻 갼 갽 갾 갿 걀 걁 걂 걃 걄 걅 걆 걇 걈 걉 걊 걋 걌 걍 걎 걏 걐 걑 걒 걓 걔 걕 걖 걗 걘 걙 걚 걛 걜 걝 걞 걟 걠 걡 걢 걣 걤 걥 걦 걧 걨 걩 걪 걫 걬 걭 걮 걯 거 걱 걲 걳 건 걵 걶 걷 걸 걹 걺 걻 걼 걽 걾 걿 ...
|
||||||
|
Private Use
|
||||||
|
...
|
||||||
|
CJK Compatibility Ideographs
|
||||||
|
豈 更 車 賈 滑 串 句 龜 龜 契 金 喇 奈 懶 癩 羅 蘿 螺 裸 邏 樂 洛 烙 珞 落 酪 駱 亂 卵 欄 爛 蘭 鸞 嵐 濫 藍 襤 拉 臘 蠟 廊 朗 浪 狼 郎 來 冷 勞 擄 櫓 爐 盧 老 蘆 虜 路 露 魯 鷺 碌 祿 綠 菉 錄 鹿 論 壟 弄 籠 聾 牢 磊 賂 雷 壘 屢 樓 淚 漏 累 縷 陋 勒 肋 凜 凌 稜 綾 菱 陵 讀 拏 樂 諾 丹 寧 怒 率 異 北 磻 便 復 不 泌 數 索 參 塞 省 葉 說 殺 辰 沈 拾 若 掠 略 亮 兩 凉 梁 糧 良 諒 量 勵 ...
|
||||||
|
Alphabetic Presentation Forms
|
||||||
|
ff fi fl ffi ffl ſt st ﬓ ﬔ ﬕ ﬖ ﬗ ﬞ ײַ ﬠ ﬡ ﬢ ﬣ ﬤ ﬥ ﬦ ﬧ ﬨ ﬩ שׁ שׂ שּׁ שּׂ אַ אָ אּ בּ גּ דּ הּ וּ זּ טּ יּ ךּ כּ לּ מּ נּ סּ ףּ פּ צּ קּ רּ שּ תּ וֹ בֿ כֿ פֿ ﭏ
|
||||||
|
Arabic Presentation Forms-A
|
||||||
|
ﭐ ﭑ ﭒ ﭓ ﭔ ﭕ ﭖ ﭗ ﭘ ﭙ ﭚ ﭛ ﭜ ﭝ ﭞ ﭟ ﭠ ﭡ ﭢ ﭣ ﭤ ﭥ ﭦ ﭧ ﭨ ﭩ ﭪ ﭫ ﭬ ﭭ ﭮ ﭯ ﭰ ﭱ ﭲ ﭳ ﭴ ﭵ ﭶ ﭷ ﭸ ﭹ ﭺ ﭻ ﭼ ﭽ ﭾ ﭿ ﮀ ﮁ ﮂ ﮃ ﮄ ﮅ ﮆ ﮇ ﮈ ﮉ ﮊ ﮋ ﮌ ﮍ ﮎ ﮏ ﮐ ﮑ ﮒ ﮓ ﮔ ﮕ ﮖ ﮗ ﮘ ﮙ ﮚ ﮛ ﮜ ﮝ ﮞ ﮟ ﮠ ﮡ ﮢ ﮣ ﮤ ﮥ ﮦ ﮧ ﮨ ﮩ ﮪ ﮫ ﮬ ﮭ ﮮ ﮯ ﮰ ﮱ ﯓ ﯔ ﯕ ﯖ ﯗ ﯘ ﯙ ﯚ ﯛ ﯜ ﯝ ﯞ ﯟ ﯠ ﯡ ﯢ ﯣ ﯤ ﯥ ﯦ ﯧ ﯨ ﯩ ﯪ ﯫ ﯬ ﯭ ﯮ ﯯ ﯰ ...
|
||||||
|
Combining Half Marks
|
||||||
|
︠ ︡ ︢ ︣
|
||||||
|
CJK Compatibility Forms
|
||||||
|
︰ ︱ ︲ ︳ ︴ ︵ ︶ ︷ ︸ ︹ ︺ ︻ ︼ ︽ ︾ ︿ ﹀ ﹁ ﹂ ﹃ ﹄ ﹉ ﹊ ﹋ ﹌ ﹍ ﹎ ﹏
|
||||||
|
Small Form Variants
|
||||||
|
﹐ ﹑ ﹒ ﹔ ﹕ ﹖ ﹗ ﹘ ﹙ ﹚ ﹛ ﹜ ﹝ ﹞ ﹟ ﹠ ﹡ ﹢ ﹣ ﹤ ﹥ ﹦ ﹨ ﹩ ﹪ ﹫
|
||||||
|
Arabic Presentation Forms-B
|
||||||
|
ﹰ ﹱ ﹲ ﹴ ﹶ ﹷ ﹸ ﹹ ﹺ ﹻ ﹼ ﹽ ﹾ ﹿ ﺀ ﺁ ﺂ ﺃ ﺄ ﺅ ﺆ ﺇ ﺈ ﺉ ﺊ ﺋ ﺌ ﺍ ﺎ ﺏ ﺐ ﺑ ﺒ ﺓ ﺔ ﺕ ﺖ ﺗ ﺘ ﺙ ﺚ ﺛ ﺜ ﺝ ﺞ ﺟ ﺠ ﺡ ﺢ ﺣ ﺤ ﺥ ﺦ ﺧ ﺨ ﺩ ﺪ ﺫ ﺬ ﺭ ﺮ ﺯ ﺰ ﺱ ﺲ ﺳ ﺴ ﺵ ﺶ ﺷ ﺸ ﺹ ﺺ ﺻ ﺼ ﺽ ﺾ ﺿ ﻀ ﻁ ﻂ ﻃ ﻄ ﻅ ﻆ ﻇ ﻈ ﻉ ﻊ ﻋ ﻌ ﻍ ﻎ ﻏ ﻐ ﻑ ﻒ ﻓ ﻔ ﻕ ﻖ ﻗ ﻘ ﻙ ﻚ ﻛ ﻜ ﻝ ﻞ ﻟ ﻠ ﻡ ﻢ ﻣ ﻤ ﻥ ﻦ ﻧ ﻨ ﻩ ﻪ ﻫ ﻬ ﻭ ﻮ ﻯ ﻰ ﻱ ...
|
||||||
|
Halfwidth and Fullwidth Forms
|
||||||
|
! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~ 。 「 」 、 ・ ヲ ァ ィ ゥ ェ ォ ャ ュ ョ ッ ー ア イ ウ エ オ カ キ ク ケ コ サ シ ス セ ソ タ チ ツ ...
|
||||||
|
Specials
|
||||||
|
|
||||||
|
Specials
|
||||||
|
<20>
|
||||||
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.tests.crypto
|
|
||||||
|
|
||||||
import org.junit.Assert.assertArrayEquals
|
|
||||||
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.Random
|
|
||||||
|
|
||||||
import junit.framework.TestCase
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.finalkey.AndroidAESKeyTransformer
|
|
||||||
import com.kunzisoft.keepass.crypto.finalkey.NativeAESKeyTransformer
|
|
||||||
|
|
||||||
class AESKeyTest : TestCase() {
|
|
||||||
private lateinit var mRand: Random
|
|
||||||
|
|
||||||
@Throws(Exception::class)
|
|
||||||
override fun setUp() {
|
|
||||||
super.setUp()
|
|
||||||
|
|
||||||
mRand = Random()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun testAES() {
|
|
||||||
// Test both an old and an even number to test my flip variable
|
|
||||||
testAESFinalKey(5)
|
|
||||||
testAESFinalKey(6)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun testAESFinalKey(rounds: Long) {
|
|
||||||
val seed = ByteArray(32)
|
|
||||||
val key = ByteArray(32)
|
|
||||||
val nativeKey: ByteArray?
|
|
||||||
val androidKey: ByteArray?
|
|
||||||
|
|
||||||
mRand.nextBytes(seed)
|
|
||||||
mRand.nextBytes(key)
|
|
||||||
|
|
||||||
val androidAESKey = AndroidAESKeyTransformer()
|
|
||||||
androidKey = androidAESKey.transformMasterKey(seed, key, rounds)
|
|
||||||
|
|
||||||
val nativeAESKey = NativeAESKeyTransformer()
|
|
||||||
nativeKey = nativeAESKey.transformMasterKey(seed, key, rounds)
|
|
||||||
|
|
||||||
assertArrayEquals("Does not match", androidKey, nativeKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.tests.crypto
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
|
||||||
|
|
||||||
import junit.framework.TestCase
|
|
||||||
|
|
||||||
import java.security.InvalidAlgorithmParameterException
|
|
||||||
import java.security.InvalidKeyException
|
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.util.Random
|
|
||||||
|
|
||||||
import javax.crypto.BadPaddingException
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.IllegalBlockSizeException
|
|
||||||
import javax.crypto.NoSuchPaddingException
|
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
import org.junit.Assert.assertArrayEquals
|
|
||||||
|
|
||||||
class AESTest : TestCase() {
|
|
||||||
|
|
||||||
private val mRand = Random()
|
|
||||||
|
|
||||||
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, IllegalBlockSizeException::class, BadPaddingException::class, InvalidAlgorithmParameterException::class)
|
|
||||||
fun testEncrypt() {
|
|
||||||
// Test above below and at the blocksize
|
|
||||||
testFinal(15)
|
|
||||||
testFinal(16)
|
|
||||||
testFinal(17)
|
|
||||||
|
|
||||||
// Test random larger sizes
|
|
||||||
val size = mRand.nextInt(494) + 18
|
|
||||||
testFinal(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(NoSuchAlgorithmException::class, NoSuchPaddingException::class, IllegalBlockSizeException::class, BadPaddingException::class, InvalidKeyException::class, InvalidAlgorithmParameterException::class)
|
|
||||||
private fun testFinal(dataSize: Int) {
|
|
||||||
|
|
||||||
// Generate some input
|
|
||||||
val input = ByteArray(dataSize)
|
|
||||||
mRand.nextBytes(input)
|
|
||||||
|
|
||||||
// Generate key
|
|
||||||
val keyArray = ByteArray(32)
|
|
||||||
mRand.nextBytes(keyArray)
|
|
||||||
val key = SecretKeySpec(keyArray, "AES")
|
|
||||||
|
|
||||||
// Generate IV
|
|
||||||
val ivArray = ByteArray(16)
|
|
||||||
mRand.nextBytes(ivArray)
|
|
||||||
val iv = IvParameterSpec(ivArray)
|
|
||||||
|
|
||||||
val android = CipherFactory.getInstance("AES/CBC/PKCS5Padding", true)
|
|
||||||
android.init(Cipher.ENCRYPT_MODE, key, iv)
|
|
||||||
val outAndroid = android.doFinal(input, 0, dataSize)
|
|
||||||
|
|
||||||
val nat = CipherFactory.getInstance("AES/CBC/PKCS5Padding")
|
|
||||||
nat.init(Cipher.ENCRYPT_MODE, key, iv)
|
|
||||||
val outNative = nat.doFinal(input, 0, dataSize)
|
|
||||||
|
|
||||||
assertArrayEquals("Arrays differ on size: $dataSize", outAndroid, outNative)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
|
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePassDX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
@@ -19,44 +19,32 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.tests.crypto
|
package com.kunzisoft.keepass.tests.crypto
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.utils.readBytesLength
|
||||||
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
import org.junit.Assert.assertArrayEquals
|
import org.junit.Assert.assertArrayEquals
|
||||||
|
import org.junit.Test
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.IOException
|
import java.util.*
|
||||||
import java.security.InvalidAlgorithmParameterException
|
|
||||||
import java.security.InvalidKeyException
|
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.util.Random
|
|
||||||
|
|
||||||
import javax.crypto.BadPaddingException
|
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.CipherInputStream
|
||||||
import javax.crypto.CipherOutputStream
|
import javax.crypto.CipherOutputStream
|
||||||
import javax.crypto.IllegalBlockSizeException
|
|
||||||
import javax.crypto.NoSuchPaddingException
|
|
||||||
|
|
||||||
import junit.framework.TestCase
|
class EncryptionTest {
|
||||||
|
|
||||||
import com.kunzisoft.keepass.crypto.CipherFactory
|
|
||||||
import com.kunzisoft.keepass.crypto.engine.AesEngine
|
|
||||||
import com.kunzisoft.keepass.stream.BetterCipherInputStream
|
|
||||||
import com.kunzisoft.keepass.stream.LittleEndianDataInputStream
|
|
||||||
|
|
||||||
class CipherTest : TestCase() {
|
|
||||||
private val rand = Random()
|
private val rand = Random()
|
||||||
|
|
||||||
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidAlgorithmParameterException::class, IllegalBlockSizeException::class, BadPaddingException::class)
|
@Test
|
||||||
fun testCipherFactory() {
|
fun testCipherFactory() {
|
||||||
val key = ByteArray(32)
|
val key = ByteArray(32)
|
||||||
|
rand.nextBytes(key)
|
||||||
|
|
||||||
val iv = ByteArray(16)
|
val iv = ByteArray(16)
|
||||||
|
rand.nextBytes(iv)
|
||||||
|
|
||||||
val plaintext = ByteArray(1024)
|
val plaintext = ByteArray(1024)
|
||||||
|
|
||||||
rand.nextBytes(key)
|
|
||||||
rand.nextBytes(iv)
|
|
||||||
rand.nextBytes(plaintext)
|
rand.nextBytes(plaintext)
|
||||||
|
|
||||||
val aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID)
|
val aes = EncryptionAlgorithm.AESRijndael.cipherEngine
|
||||||
val encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv)
|
val encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv)
|
||||||
val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv)
|
val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv)
|
||||||
|
|
||||||
@@ -66,20 +54,20 @@ class CipherTest : TestCase() {
|
|||||||
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext)
|
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class, NoSuchPaddingException::class, InvalidAlgorithmParameterException::class, IllegalBlockSizeException::class, BadPaddingException::class, IOException::class)
|
@Test
|
||||||
fun testCipherStreams() {
|
fun testCipherStreams() {
|
||||||
val MESSAGE_LENGTH = 1024
|
val length = 1024
|
||||||
|
|
||||||
val key = ByteArray(32)
|
val key = ByteArray(32)
|
||||||
val iv = ByteArray(16)
|
|
||||||
|
|
||||||
val plaintext = ByteArray(MESSAGE_LENGTH)
|
|
||||||
|
|
||||||
rand.nextBytes(key)
|
rand.nextBytes(key)
|
||||||
|
|
||||||
|
val iv = ByteArray(16)
|
||||||
rand.nextBytes(iv)
|
rand.nextBytes(iv)
|
||||||
|
|
||||||
|
val plaintext = ByteArray(length)
|
||||||
rand.nextBytes(plaintext)
|
rand.nextBytes(plaintext)
|
||||||
|
|
||||||
val aes = CipherFactory.getInstance(AesEngine.CIPHER_UUID)
|
val aes = EncryptionAlgorithm.AESRijndael.cipherEngine
|
||||||
val encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv)
|
val encrypt = aes.getCipher(Cipher.ENCRYPT_MODE, key, iv)
|
||||||
val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv)
|
val decrypt = aes.getCipher(Cipher.DECRYPT_MODE, key, iv)
|
||||||
|
|
||||||
@@ -91,10 +79,9 @@ class CipherTest : TestCase() {
|
|||||||
val secrettext = bos.toByteArray()
|
val secrettext = bos.toByteArray()
|
||||||
|
|
||||||
val bis = ByteArrayInputStream(secrettext)
|
val bis = ByteArrayInputStream(secrettext)
|
||||||
val cis = BetterCipherInputStream(bis, decrypt)
|
val cis = CipherInputStream(bis, decrypt)
|
||||||
val lis = LittleEndianDataInputStream(cis)
|
|
||||||
|
|
||||||
val decrypttext = lis.readBytes(MESSAGE_LENGTH)
|
val decrypttext = cis.readBytesLength(length)
|
||||||
|
|
||||||
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext)
|
assertArrayEquals("Encryption and decryption failed", plaintext, decrypttext)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package com.kunzisoft.keepass.tests.stream
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import com.kunzisoft.keepass.utils.readAllBytes
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryCache
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryFile
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import junit.framework.TestCase.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import java.io.DataInputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
class BinaryDataTest {
|
||||||
|
|
||||||
|
private val context: Context by lazy {
|
||||||
|
InstrumentationRegistry.getInstrumentation().context
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cacheDirectory = UriUtil.getBinaryDir(InstrumentationRegistry.getInstrumentation().targetContext)
|
||||||
|
private val fileA = File(cacheDirectory, TEST_FILE_CACHE_A)
|
||||||
|
private val fileB = File(cacheDirectory, TEST_FILE_CACHE_B)
|
||||||
|
private val fileC = File(cacheDirectory, TEST_FILE_CACHE_C)
|
||||||
|
|
||||||
|
private val binaryCache = BinaryCache()
|
||||||
|
|
||||||
|
private fun saveBinary(asset: String, binaryData: BinaryFile) {
|
||||||
|
context.assets.open(asset).use { assetInputStream ->
|
||||||
|
binaryData.getOutputDataStream(binaryCache).use { binaryOutputStream ->
|
||||||
|
assetInputStream.readAllBytes(DEFAULT_BUFFER_SIZE) { buffer ->
|
||||||
|
binaryOutputStream.write(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSaveTextInCache() {
|
||||||
|
val binaryA = BinaryFile(fileA)
|
||||||
|
val binaryB = BinaryFile(fileB)
|
||||||
|
saveBinary(TEST_TEXT_ASSET, binaryA)
|
||||||
|
saveBinary(TEST_TEXT_ASSET, binaryB)
|
||||||
|
assertEquals("Save text binary length failed.", binaryA.getSize(), binaryB.getSize())
|
||||||
|
assertEquals("Save text binary MD5 failed.", binaryA.binaryHash(), binaryB.binaryHash())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSaveImageInCache() {
|
||||||
|
val binaryA = BinaryFile(fileA)
|
||||||
|
val binaryB = BinaryFile(fileB)
|
||||||
|
saveBinary(TEST_IMAGE_ASSET, binaryA)
|
||||||
|
saveBinary(TEST_IMAGE_ASSET, binaryB)
|
||||||
|
assertEquals("Save image binary length failed.", binaryA.getSize(), binaryB.getSize())
|
||||||
|
assertEquals("Save image binary failed.", binaryA.binaryHash(), binaryB.binaryHash())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCompressText() {
|
||||||
|
val binaryA = BinaryFile(fileA)
|
||||||
|
val binaryB = BinaryFile(fileB)
|
||||||
|
val binaryC = BinaryFile(fileC)
|
||||||
|
saveBinary(TEST_TEXT_ASSET, binaryA)
|
||||||
|
saveBinary(TEST_TEXT_ASSET, binaryB)
|
||||||
|
saveBinary(TEST_TEXT_ASSET, binaryC)
|
||||||
|
binaryA.compress(binaryCache)
|
||||||
|
binaryB.compress(binaryCache)
|
||||||
|
assertEquals("Compress text length failed.", binaryA.getSize(), binaryB.getSize())
|
||||||
|
assertEquals("Compress text MD5 failed.", binaryA.binaryHash(), binaryB.binaryHash())
|
||||||
|
binaryB.decompress(binaryCache)
|
||||||
|
assertEquals("Decompress text length failed.", binaryB.getSize(), binaryC.getSize())
|
||||||
|
assertEquals("Decompress text MD5 failed.", binaryB.binaryHash(), binaryC.binaryHash())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCompressImage() {
|
||||||
|
val binaryA = BinaryFile(fileA)
|
||||||
|
var binaryB = BinaryFile(fileB)
|
||||||
|
val binaryC = BinaryFile(fileC)
|
||||||
|
saveBinary(TEST_IMAGE_ASSET, binaryA)
|
||||||
|
saveBinary(TEST_IMAGE_ASSET, binaryB)
|
||||||
|
saveBinary(TEST_IMAGE_ASSET, binaryC)
|
||||||
|
binaryA.compress(binaryCache)
|
||||||
|
binaryB.compress(binaryCache)
|
||||||
|
assertEquals("Compress image length failed.", binaryA.getSize(), binaryA.getSize())
|
||||||
|
assertEquals("Compress image failed.", binaryA.binaryHash(), binaryA.binaryHash())
|
||||||
|
binaryB = BinaryFile(fileB, true)
|
||||||
|
binaryB.decompress(binaryCache)
|
||||||
|
assertEquals("Decompress image length failed.", binaryB.getSize(), binaryC.getSize())
|
||||||
|
assertEquals("Decompress image failed.", binaryB.binaryHash(), binaryC.binaryHash())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCompressBytes() {
|
||||||
|
// Test random byte array
|
||||||
|
val byteArray = ByteArray(50)
|
||||||
|
Random.nextBytes(byteArray)
|
||||||
|
testCompressBytes(byteArray)
|
||||||
|
|
||||||
|
// Test empty byte array
|
||||||
|
testCompressBytes(ByteArray(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testCompressBytes(byteArray: ByteArray) {
|
||||||
|
val binaryA = binaryCache.getBinaryData("0", true)
|
||||||
|
binaryA.getOutputDataStream(binaryCache).use { outputStream ->
|
||||||
|
outputStream.write(byteArray)
|
||||||
|
}
|
||||||
|
val binaryB = binaryCache.getBinaryData("1", true)
|
||||||
|
binaryB.getOutputDataStream(binaryCache).use { outputStream ->
|
||||||
|
outputStream.write(byteArray)
|
||||||
|
}
|
||||||
|
val binaryC = binaryCache.getBinaryData("2", true)
|
||||||
|
binaryC.getOutputDataStream(binaryCache).use { outputStream ->
|
||||||
|
outputStream.write(byteArray)
|
||||||
|
}
|
||||||
|
binaryA.compress(binaryCache)
|
||||||
|
binaryB.compress(binaryCache)
|
||||||
|
assertEquals("Compress bytes decompressed failed.", binaryA.isCompressed, true)
|
||||||
|
assertEquals("Compress bytes length failed.", binaryA.getSize(), binaryA.getSize())
|
||||||
|
assertEquals("Compress bytes failed.", binaryA.binaryHash(), binaryA.binaryHash())
|
||||||
|
binaryB.decompress(binaryCache)
|
||||||
|
assertEquals("Decompress bytes decompressed failed.", binaryB.isCompressed, false)
|
||||||
|
assertEquals("Decompress bytes length failed.", binaryB.getSize(), binaryC.getSize())
|
||||||
|
assertEquals("Decompress bytes failed.", binaryB.binaryHash(), binaryC.binaryHash())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testReadText() {
|
||||||
|
val binaryA = BinaryFile(fileA)
|
||||||
|
saveBinary(TEST_TEXT_ASSET, binaryA)
|
||||||
|
assert(streamAreEquals(context.assets.open(TEST_TEXT_ASSET),
|
||||||
|
binaryA.getInputDataStream(binaryCache)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testReadImage() {
|
||||||
|
val binaryA = BinaryFile(fileA)
|
||||||
|
saveBinary(TEST_IMAGE_ASSET, binaryA)
|
||||||
|
assert(streamAreEquals(context.assets.open(TEST_IMAGE_ASSET),
|
||||||
|
binaryA.getInputDataStream(binaryCache)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun streamAreEquals(inputStreamA: InputStream,
|
||||||
|
inputStreamB: InputStream): Boolean {
|
||||||
|
val bufferA = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||||
|
val bufferB = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||||
|
val dataInputStreamB = DataInputStream(inputStreamB)
|
||||||
|
try {
|
||||||
|
var len: Int
|
||||||
|
while (inputStreamA.read(bufferA).also { len = it } > 0) {
|
||||||
|
dataInputStreamB.readFully(bufferB, 0, len)
|
||||||
|
for (i in 0 until len) {
|
||||||
|
if (bufferA[i] != bufferB[i])
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inputStreamB.read() < 0 // is the end of the second file also.
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
inputStreamA.close()
|
||||||
|
inputStreamB.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TEST_FILE_CACHE_A = "testA"
|
||||||
|
private const val TEST_FILE_CACHE_B = "testB"
|
||||||
|
private const val TEST_FILE_CACHE_C = "testC"
|
||||||
|
private const val TEST_IMAGE_ASSET = "test_image.png"
|
||||||
|
private const val TEST_TEXT_ASSET = "test_text.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.kunzisoft.keepass.tests.template
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.database.element.template.TemplateAttributeOption
|
||||||
|
import junit.framework.TestCase
|
||||||
|
import org.junit.Assert
|
||||||
|
|
||||||
|
class TemplateAttributeOptionTest: TestCase() {
|
||||||
|
|
||||||
|
fun testSerializeOptions() {
|
||||||
|
val options = TemplateAttributeOption().apply {
|
||||||
|
put("TestA", "TestB")
|
||||||
|
put("{D", "}C")
|
||||||
|
put("E,gyu", "15,jk")
|
||||||
|
put("ù*:**", "78:96?545")
|
||||||
|
}
|
||||||
|
|
||||||
|
val strings = TemplateAttributeOption.getStringFromOptions(options)
|
||||||
|
val optionsAfterSerialization = TemplateAttributeOption.getOptionsFromString(strings)
|
||||||
|
val otherString = TemplateAttributeOption.getStringFromOptions(optionsAfterSerialization)
|
||||||
|
|
||||||
|
Assert.assertEquals("Output not equal to input", strings, otherString)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.kunzisoft.keepass.tests.utils
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
|
import junit.framework.TestCase
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class UUIDTest: TestCase() {
|
||||||
|
|
||||||
|
fun testUUID() {
|
||||||
|
val randomUUID = UUID.randomUUID()
|
||||||
|
val hexStringUUID = UuidUtil.toHexString(randomUUID)
|
||||||
|
val retrievedUUID = UuidUtil.fromHexString(hexStringUUID)
|
||||||
|
assertEquals(randomUUID, retrievedUUID)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,9 +20,7 @@
|
|||||||
package com.kunzisoft.keepass.tests.utils
|
package com.kunzisoft.keepass.tests.utils
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.stream.*
|
import com.kunzisoft.keepass.utils.*
|
||||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
|
||||||
import com.kunzisoft.keepass.utils.UnsignedLong
|
|
||||||
import junit.framework.TestCase
|
import junit.framework.TestCase
|
||||||
import org.junit.Assert.assertArrayEquals
|
import org.junit.Assert.assertArrayEquals
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
@@ -54,7 +52,7 @@ class ValuesTest : TestCase() {
|
|||||||
val orig = ByteArray(8)
|
val orig = ByteArray(8)
|
||||||
setArray(orig, value, 8)
|
setArray(orig, value, 8)
|
||||||
|
|
||||||
assertArrayEquals(orig, longTo8Bytes(bytes64ToLong(orig)))
|
assertArrayEquals(orig, uLongTo8Bytes(bytes64ToULong(orig)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun testReadWriteIntZero() {
|
fun testReadWriteIntZero() {
|
||||||
@@ -133,7 +131,7 @@ class ValuesTest : TestCase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun testReadWriteByte(value: Byte) {
|
private fun testReadWriteByte(value: Byte) {
|
||||||
val dest: Byte = UnsignedInt(UnsignedInt.fromKotlinByte(value)).toKotlinByte()
|
val dest: Byte = UnsignedInt(value.toInt() and 0xFF).toKotlinByte()
|
||||||
assert(value == dest)
|
assert(value == dest)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,13 +142,11 @@ class ValuesTest : TestCase() {
|
|||||||
expected.set(2008, 1, 2, 3, 4, 5)
|
expected.set(2008, 1, 2, 3, 4, 5)
|
||||||
|
|
||||||
val actual = Calendar.getInstance()
|
val actual = Calendar.getInstance()
|
||||||
dateTo5Bytes(expected.time, cal)?.let { buf ->
|
actual.time = DateInstant(bytes5ToDate(dateTo5Bytes(expected.time, cal), cal)).date
|
||||||
actual.time = bytes5ToDate(buf, cal).date
|
|
||||||
}
|
|
||||||
|
|
||||||
val jDate = DateInstant(System.currentTimeMillis())
|
val jDate = DateInstant(System.currentTimeMillis())
|
||||||
val intermediate = DateInstant(jDate)
|
val intermediate = DateInstant(jDate)
|
||||||
val cDate = bytes5ToDate(dateTo5Bytes(intermediate.date)!!)
|
val cDate = DateInstant(bytes5ToDate(dateTo5Bytes(intermediate.date)))
|
||||||
|
|
||||||
assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR))
|
assertEquals("Year mismatch: ", 2008, actual.get(Calendar.YEAR))
|
||||||
assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH))
|
assertEquals("Month mismatch: ", 1, actual.get(Calendar.MONTH))
|
||||||
@@ -183,12 +179,10 @@ class ValuesTest : TestCase() {
|
|||||||
ulongBytes[i] = -1
|
ulongBytes[i] = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
val bos = ByteArrayOutputStream()
|
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||||
val leos = LittleEndianDataOutputStream(bos)
|
byteArrayOutputStream.write(UnsignedLong.MAX_BYTES)
|
||||||
leos.writeLong(UnsignedLong.MAX_VALUE)
|
byteArrayOutputStream.close()
|
||||||
leos.close()
|
val uLongMax = byteArrayOutputStream.toByteArray()
|
||||||
|
|
||||||
val uLongMax = bos.toByteArray()
|
|
||||||
|
|
||||||
assertArrayEquals(ulongBytes, uLongMax)
|
assertArrayEquals(ulongBytes, uLongMax)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 |
@@ -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.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>
|
|
||||||
|
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,29 +27,31 @@
|
|||||||
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" >
|
android:windowSoftInputMode="stateHidden|stateAlwaysHidden" >
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</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,9 +110,9 @@
|
|||||||
<!-- 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">
|
||||||
android:launchMode="singleTask">
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.default_searchable"
|
android:name="android.app.default_searchable"
|
||||||
android:value="com.kunzisoft.keepass.search.SearchResults"
|
android:value="com.kunzisoft.keepass.search.SearchResults"
|
||||||
@@ -129,9 +128,18 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.EntryActivity"
|
android:name="com.kunzisoft.keepass.activities.EntryActivity"
|
||||||
android:configChanges="keyboardHidden" />
|
android:configChanges="keyboardHidden" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.IconPickerActivity"
|
||||||
|
android:configChanges="keyboardHidden" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.KeyGeneratorActivity"
|
||||||
|
android:configChanges="keyboardHidden" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.activities.ImageViewerActivity"
|
||||||
|
android:configChanges="keyboardHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
|
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
|
||||||
android:windowSoftInputMode="adjustPan|stateAlwaysHidden" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
<!-- About and Settings -->
|
<!-- About and Settings -->
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
||||||
@@ -149,7 +157,9 @@
|
|||||||
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" />
|
||||||
@@ -163,37 +173,36 @@
|
|||||||
<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>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService"
|
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.AttachmentFileNotificationService"
|
android:name="com.kunzisoft.keepass.services.AttachmentFileNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService"
|
android:name="com.kunzisoft.keepass.services.ClipboardEntryNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.AdvancedUnlockNotificationService"
|
android:name="com.kunzisoft.keepass.services.AdvancedUnlockNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<!-- Receiver for Autofill -->
|
<!-- Receiver for Autofill -->
|
||||||
<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"
|
||||||
@@ -203,8 +212,9 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.magikeyboard.MagikIME"
|
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"/>
|
||||||
@@ -213,9 +223,17 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService"
|
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
<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>
|
||||||
|
|||||||
1067
app/src/main/java/com/igreenwood/loupe/Loupe.kt
Normal file
@@ -23,45 +23,79 @@ 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.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.inputmethod.InlineSuggestionsRequest
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
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.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 : AppCompatActivity() {
|
class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||||
|
AutofillHelper.buildActivityResultLauncher(this, true)
|
||||||
|
else null
|
||||||
|
|
||||||
|
override fun applyCustomStyle(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finishActivityIfReloadRequested(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
// Retrieve selection mode
|
// Retrieve selection mode
|
||||||
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)
|
||||||
}
|
}
|
||||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
// Build search param
|
||||||
searchInfo.webDomain = concreteWebDomain
|
bundle.getParcelable<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
|
||||||
launchSelection(searchInfo)
|
SearchInfo.getConcreteWebDomain(
|
||||||
|
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
|
||||||
@@ -69,7 +103,7 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
|||||||
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
||||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||||
searchInfo.webDomain = concreteWebDomain
|
searchInfo.webDomain = concreteWebDomain
|
||||||
launchRegistration(searchInfo, registerInfo)
|
launchRegistration(database, searchInfo, registerInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@@ -79,14 +113,11 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchSelection(searchInfo: SearchInfo) {
|
private fun launchSelection(database: Database?,
|
||||||
// Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE)
|
autofillComponent: AutofillComponent?,
|
||||||
val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent)
|
searchInfo: SearchInfo) {
|
||||||
|
|
||||||
if (autofillComponent == null) {
|
if (autofillComponent == null) {
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(Activity.RESULT_CANCELED)
|
||||||
finish()
|
finish()
|
||||||
@@ -98,28 +129,28 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
|||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(Activity.RESULT_CANCELED)
|
||||||
finish()
|
finish()
|
||||||
} else {
|
} else {
|
||||||
val database = Database.getInstance()
|
|
||||||
val readOnly = database.isReadOnly
|
|
||||||
// If database is open
|
// If database is open
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
Database.getInstance(),
|
database,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
{ items ->
|
{ openedDatabase, items ->
|
||||||
// Items found
|
// Items found
|
||||||
AutofillHelper.buildResponseAndSetResult(this, items)
|
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
|
||||||
finish()
|
finish()
|
||||||
},
|
},
|
||||||
{
|
{ openedDatabase ->
|
||||||
// Show the database UI to select the entry
|
// Show the database UI to select the entry
|
||||||
GroupActivity.launchForAutofillResult(this,
|
GroupActivity.launchForAutofillResult(this,
|
||||||
readOnly,
|
openedDatabase,
|
||||||
autofillComponent,
|
mAutofillActivityResultLauncher,
|
||||||
searchInfo,
|
autofillComponent,
|
||||||
false)
|
searchInfo,
|
||||||
|
false)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// If database not open
|
// If database not open
|
||||||
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
||||||
|
mAutofillActivityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
@@ -127,7 +158,9 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchRegistration(searchInfo: SearchInfo, registerInfo: RegisterInfo?) {
|
private fun launchRegistration(database: Database?,
|
||||||
|
searchInfo: SearchInfo,
|
||||||
|
registerInfo: RegisterInfo?) {
|
||||||
if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
|
if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
|
||||||
PreferencesUtil.applicationIdBlocklist(this))
|
PreferencesUtil.applicationIdBlocklist(this))
|
||||||
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
|
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
|
||||||
@@ -135,25 +168,26 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
|||||||
showBlockRestartMessage()
|
showBlockRestartMessage()
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(Activity.RESULT_CANCELED)
|
||||||
} else {
|
} else {
|
||||||
val database = Database.getInstance()
|
val readOnly = database?.isReadOnly != false
|
||||||
val readOnly = database.isReadOnly
|
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
database,
|
database,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
{ _ ->
|
{ openedDatabase, _ ->
|
||||||
if (!readOnly) {
|
if (!readOnly) {
|
||||||
// Show the database UI to select the entry
|
// Show the database UI to select the entry
|
||||||
GroupActivity.launchForRegistration(this,
|
GroupActivity.launchForRegistration(this,
|
||||||
registerInfo)
|
openedDatabase,
|
||||||
|
registerInfo)
|
||||||
} else {
|
} else {
|
||||||
showReadOnlySaveMessage()
|
showReadOnlySaveMessage()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{ openedDatabase ->
|
||||||
if (!readOnly) {
|
if (!readOnly) {
|
||||||
// Show the database UI to select the entry
|
// Show the database UI to select the entry
|
||||||
GroupActivity.launchForRegistration(this,
|
GroupActivity.launchForRegistration(this,
|
||||||
registerInfo)
|
openedDatabase,
|
||||||
|
registerInfo)
|
||||||
} else {
|
} else {
|
||||||
showReadOnlySaveMessage()
|
showReadOnlySaveMessage()
|
||||||
}
|
}
|
||||||
@@ -177,53 +211,47 @@ class AutofillLauncherActivity : AppCompatActivity() {
|
|||||||
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_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID"
|
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
|
||||||
private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN"
|
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
||||||
private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME"
|
private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION"
|
||||||
|
|
||||||
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
|
private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO"
|
||||||
|
|
||||||
fun getAuthIntentSenderForSelection(context: Context,
|
fun getPendingIntentForSelection(context: Context,
|
||||||
searchInfo: SearchInfo? = null,
|
searchInfo: SearchInfo? = null,
|
||||||
inlineSuggestionsRequest: InlineSuggestionsRequest? = null): IntentSender {
|
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)
|
|
||||||
}
|
|
||||||
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).intentSender
|
},
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
} else {
|
||||||
|
PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAuthIntentSenderForRegistration(context: Context,
|
fun getPendingIntentForRegistration(context: Context,
|
||||||
registerInfo: RegisterInfo): IntentSender {
|
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).intentSender
|
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,72 +32,91 @@ 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 android.widget.Toast
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.appcompat.app.AlertDialog
|
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.helpers.ReadOnlyHelper
|
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
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.Entry
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.StreamDirection
|
import com.kunzisoft.keepass.otp.OtpType
|
||||||
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
|
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
|
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import com.kunzisoft.keepass.utils.createDocument
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
import com.kunzisoft.keepass.utils.onCreateDocumentResult
|
import com.kunzisoft.keepass.view.changeControlColor
|
||||||
import com.kunzisoft.keepass.view.EntryContentsView
|
import com.kunzisoft.keepass.view.changeTitleColor
|
||||||
import com.kunzisoft.keepass.view.showActionError
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
|
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.HashMap
|
|
||||||
|
|
||||||
class EntryActivity : LockingActivity() {
|
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 entryContentsView: EntryContentsView? = null
|
private var tagsListView: RecyclerView? = null
|
||||||
private var entryProgress: ProgressBar? = 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 mDatabase: Database? = null
|
private val mEntryViewModel: EntryViewModel by viewModels()
|
||||||
|
|
||||||
private var mEntry: Entry? = null
|
private val mEntryActivityEducation = EntryActivityEducation(this)
|
||||||
|
|
||||||
private var mIsHistory: Boolean = false
|
private var mMainEntryId: NodeId<UUID>? = null
|
||||||
private var mEntryLastVersion: Entry? = null
|
private var mHistoryPosition: Int = -1
|
||||||
private var mEntryHistoryPosition: Int = -1
|
private var mEntryIsHistory: Boolean = false
|
||||||
|
private var mUrl: String? = null
|
||||||
private var mShowPassword: Boolean = 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 mAttachmentSelected: Attachment? = null
|
||||||
|
|
||||||
private var clipboardHelper: ClipboardHelper? = null
|
private var mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) {
|
||||||
private var mFirstLaunchOfActivity: Boolean = false
|
// Reload the current id from database
|
||||||
|
mEntryViewModel.loadDatabase(mDatabase)
|
||||||
|
}
|
||||||
|
|
||||||
private var iconColor: Int = 0
|
private var mIcon: IconImage? = null
|
||||||
|
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)
|
||||||
@@ -109,58 +128,209 @@ class EntryActivity : LockingActivity() {
|
|||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
mDatabase = Database.getInstance()
|
|
||||||
mReadOnly = mDatabase!!.isReadOnly || mReadOnly
|
|
||||||
|
|
||||||
mShowPassword = !PreferencesUtil.isPasswordMask(this)
|
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
|
||||||
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
|
||||||
iconColor = taIconColor.getColor(0, Color.BLACK)
|
|
||||||
taIconColor.recycle()
|
|
||||||
|
|
||||||
// Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set
|
|
||||||
invalidateOptionsMenu()
|
|
||||||
|
|
||||||
// Get views
|
// 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)
|
||||||
entryContentsView = findViewById(R.id.entry_contents)
|
tagsListView = findViewById(R.id.entry_tags_list_view)
|
||||||
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
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)
|
||||||
|
|
||||||
|
// Empty title
|
||||||
|
collapsingToolbarLayout?.title = " "
|
||||||
|
toolbar?.title = " "
|
||||||
|
|
||||||
|
// Retrieve the textColor to tint the toolbar
|
||||||
|
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||||
|
val taControlColor = theme.obtainStyledAttributes(intArrayOf(R.attr.toolbarColorControl))
|
||||||
|
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
|
||||||
|
try {
|
||||||
|
intent.getParcelableExtra<NodeId<UUID>?>(KEY_ENTRY)?.let { mainEntryId ->
|
||||||
|
intent.removeExtra(KEY_ENTRY)
|
||||||
|
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
|
||||||
|
intent.removeExtra(KEY_ENTRY_HISTORY_POSITION)
|
||||||
|
|
||||||
|
mEntryViewModel.loadEntry(mDatabase, mainEntryId, historyPosition)
|
||||||
|
}
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
Log.e(TAG, "Unable to retrieve the entry key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init SAF manager
|
||||||
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
mExternalFileHelper?.buildCreateDocument { createdFileUri ->
|
||||||
|
mAttachmentSelected?.let { attachment ->
|
||||||
|
if (createdFileUri != null) {
|
||||||
|
mAttachmentFileBinderManager
|
||||||
|
?.startDownloadAttachment(createdFileUri, attachment)
|
||||||
|
}
|
||||||
|
mAttachmentSelected = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Init attachment service binder manager
|
||||||
|
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||||
|
|
||||||
lockView?.setOnClickListener {
|
lockView?.setOnClickListener {
|
||||||
lockAndExit()
|
lockAndExit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Focus view to reinitialize timeout
|
mEntryViewModel.sectionSelected.observe(this) { entrySection ->
|
||||||
coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
|
entryContentTab?.getTabAt(entrySection.position)?.select()
|
||||||
|
}
|
||||||
|
|
||||||
// Init the clipboard helper
|
mEntryViewModel.entryInfoHistory.observe(this) { entryInfoHistory ->
|
||||||
clipboardHelper = ClipboardHelper(this)
|
if (entryInfoHistory != null) {
|
||||||
mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true
|
this.mMainEntryId = entryInfoHistory.mainEntryId
|
||||||
|
|
||||||
// Init attachment service binder manager
|
// Manage history position
|
||||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
val historyPosition = entryInfoHistory.historyPosition
|
||||||
|
this.mHistoryPosition = historyPosition
|
||||||
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
|
val entryIsHistory = historyPosition > -1
|
||||||
when (actionTask) {
|
this.mEntryIsHistory = entryIsHistory
|
||||||
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
|
// Assign history dedicated view
|
||||||
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
|
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
|
||||||
// Close the current activity after an history action
|
if (entryIsHistory) {
|
||||||
if (result.isSuccess)
|
collapsingToolbarLayout?.contentScrim =
|
||||||
finish()
|
ColorDrawable(mColorAccent)
|
||||||
}
|
}
|
||||||
ACTION_DATABASE_RELOAD_TASK -> {
|
|
||||||
// Close the current activity
|
val entryInfo = entryInfoHistory.entryInfo
|
||||||
finish()
|
// Manage entry copy to start notification if allowed (at the first start)
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
// Manage entry to launch copying notification if allowed
|
||||||
|
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
||||||
|
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
||||||
|
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
|
||||||
|
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Assign title icon
|
||||||
|
mIcon = entryInfo.icon
|
||||||
|
// Assign title text
|
||||||
|
val entryTitle =
|
||||||
|
if (entryInfo.title.isNotEmpty()) entryInfo.title else UuidUtil.toHexString(entryInfo.id)
|
||||||
|
collapsingToolbarLayout?.title = entryTitle
|
||||||
|
toolbar?.title = entryTitle
|
||||||
|
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()
|
||||||
|
mEntryLoaded = true
|
||||||
|
} else {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
// Refresh Menu
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement ->
|
||||||
|
if (otpElement == null) {
|
||||||
|
entryProgress?.visibility = View.GONE
|
||||||
|
} else when (otpElement.type) {
|
||||||
|
// Only add token if HOTP
|
||||||
|
OtpType.HOTP -> {
|
||||||
|
entryProgress?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
// Refresh view if TOTP
|
||||||
|
OtpType.TOTP -> {
|
||||||
|
entryProgress?.apply {
|
||||||
|
max = otpElement.period
|
||||||
|
setProgressCompat(otpElement.secondsRemaining, true)
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
coordinatorLayout?.showActionError(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mEntryViewModel.attachmentSelected.observe(this) { attachmentSelected ->
|
||||||
|
mAttachmentSelected = attachmentSelected
|
||||||
|
mExternalFileHelper?.createDocument(attachmentSelected.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryViewModel.historySelected.observe(this) { historySelected ->
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
launch(
|
||||||
|
this,
|
||||||
|
database,
|
||||||
|
historySelected.nodeId,
|
||||||
|
historySelected.historyPosition,
|
||||||
|
mEntryActivityResultLauncher
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finishActivityIfReloadRequested(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun viewToInvalidateTimeout(): View? {
|
||||||
|
return coordinatorLayout
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
|
mEntryViewModel.loadDatabase(database)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseActionFinished(
|
||||||
|
database: Database,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
|
when (actionTask) {
|
||||||
|
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
|
||||||
|
ACTION_DATABASE_DELETE_ENTRY_HISTORY -> {
|
||||||
|
// Close the current activity after an history action
|
||||||
|
if (result.isSuccess)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
coordinatorLayout?.showActionErrorIfNeeded(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -173,61 +343,19 @@ class EntryActivity : LockingActivity() {
|
|||||||
View.GONE
|
View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Entry from UUID
|
|
||||||
try {
|
|
||||||
val keyEntry: NodeId<UUID>? = intent.getParcelableExtra(KEY_ENTRY)
|
|
||||||
if (keyEntry != null) {
|
|
||||||
mEntry = mDatabase?.getEntryById(keyEntry)
|
|
||||||
mEntryLastVersion = mEntry
|
|
||||||
}
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
Log.e(TAG, "Unable to retrieve the entry key")
|
|
||||||
}
|
|
||||||
|
|
||||||
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, mEntryHistoryPosition)
|
|
||||||
mEntryHistoryPosition = historyPosition
|
|
||||||
if (historyPosition >= 0) {
|
|
||||||
mIsHistory = true
|
|
||||||
mEntry = mEntry?.getHistory()?.get(historyPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mEntry == null) {
|
|
||||||
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show()
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update last access time.
|
|
||||||
mEntry?.touch(modified = false, touchParents = false)
|
|
||||||
|
|
||||||
mEntry?.let { entry ->
|
|
||||||
// Fill data in resume to update from EntryEditActivity
|
|
||||||
fillEntryDataInContentsView(entry)
|
|
||||||
// Refresh Menu
|
|
||||||
invalidateOptionsMenu()
|
|
||||||
|
|
||||||
val entryInfo = entry.getEntryInfo(mDatabase)
|
|
||||||
// Manage entry copy to start notification if allowed
|
|
||||||
if (mFirstLaunchOfActivity) {
|
|
||||||
// Manage entry to launch copying notification if allowed
|
|
||||||
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
|
||||||
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
|
||||||
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
|
|
||||||
MagikIME.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mAttachmentFileBinderManager?.apply {
|
mAttachmentFileBinderManager?.apply {
|
||||||
registerProgressTask()
|
registerProgressTask()
|
||||||
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
||||||
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
||||||
entryContentsView?.putAttachment(entryAttachmentState)
|
mEntryViewModel.onAttachmentAction(entryAttachmentState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mFirstLaunchOfActivity = false
|
// Keep the screen on
|
||||||
|
if (PreferencesUtil.isKeepScreenOnEnabled(this)) {
|
||||||
|
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -236,221 +364,93 @@ class EntryActivity : LockingActivity() {
|
|||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fillEntryDataInContentsView(entry: Entry) {
|
private fun applyToolbarColors() {
|
||||||
|
appBarLayout?.setBackgroundColor(mBackgroundColor ?: mColorPrimary)
|
||||||
val entryInfo = entry.getEntryInfo(mDatabase)
|
collapsingToolbarLayout?.contentScrim = ColorDrawable(mBackgroundColor ?: mColorPrimary)
|
||||||
|
val backgroundDarker = if (mBackgroundColor != null) {
|
||||||
// Assign title icon
|
ColorUtils.blendARGB(mBackgroundColor!!, Color.WHITE, 0.1f)
|
||||||
titleIconView?.assignDatabaseIcon(mDatabase!!.drawFactory, entryInfo.icon, iconColor)
|
|
||||||
|
|
||||||
// Assign title text
|
|
||||||
val entryTitle = entryInfo.title
|
|
||||||
collapsingToolbarLayout?.title = entryTitle
|
|
||||||
toolbar?.title = entryTitle
|
|
||||||
|
|
||||||
// Assign basic fields
|
|
||||||
entryContentsView?.assignUserName(entryInfo.username) {
|
|
||||||
clipboardHelper?.timeoutCopyToClipboard(entryInfo.username,
|
|
||||||
getString(R.string.copy_field,
|
|
||||||
getString(R.string.entry_user_name)))
|
|
||||||
}
|
|
||||||
|
|
||||||
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
|
||||||
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
|
|
||||||
val allowCopyPasswordAndProtectedFields =
|
|
||||||
PreferencesUtil.allowCopyPasswordAndProtectedFields(this)
|
|
||||||
|
|
||||||
val showWarningClipboardDialogOnClickListener = View.OnClickListener {
|
|
||||||
AlertDialog.Builder(this@EntryActivity)
|
|
||||||
.setMessage(getString(R.string.allow_copy_password_warning) +
|
|
||||||
"\n\n" +
|
|
||||||
getString(R.string.clipboard_warning))
|
|
||||||
.create().apply {
|
|
||||||
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) { dialog, _ ->
|
|
||||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true)
|
|
||||||
dialog.dismiss()
|
|
||||||
fillEntryDataInContentsView(entry)
|
|
||||||
}
|
|
||||||
setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ ->
|
|
||||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, false)
|
|
||||||
dialog.dismiss()
|
|
||||||
fillEntryDataInContentsView(entry)
|
|
||||||
}
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) {
|
|
||||||
View.OnClickListener {
|
|
||||||
clipboardHelper?.timeoutCopyToClipboard(entryInfo.password,
|
|
||||||
getString(R.string.copy_field,
|
|
||||||
getString(R.string.entry_password)))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// If dialog not already shown
|
mColorBackground
|
||||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
|
||||||
showWarningClipboardDialogOnClickListener
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
entryContentsView?.assignPassword(entryInfo.password,
|
titleIconView?.background?.colorFilter = BlendModeColorFilterCompat
|
||||||
allowCopyPasswordAndProtectedFields,
|
.createBlendModeColorFilterCompat(backgroundDarker, BlendModeCompat.SRC_IN)
|
||||||
onPasswordCopyClickListener)
|
mIcon?.let { icon ->
|
||||||
|
titleIconView?.let { iconView ->
|
||||||
//Assign OTP field
|
mIconDrawableFactory?.assignDatabaseIcon(
|
||||||
entry.getOtpElement()?.let { otpElement ->
|
iconView,
|
||||||
entryContentsView?.assignOtp(otpElement, entryProgress) {
|
icon,
|
||||||
clipboardHelper?.timeoutCopyToClipboard(
|
mForegroundColor ?: mColorAccent
|
||||||
otpElement.token,
|
|
||||||
getString(R.string.copy_field, getString(R.string.entry_otp))
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
toolbar?.changeControlColor(mForegroundColor ?: mControlColor)
|
||||||
entryContentsView?.assignURL(entryInfo.url)
|
collapsingToolbarLayout?.changeTitleColor(mForegroundColor ?: mControlColor)
|
||||||
entryContentsView?.assignNotes(entryInfo.notes)
|
|
||||||
|
|
||||||
// Assign custom fields
|
|
||||||
if (mDatabase?.allowEntryCustomFields() == true) {
|
|
||||||
entryContentsView?.clearExtraFields()
|
|
||||||
entryInfo.customFields.forEach { field ->
|
|
||||||
val label = field.name
|
|
||||||
// OTP field is already managed in dedicated view
|
|
||||||
if (label != OtpEntryFields.OTP_TOKEN_FIELD) {
|
|
||||||
val value = field.protectedValue
|
|
||||||
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
|
||||||
if (allowCopyProtectedField) {
|
|
||||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
|
|
||||||
clipboardHelper?.timeoutCopyToClipboard(
|
|
||||||
value.toString(),
|
|
||||||
getString(R.string.copy_field, label)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If dialog not already shown
|
|
||||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
|
||||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
|
|
||||||
} else {
|
|
||||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
|
|
||||||
|
|
||||||
// Manage attachments
|
|
||||||
entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
|
|
||||||
createDocument(this, attachmentItem.name)?.let { requestCode ->
|
|
||||||
mAttachmentsToDownload[requestCode] = attachmentItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign dates
|
|
||||||
entryContentsView?.assignCreationDate(entryInfo.creationTime)
|
|
||||||
entryContentsView?.assignModificationDate(entryInfo.modificationTime)
|
|
||||||
entryContentsView?.setExpires(entryInfo.expires, entryInfo.expiryTime)
|
|
||||||
|
|
||||||
// Manage history
|
|
||||||
historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE
|
|
||||||
if (mIsHistory) {
|
|
||||||
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
|
||||||
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
|
||||||
taColorAccent.recycle()
|
|
||||||
}
|
|
||||||
entryContentsView?.assignHistory(entry.getHistory()) { historyItem, position ->
|
|
||||||
launch(this, historyItem, mReadOnly, position)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign special data
|
|
||||||
entryContentsView?.assignUUID(entry.nodeId.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
|
|
||||||
when (requestCode) {
|
|
||||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE ->
|
|
||||||
// Not directly get the entry from intent data but from database
|
|
||||||
mEntry?.let {
|
|
||||||
fillEntryDataInContentsView(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
|
||||||
if (createdFileUri != null) {
|
|
||||||
mAttachmentsToDownload[requestCode]?.let { attachmentToDownload ->
|
|
||||||
mAttachmentFileBinderManager
|
|
||||||
?.startDownloadAttachment(createdFileUri, attachmentToDownload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
|
if (mEntryLoaded) {
|
||||||
|
val inflater = menuInflater
|
||||||
|
|
||||||
val inflater = menuInflater
|
inflater.inflate(R.menu.entry, menu)
|
||||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
inflater.inflate(R.menu.database, menu)
|
||||||
inflater.inflate(R.menu.entry, menu)
|
|
||||||
inflater.inflate(R.menu.database, menu)
|
|
||||||
if (mIsHistory && !mReadOnly) {
|
|
||||||
inflater.inflate(R.menu.entry_history, menu)
|
|
||||||
}
|
|
||||||
if (mIsHistory || mReadOnly) {
|
|
||||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
|
||||||
menu.findItem(R.id.menu_edit)?.isVisible = false
|
|
||||||
}
|
|
||||||
if (mSpecialMode != SpecialMode.DEFAULT) {
|
|
||||||
menu.findItem(R.id.menu_reload_database)?.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
val gotoUrl = menu.findItem(R.id.menu_goto_url)
|
if (mEntryIsHistory && !mDatabaseReadOnly) {
|
||||||
gotoUrl?.apply {
|
inflater.inflate(R.menu.entry_history, menu)
|
||||||
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
|
}
|
||||||
// so mEntry may not be set
|
|
||||||
if (mEntry == null) {
|
// Show education views
|
||||||
isVisible = false
|
Handler(Looper.getMainLooper()).post {
|
||||||
} else {
|
performedNextEducation(menu)
|
||||||
if (mEntry?.url?.isEmpty() != false) {
|
|
||||||
// disable button if url is not available
|
|
||||||
isVisible = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show education views
|
|
||||||
Handler(Looper.getMainLooper()).post { performedNextEducation(EntryActivityEducation(this), menu) }
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||||
menu: Menu) {
|
if (mUrl?.isEmpty() != false) {
|
||||||
val entryFieldCopyView = entryContentsView?.firstEntryFieldCopyView()
|
menu?.findItem(R.id.menu_goto_url)?.isVisible = false
|
||||||
|
}
|
||||||
|
if (mEntryIsHistory || mDatabaseReadOnly) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if (!mMergeDataAllowed) {
|
||||||
|
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||||
|
}
|
||||||
|
if (mSpecialMode != SpecialMode.DEFAULT) {
|
||||||
|
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||||
|
menu?.findItem(R.id.menu_reload_database)?.isVisible = false
|
||||||
|
}
|
||||||
|
applyToolbarColors()
|
||||||
|
return super.onPrepareOptionsMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performedNextEducation(menu: Menu) {
|
||||||
|
val entryFragment = supportFragmentManager.findFragmentByTag(ENTRY_FRAGMENT_TAG)
|
||||||
|
as? EntryFragment?
|
||||||
|
val entryFieldCopyView: View? = entryFragment?.firstEntryFieldCopyView()
|
||||||
val entryCopyEducationPerformed = entryFieldCopyView != null
|
val entryCopyEducationPerformed = entryFieldCopyView != null
|
||||||
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
&& mEntryActivityEducation.checkAndPerformedEntryCopyEducation(
|
||||||
entryFieldCopyView,
|
entryFieldCopyView,
|
||||||
{
|
{
|
||||||
val appNameString = getString(R.string.app_name)
|
entryFragment.launchEntryCopyEducationAction()
|
||||||
clipboardHelper?.timeoutCopyToClipboard(appNameString,
|
},
|
||||||
getString(R.string.copy_field, appNameString))
|
{
|
||||||
},
|
performedNextEducation(menu)
|
||||||
{
|
})
|
||||||
performedNextEducation(entryActivityEducation, 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)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -458,65 +458,58 @@ class EntryActivity : LockingActivity() {
|
|||||||
|
|
||||||
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 -> {
|
||||||
mEntry?.let {
|
mDatabase?.let { database ->
|
||||||
EntryEditActivity.launch(this@EntryActivity, it)
|
mMainEntryId?.let { entryId ->
|
||||||
|
EntryEditActivity.launchToUpdate(
|
||||||
|
this,
|
||||||
|
database,
|
||||||
|
entryId,
|
||||||
|
mEntryActivityResultLauncher
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_goto_url -> {
|
R.id.menu_goto_url -> {
|
||||||
var url: String = mEntry?.url ?: ""
|
mUrl?.let { url ->
|
||||||
|
UriUtil.gotoUrl(this, url)
|
||||||
// Default http:// if no protocol specified
|
|
||||||
if (!url.contains("://")) {
|
|
||||||
url = "http://$url"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UriUtil.gotoUrl(this, url)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_restore_entry_history -> {
|
R.id.menu_restore_entry_history -> {
|
||||||
mEntryLastVersion?.let { mainEntry ->
|
mMainEntryId?.let { mainEntryId ->
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseRestoreEntryHistory(
|
restoreEntryHistory(
|
||||||
mainEntry,
|
mainEntryId,
|
||||||
mEntryHistoryPosition,
|
mHistoryPosition)
|
||||||
!mReadOnly && mAutoSaveEnable)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
R.id.menu_delete_entry_history -> {
|
R.id.menu_delete_entry_history -> {
|
||||||
mEntryLastVersion?.let { mainEntry ->
|
mMainEntryId?.let { mainEntryId ->
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(
|
deleteEntryHistory(
|
||||||
mainEntry,
|
mainEntryId,
|
||||||
mEntryHistoryPosition,
|
mHistoryPosition)
|
||||||
!mReadOnly && mAutoSaveEnable)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
R.id.menu_save_database -> {
|
R.id.menu_save_database -> {
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly)
|
saveDatabase()
|
||||||
|
}
|
||||||
|
R.id.menu_merge_database -> {
|
||||||
|
mergeDatabase()
|
||||||
}
|
}
|
||||||
R.id.menu_reload_database -> {
|
R.id.menu_reload_database -> {
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseReload(false)
|
reloadDatabase()
|
||||||
}
|
}
|
||||||
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
|
android.R.id.home -> finish() // close this activity and return to preview activity (if there is any)
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
|
|
||||||
outState.putBoolean(KEY_FIRST_LAUNCH_ACTIVITY, mFirstLaunchOfActivity)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
// Transit data in previous Activity after an update
|
// Transit data in previous Activity after an update
|
||||||
Intent().apply {
|
Intent().apply {
|
||||||
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry)
|
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId)
|
||||||
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, this)
|
setResult(Activity.RESULT_OK, this)
|
||||||
}
|
}
|
||||||
super.finish()
|
super.finish()
|
||||||
}
|
}
|
||||||
@@ -524,19 +517,42 @@ class EntryActivity : LockingActivity() {
|
|||||||
companion object {
|
companion object {
|
||||||
private val TAG = EntryActivity::class.java.name
|
private val TAG = EntryActivity::class.java.name
|
||||||
|
|
||||||
private const val KEY_FIRST_LAUNCH_ACTIVITY = "KEY_FIRST_LAUNCH_ACTIVITY"
|
|
||||||
|
|
||||||
const val KEY_ENTRY = "KEY_ENTRY"
|
const val KEY_ENTRY = "KEY_ENTRY"
|
||||||
const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION"
|
const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION"
|
||||||
|
|
||||||
fun launch(activity: Activity, entry: Entry, readOnly: Boolean, historyPosition: Int? = null) {
|
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
|
||||||
val intent = Intent(activity, EntryActivity::class.java)
|
/**
|
||||||
intent.putExtra(KEY_ENTRY, entry.nodeId)
|
* Open standard Entry activity
|
||||||
ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly)
|
*/
|
||||||
if (historyPosition != null)
|
fun launch(activity: Activity,
|
||||||
|
database: Database,
|
||||||
|
entryId: NodeId<UUID>,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
|
if (database.loaded) {
|
||||||
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
|
val intent = Intent(activity, EntryActivity::class.java)
|
||||||
|
intent.putExtra(KEY_ENTRY, entryId)
|
||||||
|
activityResultLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open history Entry activity
|
||||||
|
*/
|
||||||
|
fun launch(activity: Activity,
|
||||||
|
database: Database,
|
||||||
|
entryId: NodeId<UUID>,
|
||||||
|
historyPosition: Int,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
|
if (database.loaded) {
|
||||||
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
|
val intent = Intent(activity, EntryActivity::class.java)
|
||||||
|
intent.putExtra(KEY_ENTRY, entryId)
|
||||||
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition)
|
||||||
activity.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
activityResultLauncher.launch(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,549 +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
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.inputmethod.EditorInfo
|
|
||||||
import android.widget.CompoundButton
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment
|
|
||||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
|
||||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
|
||||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
|
||||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
|
||||||
import com.kunzisoft.keepass.model.*
|
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
|
||||||
import com.kunzisoft.keepass.view.collapse
|
|
||||||
import com.kunzisoft.keepass.view.expand
|
|
||||||
|
|
||||||
class EntryEditFragment: StylishFragment() {
|
|
||||||
|
|
||||||
private lateinit var entryTitleLayoutView: TextInputLayout
|
|
||||||
private lateinit var entryTitleView: EditText
|
|
||||||
private lateinit var entryIconView: ImageView
|
|
||||||
private lateinit var entryUserNameView: EditText
|
|
||||||
private lateinit var entryUrlView: EditText
|
|
||||||
private lateinit var entryPasswordLayoutView: TextInputLayout
|
|
||||||
private lateinit var entryPasswordView: EditText
|
|
||||||
private lateinit var entryPasswordGeneratorView: View
|
|
||||||
private lateinit var entryExpiresCheckBox: CompoundButton
|
|
||||||
private lateinit var entryExpiresTextView: TextView
|
|
||||||
private lateinit var entryNotesView: EditText
|
|
||||||
private lateinit var extraFieldsContainerView: View
|
|
||||||
private lateinit var extraFieldsListView: ViewGroup
|
|
||||||
private lateinit var attachmentsContainerView: View
|
|
||||||
private lateinit var attachmentsListView: RecyclerView
|
|
||||||
|
|
||||||
private lateinit var attachmentsAdapter: EntryAttachmentsItemsAdapter
|
|
||||||
|
|
||||||
private var fontInVisibility: Boolean = false
|
|
||||||
private var iconColor: Int = 0
|
|
||||||
private var expiresInstant: DateInstant = DateInstant.IN_ONE_MONTH
|
|
||||||
|
|
||||||
var drawFactory: IconDrawableFactory? = null
|
|
||||||
var setOnDateClickListener: View.OnClickListener? = null
|
|
||||||
var setOnPasswordGeneratorClickListener: View.OnClickListener? = null
|
|
||||||
var setOnIconViewClickListener: View.OnClickListener? = null
|
|
||||||
var setOnEditCustomField: ((Field) -> Unit)? = null
|
|
||||||
var setOnRemoveAttachment: ((Attachment) -> Unit)? = null
|
|
||||||
|
|
||||||
// Elements to modify the current entry
|
|
||||||
private var mEntryInfo = EntryInfo()
|
|
||||||
private var mLastFocusedEditField: FocusedEditField? = null
|
|
||||||
private var mExtraViewToRequestFocus: EditText? = null
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
||||||
super.onCreateView(inflater, container, savedInstanceState)
|
|
||||||
|
|
||||||
val rootView = inflater.cloneInContext(contextThemed)
|
|
||||||
.inflate(R.layout.fragment_entry_edit_contents, container, false)
|
|
||||||
|
|
||||||
fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(requireContext())
|
|
||||||
|
|
||||||
entryTitleLayoutView = rootView.findViewById(R.id.entry_edit_container_title)
|
|
||||||
entryTitleView = rootView.findViewById(R.id.entry_edit_title)
|
|
||||||
entryIconView = rootView.findViewById(R.id.entry_edit_icon_button)
|
|
||||||
entryIconView.setOnClickListener {
|
|
||||||
setOnIconViewClickListener?.onClick(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
entryUserNameView = rootView.findViewById(R.id.entry_edit_user_name)
|
|
||||||
entryUrlView = rootView.findViewById(R.id.entry_edit_url)
|
|
||||||
entryPasswordLayoutView = rootView.findViewById(R.id.entry_edit_container_password)
|
|
||||||
entryPasswordView = rootView.findViewById(R.id.entry_edit_password)
|
|
||||||
entryPasswordGeneratorView = rootView.findViewById(R.id.entry_edit_password_generator_button)
|
|
||||||
entryPasswordGeneratorView.setOnClickListener {
|
|
||||||
setOnPasswordGeneratorClickListener?.onClick(it)
|
|
||||||
}
|
|
||||||
entryExpiresCheckBox = rootView.findViewById(R.id.entry_edit_expires_checkbox)
|
|
||||||
entryExpiresTextView = rootView.findViewById(R.id.entry_edit_expires_text)
|
|
||||||
entryExpiresTextView.setOnClickListener {
|
|
||||||
if (entryExpiresCheckBox.isChecked)
|
|
||||||
setOnDateClickListener?.onClick(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
entryNotesView = rootView.findViewById(R.id.entry_edit_notes)
|
|
||||||
|
|
||||||
extraFieldsContainerView = rootView.findViewById(R.id.extra_fields_container)
|
|
||||||
extraFieldsListView = rootView.findViewById(R.id.extra_fields_list)
|
|
||||||
|
|
||||||
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
|
|
||||||
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
|
|
||||||
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
|
|
||||||
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
|
|
||||||
if (previousSize > 0 && newSize == 0) {
|
|
||||||
attachmentsContainerView.collapse(true)
|
|
||||||
} else if (previousSize == 0 && newSize == 1) {
|
|
||||||
attachmentsContainerView.expand(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
attachmentsListView.apply {
|
|
||||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
|
||||||
adapter = attachmentsAdapter
|
|
||||||
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
|
||||||
}
|
|
||||||
|
|
||||||
entryExpiresCheckBox.setOnCheckedChangeListener { _, _ ->
|
|
||||||
assignExpiresDateText()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
|
||||||
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
|
||||||
iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE
|
|
||||||
taIconColor?.recycle()
|
|
||||||
|
|
||||||
rootView?.resetAppTimeoutWhenViewFocusedOrChanged(requireContext())
|
|
||||||
|
|
||||||
// Retrieve the new entry after an orientation change
|
|
||||||
if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true)
|
|
||||||
mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
|
|
||||||
else if (savedInstanceState?.containsKey(KEY_TEMP_ENTRY_INFO) == true) {
|
|
||||||
mEntryInfo = savedInstanceState.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
if (savedInstanceState?.containsKey(KEY_LAST_FOCUSED_FIELD) == true) {
|
|
||||||
mLastFocusedEditField = savedInstanceState.getParcelable(KEY_LAST_FOCUSED_FIELD) ?: mLastFocusedEditField
|
|
||||||
}
|
|
||||||
|
|
||||||
populateViewsWithEntry()
|
|
||||||
|
|
||||||
return rootView
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetach() {
|
|
||||||
super.onDetach()
|
|
||||||
|
|
||||||
drawFactory = null
|
|
||||||
setOnDateClickListener = null
|
|
||||||
setOnPasswordGeneratorClickListener = null
|
|
||||||
setOnIconViewClickListener = null
|
|
||||||
setOnRemoveAttachment = null
|
|
||||||
setOnEditCustomField = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getEntryInfo(): EntryInfo? {
|
|
||||||
populateEntryWithViews()
|
|
||||||
return mEntryInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generatePasswordEducationPerformed(entryEditActivityEducation: EntryEditActivityEducation): Boolean {
|
|
||||||
return entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation(
|
|
||||||
entryPasswordGeneratorView,
|
|
||||||
{
|
|
||||||
GeneratePasswordDialogFragment().show(parentFragmentManager, "PasswordGeneratorFragment")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
(activity as? EntryEditActivity?)?.performedNextEducation(entryEditActivityEducation)
|
|
||||||
} catch (ignore: Exception) {}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun populateViewsWithEntry() {
|
|
||||||
// Set info in view
|
|
||||||
icon = mEntryInfo.icon
|
|
||||||
title = mEntryInfo.title
|
|
||||||
username = mEntryInfo.username
|
|
||||||
url = mEntryInfo.url
|
|
||||||
password = mEntryInfo.password
|
|
||||||
expires = mEntryInfo.expires
|
|
||||||
expiryTime = mEntryInfo.expiryTime
|
|
||||||
notes = mEntryInfo.notes
|
|
||||||
assignExtraFields(mEntryInfo.customFields) { fields ->
|
|
||||||
setOnEditCustomField?.invoke(fields)
|
|
||||||
}
|
|
||||||
assignAttachments(mEntryInfo.attachments, StreamDirection.UPLOAD) { attachment ->
|
|
||||||
setOnRemoveAttachment?.invoke(attachment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun populateEntryWithViews() {
|
|
||||||
// Icon already populate
|
|
||||||
mEntryInfo.title = title
|
|
||||||
mEntryInfo.username = username
|
|
||||||
mEntryInfo.url = url
|
|
||||||
mEntryInfo.password = password
|
|
||||||
mEntryInfo.expires = expires
|
|
||||||
mEntryInfo.expiryTime = expiryTime
|
|
||||||
mEntryInfo.notes = notes
|
|
||||||
mEntryInfo.customFields = getExtraFields()
|
|
||||||
mEntryInfo.otpModel = OtpEntryFields.parseFields { key ->
|
|
||||||
getExtraFields().firstOrNull { it.name == key }?.protectedValue?.toString()
|
|
||||||
}?.otpModel
|
|
||||||
mEntryInfo.attachments = getAttachments()
|
|
||||||
}
|
|
||||||
|
|
||||||
var title: String
|
|
||||||
get() {
|
|
||||||
return entryTitleView.text.toString()
|
|
||||||
}
|
|
||||||
set(value) {
|
|
||||||
entryTitleView.setText(value)
|
|
||||||
if (fontInVisibility)
|
|
||||||
entryTitleView.applyFontVisibility()
|
|
||||||
}
|
|
||||||
|
|
||||||
var icon: IconImage
|
|
||||||
get() {
|
|
||||||
return mEntryInfo.icon
|
|
||||||
}
|
|
||||||
set(value) {
|
|
||||||
mEntryInfo.icon = value
|
|
||||||
drawFactory?.let { drawFactory ->
|
|
||||||
entryIconView.assignDatabaseIcon(drawFactory, value, iconColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var username: String
|
|
||||||
get() {
|
|
||||||
return entryUserNameView.text.toString()
|
|
||||||
}
|
|
||||||
set(value) {
|
|
||||||
entryUserNameView.setText(value)
|
|
||||||
if (fontInVisibility)
|
|
||||||
entryUserNameView.applyFontVisibility()
|
|
||||||
}
|
|
||||||
|
|
||||||
var url: String
|
|
||||||
get() {
|
|
||||||
return entryUrlView.text.toString()
|
|
||||||
}
|
|
||||||
set(value) {
|
|
||||||
entryUrlView.setText(value)
|
|
||||||
if (fontInVisibility)
|
|
||||||
entryUrlView.applyFontVisibility()
|
|
||||||
}
|
|
||||||
|
|
||||||
var password: String
|
|
||||||
get() {
|
|
||||||
return entryPasswordView.text.toString()
|
|
||||||
}
|
|
||||||
set(value) {
|
|
||||||
entryPasswordView.setText(value)
|
|
||||||
if (fontInVisibility) {
|
|
||||||
entryPasswordView.applyFontVisibility()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun assignExpiresDateText() {
|
|
||||||
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
|
|
||||||
entryExpiresTextView.setOnClickListener(setOnDateClickListener)
|
|
||||||
expiresInstant.getDateTimeString(resources)
|
|
||||||
} else {
|
|
||||||
entryExpiresTextView.setOnClickListener(null)
|
|
||||||
resources.getString(R.string.never)
|
|
||||||
}
|
|
||||||
if (fontInVisibility)
|
|
||||||
entryExpiresTextView.applyFontVisibility()
|
|
||||||
}
|
|
||||||
|
|
||||||
var expires: Boolean
|
|
||||||
get() {
|
|
||||||
return entryExpiresCheckBox.isChecked
|
|
||||||
}
|
|
||||||
set(value) {
|
|
||||||
if (!value) {
|
|
||||||
expiresInstant = DateInstant.IN_ONE_MONTH
|
|
||||||
}
|
|
||||||
entryExpiresCheckBox.isChecked = value
|
|
||||||
assignExpiresDateText()
|
|
||||||
}
|
|
||||||
|
|
||||||
var expiryTime: DateInstant
|
|
||||||
get() {
|
|
||||||
return if (expires)
|
|
||||||
expiresInstant
|
|
||||||
else
|
|
||||||
DateInstant.NEVER_EXPIRE
|
|
||||||
}
|
|
||||||
set(value) {
|
|
||||||
if (expires)
|
|
||||||
expiresInstant = value
|
|
||||||
assignExpiresDateText()
|
|
||||||
}
|
|
||||||
|
|
||||||
var notes: String
|
|
||||||
get() {
|
|
||||||
return entryNotesView.text.toString()
|
|
||||||
}
|
|
||||||
set(value) {
|
|
||||||
entryNotesView.setText(value)
|
|
||||||
if (fontInVisibility)
|
|
||||||
entryNotesView.applyFontVisibility()
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------
|
|
||||||
* Extra Fields
|
|
||||||
* -------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
private var mExtraFieldsList: MutableList<Field> = ArrayList()
|
|
||||||
private var mOnEditButtonClickListener: ((item: Field)->Unit)? = null
|
|
||||||
|
|
||||||
private fun buildViewFromField(extraField: Field): View? {
|
|
||||||
val inflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
|
||||||
val itemView: View? = inflater?.inflate(R.layout.item_entry_edit_extra_field, extraFieldsListView, false)
|
|
||||||
itemView?.id = View.NO_ID
|
|
||||||
|
|
||||||
val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container)
|
|
||||||
extraFieldValueContainer?.isPasswordVisibilityToggleEnabled = extraField.protectedValue.isProtected
|
|
||||||
extraFieldValueContainer?.hint = extraField.name
|
|
||||||
extraFieldValueContainer?.id = View.NO_ID
|
|
||||||
|
|
||||||
val extraFieldValue: TextInputEditText? = itemView?.findViewById(R.id.entry_extra_field_value)
|
|
||||||
extraFieldValue?.apply {
|
|
||||||
if (extraField.protectedValue.isProtected) {
|
|
||||||
inputType = extraFieldValue.inputType or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD
|
|
||||||
}
|
|
||||||
setText(extraField.protectedValue.toString())
|
|
||||||
if (fontInVisibility)
|
|
||||||
applyFontVisibility()
|
|
||||||
}
|
|
||||||
extraFieldValue?.id = View.NO_ID
|
|
||||||
extraFieldValue?.tag = "FIELD_VALUE_TAG"
|
|
||||||
if (mLastFocusedEditField?.field == extraField) {
|
|
||||||
mExtraViewToRequestFocus = extraFieldValue
|
|
||||||
}
|
|
||||||
|
|
||||||
val extraFieldEditButton: View? = itemView?.findViewById(R.id.entry_extra_field_edit)
|
|
||||||
extraFieldEditButton?.setOnClickListener {
|
|
||||||
mOnEditButtonClickListener?.invoke(extraField)
|
|
||||||
}
|
|
||||||
extraFieldEditButton?.id = View.NO_ID
|
|
||||||
|
|
||||||
return itemView
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getExtraFields(): List<Field> {
|
|
||||||
mLastFocusedEditField = null
|
|
||||||
for (index in 0 until extraFieldsListView.childCount) {
|
|
||||||
val extraFieldValue: EditText = extraFieldsListView.getChildAt(index)
|
|
||||||
.findViewWithTag("FIELD_VALUE_TAG")
|
|
||||||
val extraField = mExtraFieldsList[index]
|
|
||||||
extraField.protectedValue.stringValue = extraFieldValue.text?.toString() ?: ""
|
|
||||||
if (extraFieldValue.isFocused) {
|
|
||||||
mLastFocusedEditField = FocusedEditField().apply {
|
|
||||||
field = extraField
|
|
||||||
cursorSelectionStart = extraFieldValue.selectionStart
|
|
||||||
cursorSelectionEnd = extraFieldValue.selectionEnd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mExtraFieldsList
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all children and add new views for each field
|
|
||||||
*/
|
|
||||||
fun assignExtraFields(fields: List<Field>,
|
|
||||||
onEditButtonClickListener: ((item: Field)->Unit)?) {
|
|
||||||
extraFieldsContainerView.visibility = if (fields.isEmpty()) View.GONE else View.VISIBLE
|
|
||||||
// Reinit focused field
|
|
||||||
mExtraFieldsList.clear()
|
|
||||||
mExtraFieldsList.addAll(fields)
|
|
||||||
extraFieldsListView.removeAllViews()
|
|
||||||
fields.forEach {
|
|
||||||
extraFieldsListView.addView(buildViewFromField(it))
|
|
||||||
}
|
|
||||||
// Request last focus
|
|
||||||
mLastFocusedEditField?.let { focusField ->
|
|
||||||
mExtraViewToRequestFocus?.apply {
|
|
||||||
requestFocus()
|
|
||||||
setSelection(focusField.cursorSelectionStart,
|
|
||||||
focusField.cursorSelectionEnd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mLastFocusedEditField = null
|
|
||||||
mOnEditButtonClickListener = onEditButtonClickListener
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update an extra field or create a new one if doesn't exists, the old value is lost
|
|
||||||
*/
|
|
||||||
fun putExtraField(extraField: Field) {
|
|
||||||
extraFieldsContainerView.visibility = View.VISIBLE
|
|
||||||
val oldField = mExtraFieldsList.firstOrNull { it.name == extraField.name }
|
|
||||||
oldField?.let {
|
|
||||||
val index = mExtraFieldsList.indexOf(oldField)
|
|
||||||
mExtraFieldsList.removeAt(index)
|
|
||||||
mExtraFieldsList.add(index, extraField)
|
|
||||||
extraFieldsListView.removeViewAt(index)
|
|
||||||
val newView = buildViewFromField(extraField)
|
|
||||||
extraFieldsListView.addView(newView, index)
|
|
||||||
newView?.requestFocus()
|
|
||||||
} ?: kotlin.run {
|
|
||||||
mExtraFieldsList.add(extraField)
|
|
||||||
val newView = buildViewFromField(extraField)
|
|
||||||
extraFieldsListView.addView(newView)
|
|
||||||
newView?.requestFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update an extra field and keep the old value
|
|
||||||
*/
|
|
||||||
fun replaceExtraField(oldExtraField: Field, newExtraField: Field) {
|
|
||||||
extraFieldsContainerView.visibility = View.VISIBLE
|
|
||||||
val index = mExtraFieldsList.indexOf(oldExtraField)
|
|
||||||
val oldValueEditText: EditText = extraFieldsListView.getChildAt(index)
|
|
||||||
.findViewWithTag("FIELD_VALUE_TAG")
|
|
||||||
val oldValue = oldValueEditText.text.toString()
|
|
||||||
val newExtraFieldWithOldValue = Field(newExtraField).apply {
|
|
||||||
this.protectedValue.stringValue = oldValue
|
|
||||||
}
|
|
||||||
mExtraFieldsList.removeAt(index)
|
|
||||||
mExtraFieldsList.add(index, newExtraFieldWithOldValue)
|
|
||||||
extraFieldsListView.removeViewAt(index)
|
|
||||||
val newView = buildViewFromField(newExtraFieldWithOldValue)
|
|
||||||
extraFieldsListView.addView(newView, index)
|
|
||||||
newView?.requestFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeExtraField(oldExtraField: Field) {
|
|
||||||
val previousSize = mExtraFieldsList.size
|
|
||||||
val index = mExtraFieldsList.indexOf(oldExtraField)
|
|
||||||
extraFieldsListView.getChildAt(index)?.let {
|
|
||||||
it.collapse(true) {
|
|
||||||
mExtraFieldsList.removeAt(index)
|
|
||||||
extraFieldsListView.removeViewAt(index)
|
|
||||||
val newSize = mExtraFieldsList.size
|
|
||||||
|
|
||||||
if (previousSize > 0 && newSize == 0) {
|
|
||||||
extraFieldsContainerView.collapse(true)
|
|
||||||
} else if (previousSize == 0 && newSize == 1) {
|
|
||||||
extraFieldsContainerView.expand(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------
|
|
||||||
* Attachments
|
|
||||||
* -------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
fun getAttachments(): List<Attachment> {
|
|
||||||
return attachmentsAdapter.itemsList.map { it.attachment }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun assignAttachments(attachments: List<Attachment>,
|
|
||||||
streamDirection: StreamDirection,
|
|
||||||
onDeleteItem: (attachment: Attachment)->Unit) {
|
|
||||||
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
|
|
||||||
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
|
|
||||||
attachmentsAdapter.onDeleteButtonClickListener = { item ->
|
|
||||||
onDeleteItem.invoke(item.attachment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun containsAttachment(): Boolean {
|
|
||||||
return !attachmentsAdapter.isEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun containsAttachment(attachment: EntryAttachmentState): Boolean {
|
|
||||||
return attachmentsAdapter.contains(attachment)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun putAttachment(attachment: EntryAttachmentState) {
|
|
||||||
attachmentsContainerView.visibility = View.VISIBLE
|
|
||||||
attachmentsAdapter.putItem(attachment)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeAttachment(attachment: EntryAttachmentState) {
|
|
||||||
attachmentsAdapter.removeItem(attachment)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearAttachments() {
|
|
||||||
attachmentsAdapter.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAttachmentViewPosition(attachment: EntryAttachmentState, position: (Float) -> Unit) {
|
|
||||||
attachmentsListView.postDelayed({
|
|
||||||
position.invoke(attachmentsContainerView.y
|
|
||||||
+ attachmentsListView.y
|
|
||||||
+ (attachmentsListView.getChildAt(attachmentsAdapter.indexOf(attachment))?.y
|
|
||||||
?: 0F)
|
|
||||||
)
|
|
||||||
}, 250)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
populateEntryWithViews()
|
|
||||||
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
|
|
||||||
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
|
|
||||||
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
|
|
||||||
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
|
|
||||||
|
|
||||||
fun getInstance(entryInfo: EntryInfo?): EntryEditFragment {
|
|
||||||
return EntryEditFragment().apply {
|
|
||||||
arguments = Bundle().apply {
|
|
||||||
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -19,18 +19,16 @@
|
|||||||
*/
|
*/
|
||||||
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.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
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.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.MagikIME
|
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
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
@@ -39,152 +37,176 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
|||||||
* Activity to search or select entry in database,
|
* Activity to search or select entry in database,
|
||||||
* Commonly used with Magikeyboard
|
* Commonly used with Magikeyboard
|
||||||
*/
|
*/
|
||||||
class EntrySelectionLauncherActivity : AppCompatActivity() {
|
class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun applyCustomStyle(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
var sharedWebDomain: String? = null
|
override fun finishActivityIfReloadRequested(): Boolean {
|
||||||
var otpString: String? = null
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
when (intent?.action) {
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
Intent.ACTION_SEND -> {
|
super.onDatabaseRetrieved(database)
|
||||||
if ("text/plain" == intent.type) {
|
|
||||||
// Retrieve web domain or OTP
|
val keySelectionBundle = intent.getBundleExtra(KEY_SELECTION_BUNDLE)
|
||||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extra ->
|
if (keySelectionBundle != null) {
|
||||||
|
// To manage package name
|
||||||
|
var searchInfo = SearchInfo()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
Intent.ACTION_VIEW -> {
|
|
||||||
// Retrieve OTP
|
// Build domain search param
|
||||||
intent.dataString?.let { extra ->
|
val searchInfo = SearchInfo().apply {
|
||||||
if (OtpEntryFields.isOTPUri(extra))
|
this.webDomain = sharedWebDomain
|
||||||
otpString = extra
|
this.otpString = otpString
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||||
// Build domain search param
|
searchInfo.webDomain = concreteWebDomain
|
||||||
val searchInfo = SearchInfo().apply {
|
launch(database, searchInfo)
|
||||||
this.webDomain = sharedWebDomain
|
}
|
||||||
this.otpString = otpString
|
|
||||||
}
|
}
|
||||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
|
||||||
searchInfo.webDomain = concreteWebDomain
|
|
||||||
launch(searchInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launch(searchInfo: SearchInfo) {
|
private fun launch(database: Database?,
|
||||||
|
searchInfo: SearchInfo) {
|
||||||
|
|
||||||
if (!searchInfo.containsOnlyNullValues()) {
|
// Setting to integrate Magikeyboard
|
||||||
// Setting to integrate Magikeyboard
|
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
|
||||||
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
|
|
||||||
|
|
||||||
// If database is open
|
// If database is open
|
||||||
val database = Database.getInstance()
|
val readOnly = database?.isReadOnly != false
|
||||||
val readOnly = database.isReadOnly
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
database,
|
||||||
database,
|
searchInfo,
|
||||||
searchInfo,
|
{ openedDatabase, items ->
|
||||||
{ items ->
|
// Items found
|
||||||
// Items found
|
if (searchInfo.otpString != null) {
|
||||||
if (searchInfo.otpString != null) {
|
if (!readOnly) {
|
||||||
if (!readOnly) {
|
GroupActivity.launchForSaveResult(
|
||||||
GroupActivity.launchForSaveResult(this,
|
this,
|
||||||
searchInfo,
|
openedDatabase,
|
||||||
false)
|
searchInfo,
|
||||||
} else {
|
false)
|
||||||
Toast.makeText(applicationContext,
|
} else {
|
||||||
R.string.autofill_read_only_save,
|
Toast.makeText(applicationContext,
|
||||||
Toast.LENGTH_LONG)
|
R.string.autofill_read_only_save,
|
||||||
.show()
|
Toast.LENGTH_LONG)
|
||||||
}
|
.show()
|
||||||
} else if (searchShareForMagikeyboard) {
|
}
|
||||||
if (items.size == 1) {
|
} else if (searchShareForMagikeyboard) {
|
||||||
|
MagikeyboardService.performSelection(
|
||||||
|
items,
|
||||||
|
{ entryInfo ->
|
||||||
// Automatically populate keyboard
|
// Automatically populate keyboard
|
||||||
val entryPopulate = items[0]
|
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||||
populateKeyboardAndMoveAppToBackground(this,
|
this,
|
||||||
entryPopulate,
|
entryInfo
|
||||||
intent)
|
)
|
||||||
} else {
|
},
|
||||||
// Select the one we want
|
{ autoSearch ->
|
||||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||||
readOnly,
|
openedDatabase,
|
||||||
searchInfo,
|
|
||||||
true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
GroupActivity.launchForSearchResult(this,
|
|
||||||
readOnly,
|
|
||||||
searchInfo,
|
searchInfo,
|
||||||
true)
|
autoSearch)
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Show the database UI to select the entry
|
|
||||||
if (searchInfo.otpString != null) {
|
|
||||||
if (!readOnly) {
|
|
||||||
GroupActivity.launchForSaveResult(this,
|
|
||||||
searchInfo,
|
|
||||||
false)
|
|
||||||
} else {
|
|
||||||
Toast.makeText(applicationContext,
|
|
||||||
R.string.autofill_read_only_save,
|
|
||||||
Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}
|
}
|
||||||
} else if (readOnly || searchShareForMagikeyboard) {
|
)
|
||||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
} else {
|
||||||
readOnly,
|
GroupActivity.launchForSearchResult(this,
|
||||||
searchInfo,
|
openedDatabase,
|
||||||
false)
|
searchInfo,
|
||||||
} else {
|
true)
|
||||||
GroupActivity.launchForSaveResult(this,
|
|
||||||
searchInfo,
|
|
||||||
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 {
|
|
||||||
FileDatabaseSelectActivity.launchForSearchResult(this,
|
|
||||||
searchInfo)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
}
|
{ 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,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
companion object {
|
||||||
entry: EntryInfo,
|
|
||||||
intent: Intent,
|
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
|
||||||
toast: Boolean = true) {
|
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
||||||
// Populate Magikeyboard with entry
|
|
||||||
MagikIME.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast)
|
fun launch(context: Context,
|
||||||
// Consume the selection mode
|
searchInfo: SearchInfo? = null) {
|
||||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
val intent = Intent(context, EntrySelectionLauncherActivity::class.java).apply {
|
||||||
activity.moveTaskToBack(true)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,50 +31,57 @@ 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.SelectFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.utils.*
|
import com.kunzisoft.keepass.utils.*
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
||||||
import kotlinx.android.synthetic.main.activity_file_selection.*
|
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
class FileDatabaseSelectActivity : SpecialModeActivity(),
|
class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||||
AssignMasterKeyDialogFragment.AssignPasswordDialogListener {
|
SetMainCredentialDialogFragment.AssignMainCredentialDialogListener {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
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
|
||||||
|
|
||||||
@@ -82,13 +89,22 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
|
|
||||||
private var mDatabaseFileUri: Uri? = null
|
private var mDatabaseFileUri: Uri? = null
|
||||||
|
|
||||||
private var mSelectFileHelper: SelectFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
private var 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)
|
||||||
@@ -98,19 +114,33 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
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
|
||||||
mSelectFileHelper = SelectFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
openDatabaseButtonView?.apply {
|
uri?.let {
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
launchPasswordActivityWithPath(uri)
|
||||||
setOnClickListener(it)
|
|
||||||
setOnLongClickListener(it)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
|
|
||||||
// History list
|
// History list
|
||||||
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
val fileDatabaseHistoryRecyclerView = findViewById<RecyclerView>(R.id.file_list)
|
||||||
@@ -131,7 +161,6 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete ->
|
||||||
// Remove from app database
|
|
||||||
databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete)
|
databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -162,29 +191,31 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
|
|
||||||
// Observe list of databases
|
// Observe list of databases
|
||||||
databaseFilesViewModel.databaseFilesLoaded.observe(this) { databaseFiles ->
|
databaseFilesViewModel.databaseFilesLoaded.observe(this) { databaseFiles ->
|
||||||
when (databaseFiles.databaseFileAction) {
|
try {
|
||||||
DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
|
when (databaseFiles.databaseFileAction) {
|
||||||
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
|
DatabaseFilesViewModel.DatabaseFileAction.NONE -> {
|
||||||
}
|
mAdapterDatabaseHistory?.replaceAllDatabaseFileHistoryList(databaseFiles.databaseFileList)
|
||||||
DatabaseFilesViewModel.DatabaseFileAction.ADD -> {
|
|
||||||
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
|
|
||||||
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
|
|
||||||
}
|
}
|
||||||
GroupActivity.launch(this@FileDatabaseSelectActivity,
|
DatabaseFilesViewModel.DatabaseFileAction.ADD -> {
|
||||||
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity))
|
databaseFiles.databaseFileToActivate?.let { databaseFileToAdd ->
|
||||||
}
|
mAdapterDatabaseHistory?.addDatabaseFileHistory(databaseFileToAdd)
|
||||||
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
|
}
|
||||||
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
|
}
|
||||||
mAdapterDatabaseHistory?.updateDatabaseFileHistory(databaseFileToUpdate)
|
DatabaseFilesViewModel.DatabaseFileAction.UPDATE -> {
|
||||||
}
|
databaseFiles.databaseFileToActivate?.let { databaseFileToUpdate ->
|
||||||
}
|
mAdapterDatabaseHistory?.updateDatabaseFileHistory(databaseFileToUpdate)
|
||||||
DatabaseFilesViewModel.DatabaseFileAction.DELETE -> {
|
}
|
||||||
databaseFiles.databaseFileToActivate?.let { databaseFileToDelete ->
|
}
|
||||||
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileToDelete)
|
DatabaseFilesViewModel.DatabaseFileAction.DELETE -> {
|
||||||
|
databaseFiles.databaseFileToActivate?.let { databaseFileToDelete ->
|
||||||
|
mAdapterDatabaseHistory?.deleteDatabaseFileHistory(databaseFileToDelete)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
databaseFilesViewModel.consumeAction()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to observe database action", e)
|
||||||
}
|
}
|
||||||
databaseFilesViewModel.consumeAction()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Observe default database
|
// Observe default database
|
||||||
@@ -192,37 +223,62 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
// Retrieve settings for default database
|
// Retrieve settings for default database
|
||||||
mAdapterDatabaseHistory?.setDefaultDatabase(it)
|
mAdapterDatabaseHistory?.setDefaultDatabase(it)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Attach the dialog thread to this activity
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
|
super.onDatabaseRetrieved(database)
|
||||||
onActionFinish = { actionTask, result ->
|
if (database != null) {
|
||||||
when (actionTask) {
|
launchGroupActivityIfLoaded(database)
|
||||||
ACTION_DATABASE_CREATE_TASK -> {
|
}
|
||||||
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
|
}
|
||||||
val keyFileUri = result.data?.getParcelable<Uri?>(KEY_FILE_URI_KEY)
|
|
||||||
databaseFilesViewModel.addDatabaseFile(databaseUri, keyFileUri)
|
override fun onDatabaseActionFinished(
|
||||||
}
|
database: Database,
|
||||||
}
|
actionTask: String,
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
result: ActionRunnable.Result
|
||||||
val database = Database.getInstance()
|
) {
|
||||||
if (result.isSuccess
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
&& database.loaded) {
|
|
||||||
launchGroupActivity(database)
|
if (result.isSuccess) {
|
||||||
} else {
|
// Update list
|
||||||
var resultError = ""
|
when (actionTask) {
|
||||||
val resultMessage = result.message
|
ACTION_DATABASE_CREATE_TASK,
|
||||||
// Show error message
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
|
||||||
resultError = "$resultError $resultMessage"
|
val mainCredential =
|
||||||
}
|
result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY)
|
||||||
Log.e(TAG, resultError)
|
?: MainCredential()
|
||||||
Snackbar.make(activity_file_selection_coordinator_layout,
|
databaseFilesViewModel.addDatabaseFile(
|
||||||
resultError,
|
databaseUri,
|
||||||
Snackbar.LENGTH_LONG).asError().show()
|
mainCredential.keyFileUri
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Launch activity
|
||||||
|
when (actionTask) {
|
||||||
|
ACTION_DATABASE_CREATE_TASK -> {
|
||||||
|
GroupActivity.launch(
|
||||||
|
this@FileDatabaseSelectActivity,
|
||||||
|
database,
|
||||||
|
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
|
launchGroupActivityIfLoaded(database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var resultError = ""
|
||||||
|
val resultMessage = result.message
|
||||||
|
// Show error message
|
||||||
|
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
||||||
|
resultError = "$resultError $resultMessage"
|
||||||
|
}
|
||||||
|
Log.e(TAG, resultError)
|
||||||
|
Snackbar.make(coordinatorLayout,
|
||||||
|
resultError,
|
||||||
|
Snackbar.LENGTH_LONG).asError().show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,35 +286,38 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
* Create a new file by calling the content provider
|
* Create a new file by calling the content provider
|
||||||
*/
|
*/
|
||||||
private fun createNewFile() {
|
private fun createNewFile() {
|
||||||
createDocument(this, getString(R.string.database_file_name_default) +
|
mExternalFileHelper?.createDocument(
|
||||||
getString(R.string.database_file_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) {
|
||||||
val error = getString(R.string.file_not_found_content)
|
val error = getString(R.string.file_not_found_content)
|
||||||
Log.e(TAG, error, e)
|
Log.e(TAG, error, e)
|
||||||
coordinatorLayout?.let {
|
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
Snackbar.make(it, error, Snackbar.LENGTH_LONG).asError().show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
||||||
PasswordActivity.launch(this,
|
MainCredentialActivity.launch(this,
|
||||||
databaseUri,
|
databaseUri,
|
||||||
keyFile,
|
keyFile,
|
||||||
{ exception ->
|
{ exception ->
|
||||||
fileNoFoundAction(exception)
|
fileNoFoundAction(exception)
|
||||||
},
|
},
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() })
|
{ onLaunchActivitySpecialMode() },
|
||||||
|
mAutofillActivityResultLauncher)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchGroupActivity(database: Database) {
|
private fun launchGroupActivityIfLoaded(database: Database) {
|
||||||
GroupActivity.launch(this,
|
if (database.loaded) {
|
||||||
database.isReadOnly,
|
GroupActivity.launch(this,
|
||||||
|
database,
|
||||||
{ onValidateSpecialMode() },
|
{ onValidateSpecialMode() },
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() })
|
{ onLaunchActivitySpecialMode() },
|
||||||
|
mAutofillActivityResultLauncher)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onValidateSpecialMode() {
|
override fun onValidateSpecialMode() {
|
||||||
@@ -281,10 +340,13 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
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 -> {
|
||||||
if (allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
if (ExternalFileHelper.allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
||||||
// There is an activity which can handle this intent.
|
// There is an activity which can handle this intent.
|
||||||
createDatabaseButtonView?.visibility = View.VISIBLE
|
createDatabaseButtonView?.visibility = View.VISIBLE
|
||||||
} else{
|
} else{
|
||||||
@@ -298,28 +360,16 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val database = Database.getInstance()
|
mDatabase?.let { database ->
|
||||||
if (database.loaded) {
|
launchGroupActivityIfLoaded(database)
|
||||||
launchGroupActivity(database)
|
|
||||||
} else {
|
|
||||||
// Construct adapter with listeners
|
|
||||||
if (PreferencesUtil.showRecentFiles(this)) {
|
|
||||||
databaseFilesViewModel.loadListOfDatabases()
|
|
||||||
} else {
|
|
||||||
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
|
||||||
mAdapterDatabaseHistory?.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register progress task
|
|
||||||
mProgressDatabaseTaskProvider?.registerProgressTask()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
// Show recent files if allowed
|
||||||
// Unregister progress task
|
if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) {
|
||||||
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
databaseFilesViewModel.loadListOfDatabases()
|
||||||
|
} else {
|
||||||
super.onPause()
|
mAdapterDatabaseHistory?.clearDatabaseFileHistoryList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
@@ -330,103 +380,62 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAssignKeyDialogPositiveClick(
|
override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) {
|
||||||
masterPasswordChecked: Boolean, masterPassword: String?,
|
|
||||||
keyFileChecked: Boolean, keyFile: Uri?) {
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mDatabaseFileUri?.let { databaseUri ->
|
mDatabaseFileUri?.let { databaseUri ->
|
||||||
|
|
||||||
// Create the new database
|
// Create the new database
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseCreate(
|
createDatabase(databaseUri, mainCredential)
|
||||||
databaseUri,
|
|
||||||
masterPasswordChecked,
|
|
||||||
masterPassword,
|
|
||||||
keyFileChecked,
|
|
||||||
keyFile
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val error = getString(R.string.error_create_database_file)
|
val error = getString(R.string.error_create_database_file)
|
||||||
Snackbar.make(activity_file_selection_coordinator_layout, error, Snackbar.LENGTH_LONG).asError().show()
|
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
Log.e(TAG, error, e)
|
Log.e(TAG, error, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAssignKeyDialogNegativeClick(
|
override fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential) {}
|
||||||
masterPasswordChecked: Boolean, masterPassword: String?,
|
|
||||||
keyFileChecked: Boolean, keyFile: Uri?) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
|
||||||
if (uri != null) {
|
|
||||||
launchPasswordActivityWithPath(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the created URI from the file manager
|
|
||||||
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)
|
|
||||||
coordinatorLayout?.let {
|
|
||||||
Snackbar.make(it, 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!!.visibility == View.VISIBLE
|
createDatabaseButtonView != null
|
||||||
&& mAdapterDatabaseHistory != null
|
&& createDatabaseButtonView!!.visibility == View.VISIBLE
|
||||||
&& mAdapterDatabaseHistory!!.itemCount > 0
|
&& mFileDatabaseSelectActivityEducation.checkAndPerformedCreateDatabaseEducation(
|
||||||
&& 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 {
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
|
mExternalFileHelper?.openDocument()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{}
|
{
|
||||||
)
|
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -500,11 +509,13 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,356 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.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.google.android.material.snackbar.Snackbar
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.IconEditDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.view.asError
|
||||||
|
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
||||||
|
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
|
|
||||||
|
class IconPickerActivity : DatabaseLockActivity() {
|
||||||
|
|
||||||
|
private lateinit var toolbar: Toolbar
|
||||||
|
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||||
|
private lateinit var uploadButton: View
|
||||||
|
private var lockView: View? = null
|
||||||
|
|
||||||
|
private var mIconImage: IconImage = IconImage()
|
||||||
|
|
||||||
|
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
|
private val iconPickerViewModel: IconPickerViewModel by viewModels()
|
||||||
|
private var mCustomIconsSelectionMode = false
|
||||||
|
private var mIconsSelected: List<IconImageCustom> = ArrayList()
|
||||||
|
|
||||||
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_icon_picker)
|
||||||
|
|
||||||
|
toolbar = findViewById(R.id.toolbar)
|
||||||
|
toolbar.title = " "
|
||||||
|
setSupportActionBar(toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
updateIconsSelectedViews()
|
||||||
|
|
||||||
|
coordinatorLayout = findViewById(R.id.icon_picker_coordinator)
|
||||||
|
|
||||||
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
|
addCustomIcon(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadButton = findViewById(R.id.icon_picker_upload)
|
||||||
|
|
||||||
|
lockView = findViewById(R.id.lock_button)
|
||||||
|
lockView?.setOnClickListener {
|
||||||
|
lockAndExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
intent?.getParcelableExtra<IconImage>(EXTRA_ICON)?.let {
|
||||||
|
mIconImage = it
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
supportFragmentManager.commit {
|
||||||
|
setReorderingAllowed(true)
|
||||||
|
add(R.id.icon_picker_fragment, IconPickerFragment.getInstance(
|
||||||
|
// Default selection tab
|
||||||
|
if (mIconImage.custom.isUnknown)
|
||||||
|
IconPickerFragment.IconTab.STANDARD
|
||||||
|
else
|
||||||
|
IconPickerFragment.IconTab.CUSTOM
|
||||||
|
), ICON_PICKER_FRAGMENT_TAG)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mIconImage = savedInstanceState.getParcelable(EXTRA_ICON) ?: mIconImage
|
||||||
|
}
|
||||||
|
|
||||||
|
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
|
||||||
|
mIconImage.standard = iconStandard
|
||||||
|
// Remove the custom icon if a standard one is selected
|
||||||
|
mIconImage.custom = IconImageCustom()
|
||||||
|
setResult()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
iconPickerViewModel.customIconPicked.observe(this) { iconCustom ->
|
||||||
|
// Keep the standard icon if a custom one is selected
|
||||||
|
mIconImage.custom = iconCustom
|
||||||
|
setResult()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
iconPickerViewModel.customIconsSelected.observe(this) { iconsSelected ->
|
||||||
|
mIconsSelected = iconsSelected
|
||||||
|
updateIconsSelectedViews()
|
||||||
|
}
|
||||||
|
iconPickerViewModel.customIconAdded.observe(this) { iconCustomAdded ->
|
||||||
|
if (iconCustomAdded.error && !iconCustomAdded.errorConsumed) {
|
||||||
|
Snackbar.make(coordinatorLayout, iconCustomAdded.errorStringId, Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
iconCustomAdded.errorConsumed = true
|
||||||
|
}
|
||||||
|
uploadButton.isEnabled = true
|
||||||
|
}
|
||||||
|
iconPickerViewModel.customIconRemoved.observe(this) { iconCustomRemoved ->
|
||||||
|
if (iconCustomRemoved.error && !iconCustomRemoved.errorConsumed) {
|
||||||
|
Snackbar.make(coordinatorLayout, iconCustomRemoved.errorStringId, Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
iconCustomRemoved.errorConsumed = true
|
||||||
|
}
|
||||||
|
uploadButton.isEnabled = true
|
||||||
|
}
|
||||||
|
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? {
|
||||||
|
return findViewById<ViewGroup>(R.id.icon_picker_container)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finishActivityIfReloadRequested(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
|
if (database?.allowCustomIcons == true) {
|
||||||
|
uploadButton.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
|
} else {
|
||||||
|
uploadButton.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateIconsSelectedViews() {
|
||||||
|
if (mIconsSelected.isEmpty()) {
|
||||||
|
mCustomIconsSelectionMode = false
|
||||||
|
toolbar.title = " "
|
||||||
|
} else {
|
||||||
|
mCustomIconsSelectionMode = true
|
||||||
|
toolbar.title = mIconsSelected.size.toString()
|
||||||
|
}
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
|
||||||
|
outState.putParcelable(EXTRA_ICON, mIconImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// Show the lock button
|
||||||
|
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Padding if lock button visible
|
||||||
|
toolbar.updateLockPaddingLeft()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
|
super.onCreateOptionsMenu(menu)
|
||||||
|
menuInflater.inflate(R.menu.icon, menu)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
isEnabled = mCustomIconsSelectionMode
|
||||||
|
isVisible = isEnabled
|
||||||
|
}
|
||||||
|
return super.onPrepareOptionsMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> {
|
||||||
|
if (mCustomIconsSelectionMode) {
|
||||||
|
iconPickerViewModel.deselectAllCustomIcons()
|
||||||
|
} else {
|
||||||
|
onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
R.id.menu_edit -> {
|
||||||
|
updateCustomIcon(mIconsSelected[0])
|
||||||
|
}
|
||||||
|
R.id.menu_delete -> {
|
||||||
|
mIconsSelected.forEach { iconToRemove ->
|
||||||
|
removeCustomIcon(iconToRemove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
R.id.menu_external_icon -> {
|
||||||
|
UriUtil.gotoUrl(this, R.string.external_icon_url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addCustomIcon(iconToUploadUri: Uri?) {
|
||||||
|
uploadButton.isEnabled = false
|
||||||
|
mainScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
// on Progress with thread
|
||||||
|
val asyncResult: Deferred<IconPickerViewModel.IconCustomState?> = async {
|
||||||
|
val iconCustomState = IconPickerViewModel.IconCustomState(null, true, R.string.error_upload_file)
|
||||||
|
UriUtil.getFileData(this@IconPickerActivity, iconToUploadUri)?.also { documentFile ->
|
||||||
|
if (documentFile.length() > MAX_ICON_SIZE) {
|
||||||
|
iconCustomState.errorStringId = R.string.error_file_to_big
|
||||||
|
} else {
|
||||||
|
mDatabase?.buildNewCustomIcon { customIcon, binary ->
|
||||||
|
if (customIcon != null) {
|
||||||
|
iconCustomState.iconCustom = customIcon
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
BinaryDatabaseManager.resizeBitmapAndStoreDataInBinaryFile(
|
||||||
|
contentResolver,
|
||||||
|
database,
|
||||||
|
iconToUploadUri,
|
||||||
|
binary)
|
||||||
|
when {
|
||||||
|
binary == null -> {
|
||||||
|
}
|
||||||
|
binary.getSize() <= 0 -> {
|
||||||
|
}
|
||||||
|
database.isCustomIconBinaryDuplicate(binary) -> {
|
||||||
|
iconCustomState.errorStringId = R.string.error_duplicate_file
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
iconCustomState.error = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (iconCustomState.error) {
|
||||||
|
mDatabase?.removeCustomIcon(customIcon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iconCustomState
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
asyncResult.await()?.let { customIcon ->
|
||||||
|
iconPickerViewModel.addCustomIcon(customIcon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateCustomIcon(iconImageCustom: IconImageCustom) {
|
||||||
|
IconEditDialogFragment.update(iconImageCustom)
|
||||||
|
.show(supportFragmentManager, IconEditDialogFragment.TAG_UPDATE_ICON)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeCustomIcon(iconImageCustom: IconImageCustom) {
|
||||||
|
uploadButton.isEnabled = false
|
||||||
|
iconPickerViewModel.deselectAllCustomIcons()
|
||||||
|
mDatabase?.removeCustomIcon(iconImageCustom)
|
||||||
|
iconPickerViewModel.removeCustomIcon(
|
||||||
|
IconPickerViewModel.IconCustomState(iconImageCustom, false, R.string.error_remove_file)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setResult() {
|
||||||
|
setResult(Activity.RESULT_OK, Intent().apply {
|
||||||
|
putExtra(EXTRA_ICON, mIconImage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
setResult()
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val ICON_PICKER_FRAGMENT_TAG = "ICON_PICKER_FRAGMENT_TAG"
|
||||||
|
private const val EXTRA_ICON = "EXTRA_ICON"
|
||||||
|
private const val MAX_ICON_SIZE = 5242880
|
||||||
|
|
||||||
|
fun registerIconSelectionForResult(context: FragmentActivity,
|
||||||
|
listener: (icon: IconImage) -> Unit): ActivityResultLauncher<Intent> {
|
||||||
|
return context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
listener.invoke(result.data?.getParcelableExtra(EXTRA_ICON) ?: IconImage())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launch(context: FragmentActivity,
|
||||||
|
previousIcon: IconImage?,
|
||||||
|
resultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
|
// Create an instance to return the picker icon
|
||||||
|
resultLauncher.launch(
|
||||||
|
Intent(context, IconPickerActivity::class.java).apply {
|
||||||
|
if (previousIcon != null)
|
||||||
|
putExtra(EXTRA_ICON, previousIcon)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.format.Formatter
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import com.igreenwood.loupe.Loupe
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
class ImageViewerActivity : DatabaseLockActivity() {
|
||||||
|
|
||||||
|
private var imageContainerView: ViewGroup? = null
|
||||||
|
private lateinit var imageView: ImageView
|
||||||
|
private lateinit var progressView: View
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_image_viewer)
|
||||||
|
|
||||||
|
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||||
|
setSupportActionBar(toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
toolbar.setOnTouchListener { _, _ ->
|
||||||
|
resetAppTimeout()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
imageContainerView = findViewById(R.id.image_viewer_container)
|
||||||
|
imageView = findViewById(R.id.image_viewer_image)
|
||||||
|
progressView = findViewById(R.id.image_viewer_progress)
|
||||||
|
|
||||||
|
Loupe.create(imageView, imageContainerView!!) {
|
||||||
|
onViewTouchedListener = View.OnTouchListener { _, _ ->
|
||||||
|
// to reset timeout when Loupe image view touched
|
||||||
|
resetAppTimeout()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
onViewTranslateListener = object : Loupe.OnViewTranslateListener {
|
||||||
|
|
||||||
|
override fun onStart(view: ImageView) {
|
||||||
|
// called when the view starts moving
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewTranslate(view: ImageView, amount: Float) {
|
||||||
|
// called whenever the view position changed
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRestore(view: ImageView) {
|
||||||
|
// called when the view drag gesture ended
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDismiss(view: ImageView) {
|
||||||
|
// called when the view drag gesture ended
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun viewToInvalidateTimeout(): View? {
|
||||||
|
// Null to manually manage events
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finishActivityIfReloadRequested(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
|
try {
|
||||||
|
progressView.visibility = View.VISIBLE
|
||||||
|
intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
|
||||||
|
|
||||||
|
supportActionBar?.title = attachment.name
|
||||||
|
|
||||||
|
val size = attachment.binaryData.getSize()
|
||||||
|
supportActionBar?.subtitle = Formatter.formatFileSize(this, size)
|
||||||
|
|
||||||
|
// Approximately, to not OOM and allow a zoom
|
||||||
|
val mImagePreviewMaxWidth = max(
|
||||||
|
resources.displayMetrics.widthPixels * 2,
|
||||||
|
resources.displayMetrics.heightPixels * 2
|
||||||
|
)
|
||||||
|
|
||||||
|
database?.let { database ->
|
||||||
|
BinaryDatabaseManager.loadBitmap(
|
||||||
|
database,
|
||||||
|
attachment.binaryData,
|
||||||
|
mImagePreviewMaxWidth
|
||||||
|
) { bitmapLoaded ->
|
||||||
|
if (bitmapLoaded == null) {
|
||||||
|
finish()
|
||||||
|
} else {
|
||||||
|
progressView.visibility = View.GONE
|
||||||
|
imageView.setImageBitmap(bitmapLoaded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: finish()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to view the binary", e)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> finish()
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val TAG = ImageViewerActivity::class.simpleName
|
||||||
|
|
||||||
|
private const val IMAGE_ATTACHMENT_TAG = "IMAGE_ATTACHMENT_TAG"
|
||||||
|
|
||||||
|
fun getInstance(context: Context, imageAttachment: Attachment) {
|
||||||
|
context.startActivity(Intent(context, ImageViewerActivity::class.java).apply {
|
||||||
|
putExtra(IMAGE_ATTACHMENT_TAG, imageAttachment)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,54 +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 android.os.Bundle
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Activity to select entry in database and populate it in Magikeyboard
|
|
||||||
*/
|
|
||||||
class MagikeyboardLauncherActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
val database = Database.getInstance()
|
|
||||||
val readOnly = database.isReadOnly
|
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
|
||||||
database,
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
// Not called
|
|
||||||
// if items found directly returns before calling this activity
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Select if not found
|
|
||||||
GroupActivity.launchForKeyboardSelectionResult(this, readOnly)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Pass extra to get entry
|
|
||||||
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
finish()
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,858 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.fragment.app.commit
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||||
|
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.DatabaseModeActivity
|
||||||
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
|
||||||
|
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||||
|
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||||
|
import com.kunzisoft.keepass.model.*
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
|
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.MAIN_CREDENTIAL_KEY
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||||
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import com.kunzisoft.keepass.view.MainCredentialView
|
||||||
|
import com.kunzisoft.keepass.view.asError
|
||||||
|
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
|
|
||||||
|
class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
||||||
|
|
||||||
|
// Views
|
||||||
|
private var toolbar: Toolbar? = null
|
||||||
|
private var filenameView: TextView? = null
|
||||||
|
private var advancedUnlockButton: View? = null
|
||||||
|
private var mainCredentialView: MainCredentialView? = null
|
||||||
|
private var confirmButtonView: Button? = null
|
||||||
|
private var infoContainerView: ViewGroup? = null
|
||||||
|
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||||
|
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
|
||||||
|
|
||||||
|
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||||
|
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by viewModels()
|
||||||
|
|
||||||
|
private val mPasswordActivityEducation = PasswordActivityEducation(this)
|
||||||
|
|
||||||
|
private var mDefaultDatabase: Boolean = false
|
||||||
|
private var mDatabaseFileUri: Uri? = null
|
||||||
|
|
||||||
|
private var mRememberKeyFile: Boolean = false
|
||||||
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
|
private var mReadOnly: Boolean = false
|
||||||
|
private var mForceReadOnly: Boolean = false
|
||||||
|
|
||||||
|
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||||
|
AutofillHelper.buildActivityResultLauncher(this)
|
||||||
|
else null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_main_credential)
|
||||||
|
|
||||||
|
toolbar = findViewById(R.id.toolbar)
|
||||||
|
toolbar?.title = getString(R.string.app_name)
|
||||||
|
setSupportActionBar(toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
|
filenameView = findViewById(R.id.filename)
|
||||||
|
advancedUnlockButton = findViewById(R.id.activity_password_advanced_unlock_button)
|
||||||
|
mainCredentialView = findViewById(R.id.activity_password_credentials)
|
||||||
|
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
||||||
|
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||||
|
coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout)
|
||||||
|
|
||||||
|
mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) {
|
||||||
|
savedInstanceState.getBoolean(KEY_READ_ONLY)
|
||||||
|
} else {
|
||||||
|
PreferencesUtil.enableReadOnlyDatabase(this)
|
||||||
|
}
|
||||||
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||||
|
|
||||||
|
mExternalFileHelper = ExternalFileHelper(this@MainCredentialActivity)
|
||||||
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
|
if (uri != null) {
|
||||||
|
mainCredentialView?.populateKeyFileTextView(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
|
||||||
|
mainCredentialView?.onValidateListener = {
|
||||||
|
loadDatabase()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If is a view intent
|
||||||
|
getUriFromIntent(intent)
|
||||||
|
|
||||||
|
// Init Biometric elements
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
advancedUnlockButton?.setOnClickListener {
|
||||||
|
startActivity(Intent(this, SettingsAdvancedUnlockActivity::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
advancedUnlockFragment = supportFragmentManager
|
||||||
|
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
|
||||||
|
if (advancedUnlockFragment == null) {
|
||||||
|
advancedUnlockFragment = AdvancedUnlockFragment()
|
||||||
|
supportFragmentManager.commit {
|
||||||
|
replace(R.id.fragment_advanced_unlock_container_view,
|
||||||
|
advancedUnlockFragment!!,
|
||||||
|
UNLOCK_FRAGMENT_TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen password checkbox to init advanced unlock and confirmation button
|
||||||
|
mainCredentialView?.onPasswordChecked =
|
||||||
|
CompoundButton.OnCheckedChangeListener { _, _ ->
|
||||||
|
mAdvancedUnlockViewModel.checkUnlockAvailability()
|
||||||
|
enableConfirmationButton()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observe if default database
|
||||||
|
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
||||||
|
mDefaultDatabase = isDefaultDatabase
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observe database file change
|
||||||
|
mDatabaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
|
||||||
|
|
||||||
|
// Force read only if the file does not exists
|
||||||
|
val databaseFileNotExists = databaseFile?.let {
|
||||||
|
!it.databaseFileExists
|
||||||
|
} ?: true
|
||||||
|
infoContainerView?.visibility = if (databaseFileNotExists) {
|
||||||
|
mReadOnly = true
|
||||||
|
View.VISIBLE
|
||||||
|
} else {
|
||||||
|
View.GONE
|
||||||
|
}
|
||||||
|
mForceReadOnly = databaseFileNotExists
|
||||||
|
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
|
||||||
|
// Post init uri with KeyFile only if needed
|
||||||
|
val databaseKeyFileUri = mainCredentialView?.getMainCredential()?.keyFileUri
|
||||||
|
val keyFileUri =
|
||||||
|
if (mRememberKeyFile
|
||||||
|
&& (databaseKeyFileUri == null || databaseKeyFileUri.toString().isEmpty())) {
|
||||||
|
databaseFile?.keyFileUri
|
||||||
|
} else {
|
||||||
|
databaseKeyFileUri
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define title
|
||||||
|
filenameView?.text = databaseFile?.databaseAlias ?: ""
|
||||||
|
|
||||||
|
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity)
|
||||||
|
|
||||||
|
// Back to previous keyboard is setting activated
|
||||||
|
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@MainCredentialActivity)) {
|
||||||
|
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow auto open prompt if lock become when UI visible
|
||||||
|
if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true) {
|
||||||
|
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseFileUri?.let { databaseFileUri ->
|
||||||
|
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
launchGroupActivityIfLoaded(database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseActionFinished(
|
||||||
|
database: Database,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
|
when (actionTask) {
|
||||||
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
|
// Recheck advanced unlock if error
|
||||||
|
mAdvancedUnlockViewModel.initAdvancedUnlockMode()
|
||||||
|
|
||||||
|
if (result.isSuccess) {
|
||||||
|
launchGroupActivityIfLoaded(database)
|
||||||
|
} else {
|
||||||
|
mainCredentialView?.requestPasswordFocus()
|
||||||
|
|
||||||
|
var resultError = ""
|
||||||
|
val resultException = result.exception
|
||||||
|
val resultMessage = result.message
|
||||||
|
|
||||||
|
if (resultException != null) {
|
||||||
|
resultError = resultException.getLocalizedMessage(resources)
|
||||||
|
|
||||||
|
when (resultException) {
|
||||||
|
is DuplicateUuidDatabaseException -> {
|
||||||
|
// Relaunch loading if we need to fix UUID
|
||||||
|
showLoadDatabaseDuplicateUuidMessage {
|
||||||
|
|
||||||
|
var databaseUri: Uri? = null
|
||||||
|
var mainCredential = MainCredential()
|
||||||
|
var readOnly = true
|
||||||
|
var cipherEncryptDatabase: CipherEncryptDatabase? = null
|
||||||
|
|
||||||
|
result.data?.let { resultData ->
|
||||||
|
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
||||||
|
mainCredential =
|
||||||
|
resultData.getParcelable(MAIN_CREDENTIAL_KEY)
|
||||||
|
?: mainCredential
|
||||||
|
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
||||||
|
cipherEncryptDatabase =
|
||||||
|
resultData.getParcelable(CIPHER_DATABASE_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
databaseUri?.let { databaseFileUri ->
|
||||||
|
showProgressDialogAndLoadDatabase(
|
||||||
|
databaseFileUri,
|
||||||
|
mainCredential,
|
||||||
|
readOnly,
|
||||||
|
cipherEncryptDatabase,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is FileNotFoundDatabaseException -> {
|
||||||
|
// Remove this default database inaccessible
|
||||||
|
if (mDefaultDatabase) {
|
||||||
|
mDatabaseFileViewModel.removeDefaultDatabase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
||||||
|
resultError = "$resultError $resultMessage"
|
||||||
|
}
|
||||||
|
Log.e(TAG, resultError)
|
||||||
|
Snackbar.make(
|
||||||
|
coordinatorLayout,
|
||||||
|
resultError,
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).asError().show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUriFromIntent(intent: Intent?) {
|
||||||
|
// If is a view intent
|
||||||
|
val action = intent?.action
|
||||||
|
if (action != null
|
||||||
|
&& action == VIEW_INTENT) {
|
||||||
|
mDatabaseFileUri = intent.data
|
||||||
|
mainCredentialView?.populateKeyFileTextView(UriUtil.getUriFromIntent(intent, KEY_KEYFILE))
|
||||||
|
} else {
|
||||||
|
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
|
||||||
|
intent?.getParcelableExtra<Uri?>(KEY_KEYFILE)?.let {
|
||||||
|
mainCredentialView?.populateKeyFileTextView(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
intent?.removeExtra(KEY_KEYFILE)
|
||||||
|
} catch (e: Exception) {}
|
||||||
|
mDatabaseFileUri?.let {
|
||||||
|
mDatabaseFileViewModel.checkIfIsDefaultDatabase(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
getUriFromIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchGroupActivityIfLoaded(database: Database) {
|
||||||
|
// Check if database really loaded
|
||||||
|
if (database.loaded) {
|
||||||
|
clearCredentialsViews(true)
|
||||||
|
GroupActivity.launch(this,
|
||||||
|
database,
|
||||||
|
{ onValidateSpecialMode() },
|
||||||
|
{ onCancelSpecialMode() },
|
||||||
|
{ onLaunchActivitySpecialMode() },
|
||||||
|
mAutofillActivityResultLauncher
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onValidateSpecialMode() {
|
||||||
|
super.onValidateSpecialMode()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCancelSpecialMode() {
|
||||||
|
super.onCancelSpecialMode()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun retrieveCredentialForEncryption(): ByteArray {
|
||||||
|
return mainCredentialView?.retrieveCredentialForStorage(credentialStorageListener)
|
||||||
|
?: byteArrayOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun conditionToStoreCredential(): Boolean {
|
||||||
|
return mainCredentialView?.conditionToStoreCredential() == true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) {
|
||||||
|
// Load the database if password is registered with biometric
|
||||||
|
loadDatabase(mDatabaseFileUri,
|
||||||
|
mainCredentialView?.getMainCredential(),
|
||||||
|
cipherEncryptDatabase
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val credentialStorageListener = object: MainCredentialView.CredentialStorageListener {
|
||||||
|
override fun passwordToStore(password: String?): ByteArray? {
|
||||||
|
return password?.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
|
||||||
|
// Load the database if password is retrieve from biometric
|
||||||
|
// Retrieve from biometric
|
||||||
|
val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadDatabase(mDatabaseFileUri,
|
||||||
|
mainCredential,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
||||||
|
// Define Key File text
|
||||||
|
if (mRememberKeyFile) {
|
||||||
|
mainCredentialView?.populateKeyFileTextView(keyFileUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define listener for validate button
|
||||||
|
confirmButtonView?.setOnClickListener { loadDatabase() }
|
||||||
|
|
||||||
|
// If Activity is launch with a password and want to open directly
|
||||||
|
val intent = intent
|
||||||
|
val password = intent.getStringExtra(KEY_PASSWORD)
|
||||||
|
// Consume the intent extra password
|
||||||
|
intent.removeExtra(KEY_PASSWORD)
|
||||||
|
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
|
||||||
|
if (password != null) {
|
||||||
|
mainCredentialView?.populatePasswordTextView(password)
|
||||||
|
}
|
||||||
|
if (launchImmediately) {
|
||||||
|
loadDatabase()
|
||||||
|
} else {
|
||||||
|
// Init Biometric elements
|
||||||
|
mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
enableConfirmationButton()
|
||||||
|
|
||||||
|
mainCredentialView?.focusPasswordFieldAndOpenKeyboard()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun enableConfirmationButton() {
|
||||||
|
// Enable or not the open button if setting is checked
|
||||||
|
if (!PreferencesUtil.emptyPasswordAllowed(this@MainCredentialActivity)) {
|
||||||
|
confirmButtonView?.isEnabled = mainCredentialView?.isFill() ?: false
|
||||||
|
} else {
|
||||||
|
confirmButtonView?.isEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
|
||||||
|
mainCredentialView?.populatePasswordTextView(null)
|
||||||
|
if (clearKeyFile) {
|
||||||
|
mainCredentialView?.populateKeyFileTextView(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
// Reinit locking activity UI variable
|
||||||
|
DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
||||||
|
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
outState.putBoolean(KEY_READ_ONLY, mReadOnly)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadDatabase() {
|
||||||
|
loadDatabase(mDatabaseFileUri,
|
||||||
|
mainCredentialView?.getMainCredential(),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadDatabase(databaseFileUri: Uri?,
|
||||||
|
mainCredential: MainCredential?,
|
||||||
|
cipherEncryptDatabase: CipherEncryptDatabase?) {
|
||||||
|
|
||||||
|
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
|
||||||
|
clearCredentialsViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mReadOnly && (
|
||||||
|
mSpecialMode == SpecialMode.SAVE
|
||||||
|
|| mSpecialMode == SpecialMode.REGISTRATION)
|
||||||
|
) {
|
||||||
|
Log.e(TAG, getString(R.string.autofill_read_only_save))
|
||||||
|
Snackbar.make(coordinatorLayout,
|
||||||
|
R.string.autofill_read_only_save,
|
||||||
|
Snackbar.LENGTH_LONG).asError().show()
|
||||||
|
} else {
|
||||||
|
databaseFileUri?.let { databaseUri ->
|
||||||
|
// Show the progress dialog and load the database
|
||||||
|
showProgressDialogAndLoadDatabase(
|
||||||
|
databaseUri,
|
||||||
|
mainCredential ?: MainCredential(),
|
||||||
|
mReadOnly,
|
||||||
|
cipherEncryptDatabase,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
|
||||||
|
mainCredential: MainCredential,
|
||||||
|
readOnly: Boolean,
|
||||||
|
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||||
|
fixDuplicateUUID: Boolean) {
|
||||||
|
loadDatabase(
|
||||||
|
databaseUri,
|
||||||
|
mainCredential,
|
||||||
|
readOnly,
|
||||||
|
cipherEncryptDatabase,
|
||||||
|
fixDuplicateUUID
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLoadDatabaseDuplicateUuidMessage(loadDatabaseWithFix: (() -> Unit)? = null) {
|
||||||
|
DuplicateUuidDialog().apply {
|
||||||
|
positiveAction = loadDatabaseWithFix
|
||||||
|
}.show(supportFragmentManager, "duplicateUUIDDialog")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
val inflater = menuInflater
|
||||||
|
// Read menu
|
||||||
|
inflater.inflate(R.menu.open_file, menu)
|
||||||
|
if (mForceReadOnly) {
|
||||||
|
menu.removeItem(R.id.menu_open_file_read_mode_key)
|
||||||
|
} else {
|
||||||
|
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mSpecialMode == SpecialMode.DEFAULT) {
|
||||||
|
MenuUtil.defaultMenuInflater(this, inflater, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onCreateOptionsMenu(menu)
|
||||||
|
|
||||||
|
launchEducation(menu)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// To fix multiple view education
|
||||||
|
private var performedEductionInProgress = false
|
||||||
|
private fun launchEducation(menu: Menu) {
|
||||||
|
if (!performedEductionInProgress) {
|
||||||
|
performedEductionInProgress = true
|
||||||
|
// Show education views
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
performedNextEducation(menu)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performedNextEducation(menu: Menu) {
|
||||||
|
val educationToolbar = toolbar
|
||||||
|
val unlockEducationPerformed = educationToolbar != null
|
||||||
|
&& mPasswordActivityEducation.checkAndPerformedUnlockEducation(
|
||||||
|
educationToolbar,
|
||||||
|
{
|
||||||
|
performedNextEducation(menu)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
performedNextEducation(menu)
|
||||||
|
})
|
||||||
|
if (!unlockEducationPerformed) {
|
||||||
|
val readOnlyEducationPerformed =
|
||||||
|
educationToolbar?.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
|
||||||
|
&& mPasswordActivityEducation.checkAndPerformedReadOnlyEducation(
|
||||||
|
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
menu.findItem(R.id.menu_open_file_read_mode_key)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to find read mode menu")
|
||||||
|
}
|
||||||
|
performedNextEducation(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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ignored: Exception) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun changeOpenFileReadIcon(togglePassword: MenuItem) {
|
||||||
|
if (mReadOnly) {
|
||||||
|
togglePassword.setTitle(R.string.menu_file_selection_read_only)
|
||||||
|
togglePassword.setIcon(R.drawable.ic_read_only_white_24dp)
|
||||||
|
} else {
|
||||||
|
togglePassword.setTitle(R.string.menu_open_file_read_and_write)
|
||||||
|
togglePassword.setIcon(R.drawable.ic_read_write_white_24dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> finish()
|
||||||
|
R.id.menu_open_file_read_mode_key -> {
|
||||||
|
mReadOnly = !mReadOnly
|
||||||
|
changeOpenFileReadIcon(item)
|
||||||
|
}
|
||||||
|
else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val TAG = MainCredentialActivity::class.java.name
|
||||||
|
|
||||||
|
private const val UNLOCK_FRAGMENT_TAG = "UNLOCK_FRAGMENT_TAG"
|
||||||
|
|
||||||
|
private const val KEY_FILENAME = "fileName"
|
||||||
|
private const val KEY_KEYFILE = "keyFile"
|
||||||
|
private const val VIEW_INTENT = "android.intent.action.VIEW"
|
||||||
|
|
||||||
|
private const val KEY_READ_ONLY = "KEY_READ_ONLY"
|
||||||
|
private const val KEY_PASSWORD = "password"
|
||||||
|
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||||
|
|
||||||
|
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
|
||||||
|
intentBuildLauncher: (Intent) -> Unit) {
|
||||||
|
val intent = Intent(activity, MainCredentialActivity::class.java)
|
||||||
|
intent.putExtra(KEY_FILENAME, databaseFile)
|
||||||
|
if (keyFile != null)
|
||||||
|
intent.putExtra(KEY_KEYFILE, keyFile)
|
||||||
|
intentBuildLauncher.invoke(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Standard Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launch(activity: Activity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
|
activity.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Share Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launchForSearchResult(activity: Activity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
searchInfo: SearchInfo) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
|
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Save Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launchForSaveResult(activity: Activity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
searchInfo: SearchInfo) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
|
EntrySelectionHelper.startActivityForSaveModeResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Keyboard Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launchForKeyboardResult(activity: Activity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
searchInfo: SearchInfo?) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
|
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Autofill Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
autofillComponent: AutofillComponent,
|
||||||
|
searchInfo: SearchInfo?) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
|
AutofillHelper.startActivityForAutofillResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
activityResultLauncher,
|
||||||
|
autofillComponent,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Registration Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
fun launchForRegistration(activity: Activity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
registerInfo: RegisterInfo?) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||||
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
registerInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Global Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
fun launch(activity: AppCompatActivity,
|
||||||
|
databaseUri: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
||||||
|
onCancelSpecialMode: () -> Unit,
|
||||||
|
onLaunchActivitySpecialMode: () -> Unit,
|
||||||
|
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||||
|
{
|
||||||
|
MainCredentialActivity.launch(activity,
|
||||||
|
databaseUri, keyFile)
|
||||||
|
},
|
||||||
|
{ searchInfo -> // Search Action
|
||||||
|
MainCredentialActivity.launchForSearchResult(activity,
|
||||||
|
databaseUri, keyFile,
|
||||||
|
searchInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
{ searchInfo -> // Save Action
|
||||||
|
MainCredentialActivity.launchForSaveResult(activity,
|
||||||
|
databaseUri, keyFile,
|
||||||
|
searchInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
{ searchInfo -> // Keyboard Selection Action
|
||||||
|
MainCredentialActivity.launchForKeyboardResult(activity,
|
||||||
|
databaseUri, keyFile,
|
||||||
|
searchInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
{ searchInfo, autofillComponent -> // Autofill Selection Action
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
MainCredentialActivity.launchForAutofillResult(activity,
|
||||||
|
databaseUri, keyFile,
|
||||||
|
autofillActivityResultLauncher,
|
||||||
|
autofillComponent,
|
||||||
|
searchInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
} else {
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ registerInfo -> // Registration Action
|
||||||
|
MainCredentialActivity.launchForRegistration(activity,
|
||||||
|
databaseUri, keyFile,
|
||||||
|
registerInfo)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
fileNoFoundAction(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,928 +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
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.text.Editable
|
|
||||||
import android.text.TextWatcher
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.*
|
|
||||||
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
|
||||||
import android.widget.*
|
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.fragment.app.commit
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
|
||||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
|
||||||
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
|
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
|
|
||||||
import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
|
||||||
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
|
||||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.CIPHER_ENTITY_KEY
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.KEY_FILE_URI_KEY
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.MASTER_PASSWORD_KEY
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
|
||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
|
||||||
import com.kunzisoft.keepass.view.asError
|
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
|
||||||
import kotlinx.android.synthetic.main.activity_password.*
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
|
|
||||||
open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
|
||||||
|
|
||||||
// Views
|
|
||||||
private var toolbar: Toolbar? = null
|
|
||||||
private var filenameView: TextView? = null
|
|
||||||
private var passwordView: EditText? = null
|
|
||||||
private var keyFileSelectionView: KeyFileSelectionView? = null
|
|
||||||
private var confirmButtonView: Button? = null
|
|
||||||
private var checkboxPasswordView: CompoundButton? = null
|
|
||||||
private var checkboxKeyFileView: CompoundButton? = null
|
|
||||||
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
|
|
||||||
private var infoContainerView: ViewGroup? = null
|
|
||||||
|
|
||||||
private val databaseFileViewModel: DatabaseFileViewModel by viewModels()
|
|
||||||
|
|
||||||
private var mDefaultDatabase: Boolean = false
|
|
||||||
private var mDatabaseFileUri: Uri? = null
|
|
||||||
private var mDatabaseKeyFileUri: Uri? = null
|
|
||||||
|
|
||||||
private var mRememberKeyFile: Boolean = false
|
|
||||||
private var mSelectFileHelper: SelectFileHelper? = null
|
|
||||||
|
|
||||||
private var mPermissionAsked = false
|
|
||||||
private var readOnly: Boolean = false
|
|
||||||
private var mForceReadOnly: Boolean = false
|
|
||||||
set(value) {
|
|
||||||
infoContainerView?.visibility = if (value) {
|
|
||||||
readOnly = true
|
|
||||||
View.VISIBLE
|
|
||||||
} else {
|
|
||||||
View.GONE
|
|
||||||
}
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
|
||||||
|
|
||||||
private var mAllowAutoOpenBiometricPrompt: Boolean = true
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_password)
|
|
||||||
|
|
||||||
toolbar = findViewById(R.id.toolbar)
|
|
||||||
toolbar?.title = getString(R.string.app_name)
|
|
||||||
setSupportActionBar(toolbar)
|
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
|
||||||
|
|
||||||
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
|
||||||
filenameView = findViewById(R.id.filename)
|
|
||||||
passwordView = findViewById(R.id.password)
|
|
||||||
keyFileSelectionView = findViewById(R.id.keyfile_selection)
|
|
||||||
checkboxPasswordView = findViewById(R.id.password_checkbox)
|
|
||||||
checkboxKeyFileView = findViewById(R.id.keyfile_checkox)
|
|
||||||
infoContainerView = findViewById(R.id.activity_password_info_container)
|
|
||||||
|
|
||||||
mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked
|
|
||||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
|
|
||||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
|
||||||
|
|
||||||
mSelectFileHelper = SelectFileHelper(this@PasswordActivity)
|
|
||||||
keyFileSelectionView?.apply {
|
|
||||||
mSelectFileHelper?.selectFileOnClickViewListener?.let {
|
|
||||||
setOnClickListener(it)
|
|
||||||
setOnLongClickListener(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
passwordView?.setOnEditorActionListener(onEditorActionListener)
|
|
||||||
passwordView?.addTextChangedListener(object : TextWatcher {
|
|
||||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
|
||||||
|
|
||||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
|
||||||
|
|
||||||
override fun afterTextChanged(editable: Editable) {
|
|
||||||
if (editable.toString().isNotEmpty() && checkboxPasswordView?.isChecked != true)
|
|
||||||
checkboxPasswordView?.isChecked = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// If is a view 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
|
|
||||||
advancedUnlockFragment = supportFragmentManager
|
|
||||||
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
|
|
||||||
if (advancedUnlockFragment == null) {
|
|
||||||
advancedUnlockFragment = AdvancedUnlockFragment()
|
|
||||||
supportFragmentManager.commit {
|
|
||||||
replace(R.id.fragment_advanced_unlock_container_view,
|
|
||||||
advancedUnlockFragment!!,
|
|
||||||
UNLOCK_FRAGMENT_TAG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen password checkbox to init advanced unlock and confirmation button
|
|
||||||
checkboxPasswordView?.setOnCheckedChangeListener { _, _ ->
|
|
||||||
advancedUnlockFragment?.checkUnlockAvailability()
|
|
||||||
enableOrNotTheConfirmationButton()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Observe if default database
|
|
||||||
databaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
|
||||||
mDefaultDatabase = isDefaultDatabase
|
|
||||||
}
|
|
||||||
|
|
||||||
// Observe database file change
|
|
||||||
databaseFileViewModel.databaseFileLoaded.observe(this) { databaseFile ->
|
|
||||||
// Force read only if the file does not exists
|
|
||||||
mForceReadOnly = databaseFile?.let {
|
|
||||||
!it.databaseFileExists
|
|
||||||
} ?: true
|
|
||||||
invalidateOptionsMenu()
|
|
||||||
|
|
||||||
// Post init uri with KeyFile only if needed
|
|
||||||
val keyFileUri =
|
|
||||||
if (mRememberKeyFile
|
|
||||||
&& (mDatabaseKeyFileUri == null || mDatabaseKeyFileUri.toString().isEmpty())) {
|
|
||||||
databaseFile?.keyFileUri
|
|
||||||
} else {
|
|
||||||
mDatabaseKeyFileUri
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define title
|
|
||||||
filenameView?.text = databaseFile?.databaseAlias ?: ""
|
|
||||||
|
|
||||||
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
|
|
||||||
}
|
|
||||||
|
|
||||||
mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply {
|
|
||||||
onActionFinish = { actionTask, result ->
|
|
||||||
when (actionTask) {
|
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
|
||||||
// Recheck advanced unlock if error
|
|
||||||
advancedUnlockFragment?.initAdvancedUnlockMode()
|
|
||||||
|
|
||||||
if (result.isSuccess) {
|
|
||||||
mDatabaseKeyFileUri = null
|
|
||||||
clearCredentialsViews(true)
|
|
||||||
launchGroupActivity()
|
|
||||||
} else {
|
|
||||||
var resultError = ""
|
|
||||||
val resultException = result.exception
|
|
||||||
val resultMessage = result.message
|
|
||||||
|
|
||||||
if (resultException != null) {
|
|
||||||
resultError = resultException.getLocalizedMessage(resources)
|
|
||||||
|
|
||||||
when (resultException) {
|
|
||||||
is DuplicateUuidDatabaseException -> {
|
|
||||||
// Relaunch loading if we need to fix UUID
|
|
||||||
showLoadDatabaseDuplicateUuidMessage {
|
|
||||||
|
|
||||||
var databaseUri: Uri? = null
|
|
||||||
var masterPassword: String? = null
|
|
||||||
var keyFileUri: Uri? = null
|
|
||||||
var readOnly = true
|
|
||||||
var cipherEntity: CipherDatabaseEntity? = null
|
|
||||||
|
|
||||||
result.data?.let { resultData ->
|
|
||||||
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
|
|
||||||
masterPassword = resultData.getString(MASTER_PASSWORD_KEY)
|
|
||||||
keyFileUri = resultData.getParcelable(KEY_FILE_URI_KEY)
|
|
||||||
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
|
||||||
cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY)
|
|
||||||
}
|
|
||||||
|
|
||||||
databaseUri?.let { databaseFileUri ->
|
|
||||||
showProgressDialogAndLoadDatabase(
|
|
||||||
databaseFileUri,
|
|
||||||
masterPassword,
|
|
||||||
keyFileUri,
|
|
||||||
readOnly,
|
|
||||||
cipherEntity,
|
|
||||||
true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is FileNotFoundDatabaseException -> {
|
|
||||||
// Remove this default database inaccessible
|
|
||||||
if (mDefaultDatabase) {
|
|
||||||
databaseFileViewModel.removeDefaultDatabase()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show error message
|
|
||||||
if (resultMessage != null && resultMessage.isNotEmpty()) {
|
|
||||||
resultError = "$resultError $resultMessage"
|
|
||||||
}
|
|
||||||
Log.e(TAG, resultError)
|
|
||||||
Snackbar.make(activity_password_coordinator_layout,
|
|
||||||
resultError,
|
|
||||||
Snackbar.LENGTH_LONG).asError().show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getUriFromIntent(intent: Intent?) {
|
|
||||||
// If is a view intent
|
|
||||||
val action = intent?.action
|
|
||||||
if (action != null
|
|
||||||
&& action == VIEW_INTENT) {
|
|
||||||
mDatabaseFileUri = intent.data
|
|
||||||
mDatabaseKeyFileUri = UriUtil.getUriFromIntent(intent, KEY_KEYFILE)
|
|
||||||
} else {
|
|
||||||
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
|
|
||||||
mDatabaseKeyFileUri = intent?.getParcelableExtra(KEY_KEYFILE)
|
|
||||||
}
|
|
||||||
mDatabaseFileUri?.let {
|
|
||||||
databaseFileViewModel.checkIfIsDefaultDatabase(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
|
||||||
super.onNewIntent(intent)
|
|
||||||
getUriFromIntent(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchGroupActivity() {
|
|
||||||
GroupActivity.launch(this,
|
|
||||||
readOnly,
|
|
||||||
{ onValidateSpecialMode() },
|
|
||||||
{ onCancelSpecialMode() },
|
|
||||||
{ onLaunchActivitySpecialMode() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onValidateSpecialMode() {
|
|
||||||
super.onValidateSpecialMode()
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCancelSpecialMode() {
|
|
||||||
super.onCancelSpecialMode()
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun retrieveCredentialForEncryption(): String {
|
|
||||||
return passwordView?.text?.toString() ?: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun conditionToStoreCredential(): Boolean {
|
|
||||||
return checkboxPasswordView?.isChecked == true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCredentialEncrypted(databaseUri: Uri,
|
|
||||||
encryptedCredential: String,
|
|
||||||
ivSpec: String) {
|
|
||||||
// Load the database if password is registered with biometric
|
|
||||||
verifyCheckboxesAndLoadDatabase(
|
|
||||||
CipherDatabaseEntity(
|
|
||||||
databaseUri.toString(),
|
|
||||||
encryptedCredential,
|
|
||||||
ivSpec)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCredentialDecrypted(databaseUri: Uri,
|
|
||||||
decryptedCredential: String) {
|
|
||||||
// Load the database if password is retrieve from biometric
|
|
||||||
// Retrieve from biometric
|
|
||||||
verifyKeyFileCheckboxAndLoadDatabase(decryptedCredential)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val onEditorActionListener = object : TextView.OnEditorActionListener {
|
|
||||||
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
|
|
||||||
if (actionId == IME_ACTION_DONE) {
|
|
||||||
verifyCheckboxesAndLoadDatabase()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
if (Database.getInstance().loaded) {
|
|
||||||
launchGroupActivity()
|
|
||||||
} else {
|
|
||||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
|
||||||
|
|
||||||
// If the database isn't accessible make sure to clear the password field, if it
|
|
||||||
// was saved in the instance state
|
|
||||||
if (Database.getInstance().loaded) {
|
|
||||||
clearCredentialsViews()
|
|
||||||
}
|
|
||||||
|
|
||||||
mProgressDatabaseTaskProvider?.registerProgressTask()
|
|
||||||
|
|
||||||
// Back to previous keyboard is setting activated
|
|
||||||
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this)) {
|
|
||||||
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't allow auto open prompt if lock become when UI visible
|
|
||||||
mAllowAutoOpenBiometricPrompt = if (LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true)
|
|
||||||
false
|
|
||||||
else
|
|
||||||
mAllowAutoOpenBiometricPrompt
|
|
||||||
mDatabaseFileUri?.let { databaseFileUri ->
|
|
||||||
databaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
|
||||||
}
|
|
||||||
|
|
||||||
checkPermission()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
|
||||||
// Define Key File text
|
|
||||||
if (mRememberKeyFile) {
|
|
||||||
populateKeyFileTextView(keyFileUri)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define listener for validate button
|
|
||||||
confirmButtonView?.setOnClickListener { verifyCheckboxesAndLoadDatabase() }
|
|
||||||
|
|
||||||
// If Activity is launch with a password and want to open directly
|
|
||||||
val intent = intent
|
|
||||||
val password = intent.getStringExtra(KEY_PASSWORD)
|
|
||||||
// Consume the intent extra password
|
|
||||||
intent.removeExtra(KEY_PASSWORD)
|
|
||||||
val launchImmediately = intent.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false)
|
|
||||||
if (password != null) {
|
|
||||||
populatePasswordTextView(password)
|
|
||||||
}
|
|
||||||
if (launchImmediately) {
|
|
||||||
verifyCheckboxesAndLoadDatabase(password, keyFileUri)
|
|
||||||
} else {
|
|
||||||
// Init Biometric elements
|
|
||||||
advancedUnlockFragment?.loadDatabase(databaseFileUri,
|
|
||||||
mAllowAutoOpenBiometricPrompt
|
|
||||||
&& mProgressDatabaseTaskProvider?.isBinded() != true)
|
|
||||||
}
|
|
||||||
|
|
||||||
enableOrNotTheConfirmationButton()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun enableOrNotTheConfirmationButton() {
|
|
||||||
// Enable or not the open button if setting is checked
|
|
||||||
if (!PreferencesUtil.emptyPasswordAllowed(this@PasswordActivity)) {
|
|
||||||
checkboxPasswordView?.let {
|
|
||||||
confirmButtonView?.isEnabled = (checkboxPasswordView?.isChecked == true
|
|
||||||
|| checkboxKeyFileView?.isChecked == true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
confirmButtonView?.isEnabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
|
|
||||||
populatePasswordTextView(null)
|
|
||||||
if (clearKeyFile) {
|
|
||||||
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() {
|
|
||||||
mProgressDatabaseTaskProvider?.unregisterProgressTask()
|
|
||||||
|
|
||||||
// Reinit locking activity UI variable
|
|
||||||
LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
|
||||||
mAllowAutoOpenBiometricPrompt = true
|
|
||||||
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
outState.putBoolean(KEY_PERMISSION_ASKED, mPermissionAsked)
|
|
||||||
mDatabaseKeyFileUri?.let {
|
|
||||||
outState.putString(KEY_KEYFILE, it.toString())
|
|
||||||
}
|
|
||||||
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
|
||||||
outState.putBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT, false)
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun verifyCheckboxesAndLoadDatabase(cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
|
||||||
val password: String? = passwordView?.text?.toString()
|
|
||||||
val keyFile: Uri? = keyFileSelectionView?.uri
|
|
||||||
verifyCheckboxesAndLoadDatabase(password, keyFile, cipherDatabaseEntity)
|
|
||||||
}
|
|
||||||
|
|
||||||
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?,
|
|
||||||
password: String?,
|
|
||||||
keyFileUri: Uri?,
|
|
||||||
cipherDatabaseEntity: CipherDatabaseEntity? = null) {
|
|
||||||
|
|
||||||
if (PreferencesUtil.deletePasswordAfterConnexionAttempt(this)) {
|
|
||||||
clearCredentialsViews()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (readOnly && (
|
|
||||||
mSpecialMode == SpecialMode.SAVE
|
|
||||||
|| mSpecialMode == SpecialMode.REGISTRATION)
|
|
||||||
) {
|
|
||||||
Log.e(TAG, getString(R.string.autofill_read_only_save))
|
|
||||||
Snackbar.make(activity_password_coordinator_layout,
|
|
||||||
R.string.autofill_read_only_save,
|
|
||||||
Snackbar.LENGTH_LONG).asError().show()
|
|
||||||
} else {
|
|
||||||
databaseFileUri?.let { databaseUri ->
|
|
||||||
// Show the progress dialog and load the database
|
|
||||||
showProgressDialogAndLoadDatabase(
|
|
||||||
databaseUri,
|
|
||||||
password,
|
|
||||||
keyFileUri,
|
|
||||||
readOnly,
|
|
||||||
cipherDatabaseEntity,
|
|
||||||
false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showProgressDialogAndLoadDatabase(databaseUri: Uri,
|
|
||||||
password: String?,
|
|
||||||
keyFile: Uri?,
|
|
||||||
readOnly: Boolean,
|
|
||||||
cipherDatabaseEntity: CipherDatabaseEntity?,
|
|
||||||
fixDuplicateUUID: Boolean) {
|
|
||||||
mProgressDatabaseTaskProvider?.startDatabaseLoad(
|
|
||||||
databaseUri,
|
|
||||||
password,
|
|
||||||
keyFile,
|
|
||||||
readOnly,
|
|
||||||
cipherDatabaseEntity,
|
|
||||||
fixDuplicateUUID
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showLoadDatabaseDuplicateUuidMessage(loadDatabaseWithFix: (() -> Unit)? = null) {
|
|
||||||
DuplicateUuidDialog().apply {
|
|
||||||
positiveAction = loadDatabaseWithFix
|
|
||||||
}.show(supportFragmentManager, "duplicateUUIDDialog")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
||||||
val inflater = menuInflater
|
|
||||||
// Read menu
|
|
||||||
inflater.inflate(R.menu.open_file, menu)
|
|
||||||
if (mForceReadOnly) {
|
|
||||||
menu.removeItem(R.id.menu_open_file_read_mode_key)
|
|
||||||
} else {
|
|
||||||
changeOpenFileReadIcon(menu.findItem(R.id.menu_open_file_read_mode_key))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mSpecialMode == SpecialMode.DEFAULT) {
|
|
||||||
MenuUtil.defaultMenuInflater(inflater, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onCreateOptionsMenu(menu)
|
|
||||||
|
|
||||||
launchEducation(menu)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check permission
|
|
||||||
private fun checkPermission() {
|
|
||||||
if (Build.VERSION.SDK_INT in 23..28
|
|
||||||
&& !readOnly
|
|
||||||
&& !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
|
|
||||||
private var performedEductionInProgress = false
|
|
||||||
private fun launchEducation(menu: Menu) {
|
|
||||||
if (!performedEductionInProgress) {
|
|
||||||
performedEductionInProgress = true
|
|
||||||
// Show education views
|
|
||||||
Handler(Looper.getMainLooper()).post { performedNextEducation(PasswordActivityEducation(this), menu) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun performedNextEducation(passwordActivityEducation: PasswordActivityEducation,
|
|
||||||
menu: Menu) {
|
|
||||||
val educationToolbar = toolbar
|
|
||||||
val unlockEducationPerformed = educationToolbar != null
|
|
||||||
&& passwordActivityEducation.checkAndPerformedUnlockEducation(
|
|
||||||
educationToolbar,
|
|
||||||
{
|
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
|
||||||
})
|
|
||||||
if (!unlockEducationPerformed) {
|
|
||||||
val readOnlyEducationPerformed =
|
|
||||||
educationToolbar?.findViewById<View>(R.id.menu_open_file_read_mode_key) != null
|
|
||||||
&& passwordActivityEducation.checkAndPerformedReadOnlyEducation(
|
|
||||||
educationToolbar.findViewById(R.id.menu_open_file_read_mode_key),
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
menu.findItem(R.id.menu_open_file_read_mode_key)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to find read mode menu")
|
|
||||||
}
|
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
|
||||||
})
|
|
||||||
|
|
||||||
advancedUnlockFragment?.performEducation(passwordActivityEducation,
|
|
||||||
readOnlyEducationPerformed,
|
|
||||||
{
|
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
performedNextEducation(passwordActivityEducation, menu)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun changeOpenFileReadIcon(togglePassword: MenuItem) {
|
|
||||||
if (readOnly) {
|
|
||||||
togglePassword.setTitle(R.string.menu_file_selection_read_only)
|
|
||||||
togglePassword.setIcon(R.drawable.ic_read_only_white_24dp)
|
|
||||||
} else {
|
|
||||||
togglePassword.setTitle(R.string.menu_open_file_read_and_write)
|
|
||||||
togglePassword.setIcon(R.drawable.ic_read_write_white_24dp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
|
|
||||||
when (item.itemId) {
|
|
||||||
android.R.id.home -> finish()
|
|
||||||
R.id.menu_open_file_read_mode_key -> {
|
|
||||||
readOnly = !readOnly
|
|
||||||
changeOpenFileReadIcon(item)
|
|
||||||
}
|
|
||||||
else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, 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
|
|
||||||
mSelectFileHelper?.let {
|
|
||||||
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
|
|
||||||
) { uri ->
|
|
||||||
if (uri != null) {
|
|
||||||
mDatabaseKeyFileUri = uri
|
|
||||||
populateKeyFileTextView(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!keyFileResult) {
|
|
||||||
// this block if not a key file response
|
|
||||||
when (resultCode) {
|
|
||||||
LockingActivity.RESULT_EXIT_LOCK -> {
|
|
||||||
clearCredentialsViews()
|
|
||||||
Database.getInstance().clearAndClose(UriUtil.getBinaryDir(this))
|
|
||||||
}
|
|
||||||
Activity.RESULT_CANCELED -> {
|
|
||||||
clearCredentialsViews()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private val TAG = PasswordActivity::class.java.name
|
|
||||||
|
|
||||||
private const val UNLOCK_FRAGMENT_TAG = "UNLOCK_FRAGMENT_TAG"
|
|
||||||
|
|
||||||
private const val KEY_FILENAME = "fileName"
|
|
||||||
private const val KEY_KEYFILE = "keyFile"
|
|
||||||
private const val VIEW_INTENT = "android.intent.action.VIEW"
|
|
||||||
|
|
||||||
private const val KEY_PASSWORD = "password"
|
|
||||||
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
|
||||||
private 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?,
|
|
||||||
intentBuildLauncher: (Intent) -> Unit) {
|
|
||||||
val intent = Intent(activity, PasswordActivity::class.java)
|
|
||||||
intent.putExtra(KEY_FILENAME, databaseFile)
|
|
||||||
if (keyFile != null)
|
|
||||||
intent.putExtra(KEY_KEYFILE, keyFile)
|
|
||||||
intentBuildLauncher.invoke(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Standard Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
fun launch(activity: Activity,
|
|
||||||
databaseFile: Uri,
|
|
||||||
keyFile: Uri?) {
|
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
|
||||||
activity.startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Share Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
fun launchForSearchResult(activity: Activity,
|
|
||||||
databaseFile: Uri,
|
|
||||||
keyFile: Uri?,
|
|
||||||
searchInfo: SearchInfo) {
|
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
|
||||||
EntrySelectionHelper.startActivityForSearchModeResult(
|
|
||||||
activity,
|
|
||||||
intent,
|
|
||||||
searchInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Save Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
fun launchForSaveResult(activity: Activity,
|
|
||||||
databaseFile: Uri,
|
|
||||||
keyFile: Uri?,
|
|
||||||
searchInfo: SearchInfo) {
|
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
|
||||||
EntrySelectionHelper.startActivityForSaveModeResult(
|
|
||||||
activity,
|
|
||||||
intent,
|
|
||||||
searchInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Keyboard Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
fun launchForKeyboardResult(activity: Activity,
|
|
||||||
databaseFile: Uri,
|
|
||||||
keyFile: Uri?,
|
|
||||||
searchInfo: SearchInfo?) {
|
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
|
||||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
|
|
||||||
activity,
|
|
||||||
intent,
|
|
||||||
searchInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Autofill Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
fun launchForAutofillResult(activity: Activity,
|
|
||||||
databaseFile: Uri,
|
|
||||||
keyFile: Uri?,
|
|
||||||
autofillComponent: AutofillComponent,
|
|
||||||
searchInfo: SearchInfo?) {
|
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
|
||||||
AutofillHelper.startActivityForAutofillResult(
|
|
||||||
activity,
|
|
||||||
intent,
|
|
||||||
autofillComponent,
|
|
||||||
searchInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Registration Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
fun launchForRegistration(activity: Activity,
|
|
||||||
databaseFile: Uri,
|
|
||||||
keyFile: Uri?,
|
|
||||||
registerInfo: RegisterInfo?) {
|
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
|
||||||
activity,
|
|
||||||
intent,
|
|
||||||
registerInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* -------------------------
|
|
||||||
* Global Launch
|
|
||||||
* -------------------------
|
|
||||||
*/
|
|
||||||
fun launch(activity: Activity,
|
|
||||||
databaseUri: Uri,
|
|
||||||
keyFile: Uri?,
|
|
||||||
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
|
||||||
onCancelSpecialMode: () -> Unit,
|
|
||||||
onLaunchActivitySpecialMode: () -> Unit) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
|
||||||
{
|
|
||||||
PasswordActivity.launch(activity,
|
|
||||||
databaseUri, keyFile)
|
|
||||||
},
|
|
||||||
{ searchInfo -> // Search Action
|
|
||||||
PasswordActivity.launchForSearchResult(activity,
|
|
||||||
databaseUri, keyFile,
|
|
||||||
searchInfo)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
},
|
|
||||||
{ searchInfo -> // Save Action
|
|
||||||
PasswordActivity.launchForSaveResult(activity,
|
|
||||||
databaseUri, keyFile,
|
|
||||||
searchInfo)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
},
|
|
||||||
{ searchInfo -> // Keyboard Selection Action
|
|
||||||
PasswordActivity.launchForKeyboardResult(activity,
|
|
||||||
databaseUri, keyFile,
|
|
||||||
searchInfo)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
},
|
|
||||||
{ searchInfo, autofillComponent -> // Autofill Selection Action
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
PasswordActivity.launchForAutofillResult(activity,
|
|
||||||
databaseUri, keyFile,
|
|
||||||
autofillComponent,
|
|
||||||
searchInfo)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
} else {
|
|
||||||
onCancelSpecialMode()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ registerInfo -> // Registration Action
|
|
||||||
PasswordActivity.launchForRegistration(activity,
|
|
||||||
databaseUri, keyFile,
|
|
||||||
registerInfo)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
fileNoFoundAction(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,12 +23,11 @@ import android.app.Dialog
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||||
|
|
||||||
|
|
||||||
class DatabaseChangedDialogFragment : DialogFragment() {
|
class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
var actionDatabaseListener: ActionDatabaseChangedListener? = null
|
var actionDatabaseListener: ActionDatabaseChangedListener? = null
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
|
||||||
|
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
||||||
|
|
||||||
|
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||||
|
private var mDatabase: Database? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
mDatabaseViewModel.database.observe(this) { database ->
|
||||||
|
this.mDatabase = database
|
||||||
|
resetAppTimeoutOnTouchOrFocus()
|
||||||
|
onDatabaseRetrieved(database)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.actionFinished.observe(this) { result ->
|
||||||
|
onDatabaseActionFinished(result.database, result.actionTask, result.result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
|
||||||
|
resetAppTimeoutOnTouchOrFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
// Can be overridden by a subclass
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseActionFinished(
|
||||||
|
database: Database,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
// Can be overridden by a subclass
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetAppTimeout() {
|
||||||
|
context?.let {
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(it,
|
||||||
|
mDatabase?.loaded ?: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun overrideTimeoutTouchAndFocusEvents(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetAppTimeoutOnTouchOrFocus() {
|
||||||
|
if (!overrideTimeoutTouchAndFocusEvents()) {
|
||||||
|
context?.let {
|
||||||
|
dialog?.window?.decorView?.resetAppTimeoutWhenViewTouchedOrFocused(
|
||||||
|
it,
|
||||||
|
mDatabase?.loaded
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import android.content.Context
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
|
|
||||||
|
// Not as DatabaseDialogFragment because crash on KitKat
|
||||||
class DatePickerFragment : DialogFragment() {
|
class DatePickerFragment : DialogFragment() {
|
||||||
|
|
||||||
private var mDefaultYear: Int = 2000
|
private var mDefaultYear: Int = 2000
|
||||||
|
|||||||
@@ -20,61 +20,38 @@
|
|||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.activityViewModels
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.viewmodels.NodesViewModel
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getBundleFromListNodes
|
|
||||||
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
|
||||||
|
|
||||||
open class DeleteNodesDialogFragment : DialogFragment() {
|
class DeleteNodesDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
private var mNodesToDelete: List<Node> = ArrayList()
|
private var mNodesToDelete: List<Node> = listOf()
|
||||||
private var mListener: DeleteNodeListener? = null
|
private val mNodesViewModel: NodesViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
try {
|
|
||||||
mListener = context as DeleteNodeListener
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
throw ClassCastException(context.toString()
|
|
||||||
+ " must implement " + DeleteNodeListener::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetach() {
|
|
||||||
mListener = null
|
|
||||||
super.onDetach()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun retrieveMessage(): String {
|
|
||||||
return getString(R.string.warning_permanently_delete_nodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
mNodesViewModel.nodesToDelete.observe(this) { nodes ->
|
||||||
|
this.mNodesToDelete = nodes
|
||||||
|
}
|
||||||
|
var recycleBin = false
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY)
|
if (containsKey(RECYCLE_BIN_TAG)) {
|
||||||
&& containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) {
|
recycleBin = this.getBoolean(RECYCLE_BIN_TAG)
|
||||||
mNodesToDelete = getListNodesFromBundle(Database.getInstance(), this)
|
|
||||||
}
|
|
||||||
} ?: savedInstanceState?.apply {
|
|
||||||
if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY)
|
|
||||||
&& containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) {
|
|
||||||
mNodesToDelete = getListNodesFromBundle(Database.getInstance(), savedInstanceState)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
// Use the Builder class for convenient dialog construction
|
// Use the Builder class for convenient dialog construction
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|
||||||
builder.setMessage(retrieveMessage())
|
builder.setMessage(if (recycleBin)
|
||||||
|
getString(R.string.warning_empty_recycle_bin)
|
||||||
|
else
|
||||||
|
getString(R.string.warning_permanently_delete_nodes))
|
||||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
mListener?.permanentlyDeleteNodes(mNodesToDelete)
|
mNodesViewModel.permanentlyDeleteNodes(mNodesToDelete)
|
||||||
}
|
}
|
||||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||||
// Create the AlertDialog object and return it
|
// Create the AlertDialog object and return it
|
||||||
@@ -83,19 +60,14 @@ open class DeleteNodesDialogFragment : DialogFragment() {
|
|||||||
return super.onCreateDialog(savedInstanceState)
|
return super.onCreateDialog(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
outState.putAll(getBundleFromListNodes(mNodesToDelete))
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeleteNodeListener {
|
|
||||||
fun permanentlyDeleteNodes(nodes: List<Node>)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getInstance(nodesToDelete: List<Node>): DeleteNodesDialogFragment {
|
private const val RECYCLE_BIN_TAG = "RECYCLE_BIN_TAG"
|
||||||
|
|
||||||
|
fun getInstance(recycleBin: Boolean): DeleteNodesDialogFragment {
|
||||||
return DeleteNodesDialogFragment().apply {
|
return DeleteNodesDialogFragment().apply {
|
||||||
arguments = getBundleFromListNodes(nodesToDelete)
|
arguments = Bundle().apply {
|
||||||
|
putBoolean(RECYCLE_BIN_TAG, recycleBin)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ class DuplicateUuidDialog : DialogFragment() {
|
|||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
// Use the Builder class for convenient dialog construction
|
// Use the Builder class for convenient dialog construction
|
||||||
val builder = androidx.appcompat.app.AlertDialog.Builder(activity).apply {
|
val builder = AlertDialog.Builder(activity).apply {
|
||||||
val message = getString(R.string.contains_duplicate_uuid) +
|
val message = getString(R.string.contains_duplicate_uuid) +
|
||||||
"\n\n" + getString(R.string.contains_duplicate_uuid_procedure)
|
"\n\n" + getString(R.string.contains_duplicate_uuid_procedure)
|
||||||
setMessage(message)
|
setMessage(message)
|
||||||
|
|||||||
@@ -31,14 +31,13 @@ import android.widget.ImageView
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Field
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.model.Field
|
|
||||||
|
|
||||||
|
|
||||||
class EntryCustomFieldDialogFragment: DialogFragment() {
|
class EntryCustomFieldDialogFragment: DatabaseDialogFragment() {
|
||||||
|
|
||||||
private var oldField: Field? = null
|
private var oldField: Field? = null
|
||||||
|
|
||||||
|
|||||||
@@ -1,210 +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 com.google.android.material.textfield.TextInputLayout
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.*
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.password.PasswordGenerator
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
|
||||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
|
||||||
|
|
||||||
class GeneratePasswordDialogFragment : DialogFragment() {
|
|
||||||
|
|
||||||
private var mListener: GeneratePasswordListener? = null
|
|
||||||
|
|
||||||
private var root: View? = null
|
|
||||||
private var lengthTextView: EditText? = null
|
|
||||||
private var passwordInputLayoutView: TextInputLayout? = null
|
|
||||||
private var passwordView: EditText? = null
|
|
||||||
|
|
||||||
private var uppercaseBox: CompoundButton? = null
|
|
||||||
private var lowercaseBox: CompoundButton? = null
|
|
||||||
private var digitsBox: CompoundButton? = null
|
|
||||||
private var minusBox: CompoundButton? = null
|
|
||||||
private var underlineBox: CompoundButton? = null
|
|
||||||
private var spaceBox: CompoundButton? = null
|
|
||||||
private var specialsBox: CompoundButton? = null
|
|
||||||
private var bracketsBox: CompoundButton? = null
|
|
||||||
private var extendedBox: CompoundButton? = null
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
try {
|
|
||||||
mListener = context as GeneratePasswordListener
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
throw ClassCastException(context.toString()
|
|
||||||
+ " must implement " + GeneratePasswordListener::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun 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.allowCopyPasswordAndProtectedFields(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)
|
|
||||||
|
|
||||||
assignDefaultCharacters()
|
|
||||||
|
|
||||||
val seekBar = root?.findViewById<SeekBar>(R.id.seekbar_length)
|
|
||||||
seekBar?.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
||||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
|
||||||
lengthTextView?.setText(progress.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
|
||||||
|
|
||||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
|
||||||
})
|
|
||||||
|
|
||||||
context?.let { context ->
|
|
||||||
seekBar?.progress = PreferencesUtil.getDefaultPasswordLength(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
root?.findViewById<Button>(R.id.generate_password_button)
|
|
||||||
?.setOnClickListener { fillPassword() }
|
|
||||||
|
|
||||||
builder.setView(root)
|
|
||||||
.setPositiveButton(R.string.accept) { _, _ ->
|
|
||||||
val bundle = Bundle()
|
|
||||||
bundle.putString(KEY_PASSWORD_ID, passwordView!!.text.toString())
|
|
||||||
mListener?.acceptPassword(bundle)
|
|
||||||
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
val bundle = Bundle()
|
|
||||||
mListener?.cancelPassword(bundle)
|
|
||||||
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pre-populate a password to possibly save the user a few clicks
|
|
||||||
fillPassword()
|
|
||||||
|
|
||||||
return builder.create()
|
|
||||||
}
|
|
||||||
return super.onCreateDialog(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun assignDefaultCharacters() {
|
|
||||||
uppercaseBox?.isChecked = false
|
|
||||||
lowercaseBox?.isChecked = false
|
|
||||||
digitsBox?.isChecked = false
|
|
||||||
minusBox?.isChecked = false
|
|
||||||
underlineBox?.isChecked = false
|
|
||||||
spaceBox?.isChecked = false
|
|
||||||
specialsBox?.isChecked = false
|
|
||||||
bracketsBox?.isChecked = false
|
|
||||||
extendedBox?.isChecked = false
|
|
||||||
|
|
||||||
context?.let { context ->
|
|
||||||
PreferencesUtil.getDefaultPasswordCharacters(context)?.let { charSet ->
|
|
||||||
for (passwordChar in charSet) {
|
|
||||||
when (passwordChar) {
|
|
||||||
getString(R.string.value_password_uppercase) -> uppercaseBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_lowercase) -> lowercaseBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_digits) -> digitsBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_minus) -> minusBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_underline) -> underlineBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_space) -> spaceBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_special) -> specialsBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_brackets) -> bracketsBox?.isChecked = true
|
|
||||||
getString(R.string.value_password_extended) -> extendedBox?.isChecked = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fillPassword() {
|
|
||||||
root?.findViewById<EditText>(R.id.password)?.setText(generatePassword())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generatePassword(): String {
|
|
||||||
var password = ""
|
|
||||||
try {
|
|
||||||
val length = Integer.valueOf(root?.findViewById<EditText>(R.id.length)?.text.toString())
|
|
||||||
password = PasswordGenerator(resources).generatePassword(length,
|
|
||||||
uppercaseBox?.isChecked == true,
|
|
||||||
lowercaseBox?.isChecked == true,
|
|
||||||
digitsBox?.isChecked == true,
|
|
||||||
minusBox?.isChecked == true,
|
|
||||||
underlineBox?.isChecked == true,
|
|
||||||
spaceBox?.isChecked == true,
|
|
||||||
specialsBox?.isChecked == true,
|
|
||||||
bracketsBox?.isChecked == true,
|
|
||||||
extendedBox?.isChecked == true)
|
|
||||||
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(bundle: Bundle)
|
|
||||||
fun cancelPassword(bundle: Bundle)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val KEY_PASSWORD_ID = "KEY_PASSWORD_ID"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,37 +20,52 @@
|
|||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import android.view.View
|
||||||
import androidx.fragment.app.DialogFragment
|
import android.view.ViewGroup
|
||||||
|
import android.widget.*
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import android.widget.Button
|
import androidx.fragment.app.activityViewModels
|
||||||
import android.widget.ImageView
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import android.widget.TextView
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION
|
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.*
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
|
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.Group
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.icons.assignDatabaseIcon
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
|
import com.kunzisoft.keepass.view.DateTimeEditFieldView
|
||||||
|
import com.kunzisoft.keepass.view.InheritedCompletionView
|
||||||
|
import com.kunzisoft.keepass.view.TagsCompletionView
|
||||||
|
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
|
||||||
|
import com.tokenautocomplete.FilteredArrayAdapter
|
||||||
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconPickerListener {
|
class GroupEditDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
private var mDatabase: Database? = null
|
private val mGroupEditViewModel: GroupEditViewModel by activityViewModels()
|
||||||
|
|
||||||
private var editGroupListener: EditGroupListener? = null
|
private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null
|
||||||
|
private var mEditGroupDialogAction = NONE
|
||||||
|
private var mGroupInfo = GroupInfo()
|
||||||
|
private var mGroupNamesNotAllowed: List<String>? = null
|
||||||
|
|
||||||
private var editGroupDialogAction: EditGroupDialogAction? = null
|
private lateinit var iconButtonView: ImageView
|
||||||
private var nameGroup: String? = null
|
private var mIconColor: Int = 0
|
||||||
private var iconGroup: IconImage? = null
|
private lateinit var nameTextLayoutView: TextInputLayout
|
||||||
|
private lateinit var nameTextView: TextView
|
||||||
private var nameTextLayoutView: TextInputLayout? = null
|
private lateinit var notesTextLayoutView: TextInputLayout
|
||||||
private var nameTextView: TextView? = null
|
private lateinit var notesTextView: TextView
|
||||||
private var iconButtonView: ImageView? = null
|
private lateinit var expirationView: DateTimeEditFieldView
|
||||||
private var iconColor: Int = 0
|
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;
|
||||||
@@ -62,81 +77,127 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onAttach(context)
|
super.onCreate(savedInstanceState)
|
||||||
// Verify that the host activity implements the callback interface
|
|
||||||
try {
|
mGroupEditViewModel.onIconSelected.observe(this) { iconImage ->
|
||||||
// Instantiate the NoticeDialogListener so we can send events to the host
|
mGroupInfo.icon = iconImage
|
||||||
editGroupListener = context as EditGroupListener
|
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
||||||
} catch (e: ClassCastException) {
|
}
|
||||||
// The activity doesn't implement the interface, throw exception
|
|
||||||
throw ClassCastException(context.toString()
|
mGroupEditViewModel.onDateSelected.observe(this) { viewModelDate ->
|
||||||
+ " must implement " + GroupEditDialogFragment::class.java.name)
|
// Save the date
|
||||||
|
mGroupInfo.expiryTime = DateInstant(
|
||||||
|
DateTime(mGroupInfo.expiryTime.date)
|
||||||
|
.withYear(viewModelDate.year)
|
||||||
|
.withMonthOfYear(viewModelDate.month + 1)
|
||||||
|
.withDayOfMonth(viewModelDate.day)
|
||||||
|
.toDate())
|
||||||
|
expirationView.dateTime = mGroupInfo.expiryTime
|
||||||
|
if (expirationView.dateTime.type == DateInstant.Type.DATE_TIME) {
|
||||||
|
val instantTime = DateInstant(mGroupInfo.expiryTime.date, DateInstant.Type.TIME)
|
||||||
|
// Trick to recall selection with time
|
||||||
|
mGroupEditViewModel.requestDateTimeSelection(instantTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mGroupEditViewModel.onTimeSelected.observe(this) { viewModelTime ->
|
||||||
|
// Save the time
|
||||||
|
mGroupInfo.expiryTime = DateInstant(
|
||||||
|
DateTime(mGroupInfo.expiryTime.date)
|
||||||
|
.withHourOfDay(viewModelTime.hours)
|
||||||
|
.withMinuteOfHour(viewModelTime.minutes)
|
||||||
|
.toDate(), mGroupInfo.expiryTime.type)
|
||||||
|
expirationView.dateTime = mGroupInfo.expiryTime
|
||||||
|
}
|
||||||
|
|
||||||
|
mGroupEditViewModel.groupNamesNotAllowed.observe(this) { namesNotAllowed ->
|
||||||
|
this.mGroupNamesNotAllowed = namesNotAllowed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetach() {
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
editGroupListener = null
|
super.onDatabaseRetrieved(database)
|
||||||
super.onDetach()
|
|
||||||
|
mPopulateIconMethod = { imageView, icon ->
|
||||||
|
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||||
|
}
|
||||||
|
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 {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val root = activity.layoutInflater.inflate(R.layout.fragment_group_edit, null)
|
val root = activity.layoutInflater.inflate(R.layout.fragment_group_edit, null)
|
||||||
nameTextLayoutView = root?.findViewById(R.id.group_edit_name_container)
|
iconButtonView = root.findViewById(R.id.group_edit_icon_button)
|
||||||
nameTextView = root?.findViewById(R.id.group_edit_name)
|
nameTextLayoutView = root.findViewById(R.id.group_edit_name_container)
|
||||||
iconButtonView = root?.findViewById(R.id.group_edit_icon_button)
|
nameTextView = root.findViewById(R.id.group_edit_name)
|
||||||
|
notesTextLayoutView = root.findViewById(R.id.group_edit_note_container)
|
||||||
|
notesTextView = root.findViewById(R.id.group_edit_note)
|
||||||
|
expirationView = root.findViewById(R.id.group_edit_expiration)
|
||||||
|
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))
|
||||||
iconColor = ta.getColor(0, Color.WHITE)
|
mIconColor = ta.getColor(0, Color.WHITE)
|
||||||
ta.recycle()
|
ta.recycle()
|
||||||
|
|
||||||
// Init elements
|
|
||||||
mDatabase = Database.getInstance()
|
|
||||||
editGroupDialogAction = EditGroupDialogAction.NONE
|
|
||||||
nameGroup = ""
|
|
||||||
iconGroup = mDatabase?.iconFactory?.folderIcon
|
|
||||||
|
|
||||||
if (savedInstanceState != null
|
if (savedInstanceState != null
|
||||||
&& savedInstanceState.containsKey(KEY_ACTION_ID)
|
&& savedInstanceState.containsKey(KEY_ACTION_ID)
|
||||||
&& savedInstanceState.containsKey(KEY_NAME)
|
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
|
||||||
&& savedInstanceState.containsKey(KEY_ICON)) {
|
mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID))
|
||||||
editGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID))
|
mGroupInfo = savedInstanceState.getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
||||||
nameGroup = savedInstanceState.getString(KEY_NAME)
|
|
||||||
iconGroup = savedInstanceState.getParcelable(KEY_ICON)
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
if (containsKey(KEY_ACTION_ID))
|
if (containsKey(KEY_ACTION_ID))
|
||||||
editGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID))
|
mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID))
|
||||||
|
if (containsKey(KEY_GROUP_INFO)) {
|
||||||
if (containsKey(KEY_NAME) && containsKey(KEY_ICON)) {
|
mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
||||||
nameGroup = getString(KEY_NAME)
|
|
||||||
iconGroup = getParcelable(KEY_ICON)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// populate the name
|
// populate info in views
|
||||||
nameTextView?.text = nameGroup
|
populateInfoToViews(mGroupInfo)
|
||||||
// populate the icon
|
|
||||||
assignIconView()
|
iconButtonView.setOnClickListener { _ ->
|
||||||
|
mGroupEditViewModel.requestIconSelection(mGroupInfo.icon)
|
||||||
|
}
|
||||||
|
expirationView.setOnDateClickListener = { dateInstant ->
|
||||||
|
mGroupEditViewModel.requestDateTimeSelection(dateInstant)
|
||||||
|
}
|
||||||
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
builder.setView(root)
|
builder.setView(root)
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
editGroupListener?.cancelEditGroup(
|
// Do nothing
|
||||||
editGroupDialogAction,
|
|
||||||
nameTextView?.text?.toString(),
|
|
||||||
iconGroup)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
iconButtonView?.setOnClickListener { _ ->
|
|
||||||
IconPickerDialogFragment().show(parentFragmentManager, "IconPickerDialogFragment")
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.create()
|
return builder.create()
|
||||||
}
|
}
|
||||||
return super.onCreateDialog(savedInstanceState)
|
return super.onCreateDialog(savedInstanceState)
|
||||||
@@ -146,73 +207,116 @@ class GroupEditDialogFragment : DialogFragment(), IconPickerDialogFragment.IconP
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
// To prevent auto dismiss
|
// To prevent auto dismiss
|
||||||
val d = dialog as AlertDialog?
|
val alertDialog = dialog as AlertDialog?
|
||||||
if (d != null) {
|
if (alertDialog != null) {
|
||||||
val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
|
val positiveButton = alertDialog.getButton(Dialog.BUTTON_POSITIVE) as Button
|
||||||
positiveButton.setOnClickListener {
|
positiveButton.setOnClickListener {
|
||||||
|
retrieveGroupInfoFromViews()
|
||||||
if (isValid()) {
|
if (isValid()) {
|
||||||
editGroupListener?.approveEditGroup(
|
when (mEditGroupDialogAction) {
|
||||||
editGroupDialogAction,
|
CREATION ->
|
||||||
nameTextView?.text?.toString(),
|
mGroupEditViewModel.approveGroupCreation(mGroupInfo)
|
||||||
iconGroup)
|
UPDATE ->
|
||||||
d.dismiss()
|
mGroupEditViewModel.approveGroupUpdate(mGroupInfo)
|
||||||
|
NONE -> {}
|
||||||
|
}
|
||||||
|
alertDialog.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assignIconView() {
|
private fun populateInfoToViews(groupInfo: GroupInfo) {
|
||||||
if (mDatabase?.drawFactory != null && iconGroup != null) {
|
mGroupEditViewModel.selectIcon(groupInfo.icon)
|
||||||
iconButtonView?.assignDatabaseIcon(mDatabase?.drawFactory!!, iconGroup!!, iconColor)
|
nameTextView.text = groupInfo.title
|
||||||
|
notesTextLayoutView.visibility = if (groupInfo.notes == null) View.GONE else View.VISIBLE
|
||||||
|
groupInfo.notes?.let {
|
||||||
|
notesTextView.text = it
|
||||||
|
}
|
||||||
|
expirationView.activation = groupInfo.expires
|
||||||
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun iconPicked(bundle: Bundle) {
|
private fun retrieveGroupInfoFromViews() {
|
||||||
iconGroup = IconPickerDialogFragment.getIconStandardFromBundle(bundle)
|
mGroupInfo.title = nameTextView.text.toString()
|
||||||
assignIconView()
|
// Only if there
|
||||||
|
val newNotes = notesTextView.text.toString()
|
||||||
|
if (newNotes.isNotEmpty()) {
|
||||||
|
mGroupInfo.notes = newNotes
|
||||||
|
}
|
||||||
|
mGroupInfo.expires = expirationView.activation
|
||||||
|
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) {
|
||||||
outState.putInt(KEY_ACTION_ID, editGroupDialogAction!!.ordinal)
|
retrieveGroupInfoFromViews()
|
||||||
outState.putString(KEY_NAME, nameGroup)
|
outState.putInt(KEY_ACTION_ID, mEditGroupDialogAction.ordinal)
|
||||||
outState.putParcelable(KEY_ICON, iconGroup)
|
outState.putParcelable(KEY_GROUP_INFO, mGroupInfo)
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isValid(): Boolean {
|
private fun isValid(): Boolean {
|
||||||
if (nameTextView?.text?.toString()?.isNotEmpty() != true) {
|
val name = nameTextView.text.toString()
|
||||||
nameTextLayoutView?.error = getString(R.string.error_no_name)
|
val error = when {
|
||||||
return false
|
name.isEmpty() -> {
|
||||||
|
Error(true, R.string.error_no_name)
|
||||||
|
}
|
||||||
|
mGroupNamesNotAllowed == null -> {
|
||||||
|
Error(true, R.string.error_word_reserved)
|
||||||
|
}
|
||||||
|
mGroupNamesNotAllowed?.find { it.equals(name, ignoreCase = true) } != null -> {
|
||||||
|
Error(true, R.string.error_word_reserved)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Error(false, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true
|
error.messageId?.let { messageId ->
|
||||||
|
nameTextLayoutView.error = getString(messageId)
|
||||||
|
} ?: kotlin.run {
|
||||||
|
nameTextLayoutView.error = null
|
||||||
|
}
|
||||||
|
return !error.isError
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EditGroupListener {
|
data class Error(val isError: Boolean, val messageId: Int?)
|
||||||
fun approveEditGroup(action: EditGroupDialogAction?, name: String?, icon: IconImage?)
|
|
||||||
fun cancelEditGroup(action: EditGroupDialogAction?, name: String?, icon: IconImage?)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
|
const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP"
|
||||||
|
private const val KEY_ACTION_ID = "KEY_ACTION_ID"
|
||||||
|
private const val KEY_GROUP_INFO = "KEY_GROUP_INFO"
|
||||||
|
|
||||||
const val KEY_NAME = "KEY_NAME"
|
fun create(groupInfo: GroupInfo): GroupEditDialogFragment {
|
||||||
const val KEY_ICON = "KEY_ICON"
|
|
||||||
const val KEY_ACTION_ID = "KEY_ACTION_ID"
|
|
||||||
|
|
||||||
fun build(): GroupEditDialogFragment {
|
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
bundle.putInt(KEY_ACTION_ID, CREATION.ordinal)
|
bundle.putInt(KEY_ACTION_ID, CREATION.ordinal)
|
||||||
|
bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
|
||||||
val fragment = GroupEditDialogFragment()
|
val fragment = GroupEditDialogFragment()
|
||||||
fragment.arguments = bundle
|
fragment.arguments = bundle
|
||||||
return fragment
|
return fragment
|
||||||
}
|
}
|
||||||
|
|
||||||
fun build(group: Group): GroupEditDialogFragment {
|
fun update(groupInfo: GroupInfo): GroupEditDialogFragment {
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
bundle.putString(KEY_NAME, group.title)
|
|
||||||
bundle.putParcelable(KEY_ICON, group.icon)
|
|
||||||
bundle.putInt(KEY_ACTION_ID, UPDATE.ordinal)
|
bundle.putInt(KEY_ACTION_ID, UPDATE.ordinal)
|
||||||
|
bundle.putParcelable(KEY_GROUP_INFO, groupInfo)
|
||||||
val fragment = GroupEditDialogFragment()
|
val fragment = GroupEditDialogFragment()
|
||||||
fragment.arguments = bundle
|
fragment.arguments = bundle
|
||||||
return fragment
|
return fragment
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.res.ColorStateList
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.BaseAdapter
|
|
||||||
import android.widget.GridView
|
|
||||||
import android.widget.ImageView
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.widget.ImageViewCompat
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
|
||||||
import com.kunzisoft.keepass.icons.IconPack
|
|
||||||
import com.kunzisoft.keepass.icons.IconPackChooser
|
|
||||||
|
|
||||||
|
|
||||||
class IconPickerDialogFragment : DialogFragment() {
|
|
||||||
|
|
||||||
private var iconPickerListener: IconPickerListener? = null
|
|
||||||
private var iconPack: IconPack? = null
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
try {
|
|
||||||
iconPickerListener = context as IconPickerListener
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
// The activity doesn't implement the interface, throw exception
|
|
||||||
throw ClassCastException(context.toString()
|
|
||||||
+ " must implement " + IconPickerListener::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetach() {
|
|
||||||
iconPickerListener = null
|
|
||||||
super.onDetach()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
activity?.let { activity ->
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
|
||||||
|
|
||||||
iconPack = IconPackChooser.getSelectedIconPack(requireContext())
|
|
||||||
|
|
||||||
// Inflate and set the layout for the dialog
|
|
||||||
// Pass null as the parent view because its going in the dialog layout
|
|
||||||
val root = activity.layoutInflater.inflate(R.layout.fragment_icon_picker, null)
|
|
||||||
builder.setView(root)
|
|
||||||
|
|
||||||
val currIconGridView = root.findViewById<GridView>(R.id.IconGridView)
|
|
||||||
currIconGridView.adapter = ImageAdapter(activity)
|
|
||||||
|
|
||||||
currIconGridView.setOnItemClickListener { _, _, position, _ ->
|
|
||||||
val bundle = Bundle()
|
|
||||||
bundle.putParcelable(KEY_ICON_STANDARD, IconImageStandard(position))
|
|
||||||
iconPickerListener?.iconPicked(bundle)
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> this@IconPickerDialogFragment.dialog?.cancel() }
|
|
||||||
|
|
||||||
return builder.create()
|
|
||||||
}
|
|
||||||
return super.onCreateDialog(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ImageAdapter internal constructor(private val context: Context) : BaseAdapter() {
|
|
||||||
|
|
||||||
override fun getCount(): Int {
|
|
||||||
return iconPack?.numberOfIcons() ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItem(position: Int): Any? {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
|
||||||
val currentView: View = convertView
|
|
||||||
?: (context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)
|
|
||||||
.inflate(R.layout.item_icon, parent, false)
|
|
||||||
|
|
||||||
iconPack?.let { iconPack ->
|
|
||||||
val iconImageView = currentView.findViewById<ImageView>(R.id.icon_image)
|
|
||||||
iconImageView.setImageResource(iconPack.iconToResId(position))
|
|
||||||
|
|
||||||
// Assign color if icons are tintable
|
|
||||||
if (iconPack.tintable()) {
|
|
||||||
// Retrieve the textColor to tint the icon
|
|
||||||
val ta = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
|
||||||
ImageViewCompat.setImageTintList(iconImageView, ColorStateList.valueOf(ta.getColor(0, Color.BLACK)))
|
|
||||||
ta.recycle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentView
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IconPickerListener {
|
|
||||||
fun iconPicked(bundle: Bundle)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val KEY_ICON_STANDARD = "KEY_ICON_STANDARD"
|
|
||||||
|
|
||||||
fun getIconStandardFromBundle(bundle: Bundle): IconImageStandard? {
|
|
||||||
return bundle.getParcelable(KEY_ICON_STANDARD)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun launch(activity: FragmentActivity) {
|
|
||||||
// Create an instance of the dialog fragment and show it
|
|
||||||
val dialog = IconPickerDialogFragment()
|
|
||||||
dialog.show(activity.supportFragmentManager, "IconPickerDialogFragment")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,13 +19,14 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
import android.app.AlertDialog
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
|
|
||||||
class PasswordEncodingDialogFragment : DialogFragment() {
|
class PasswordEncodingDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
@@ -49,10 +50,7 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
|||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
|
||||||
val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY)
|
val databaseUri: Uri? = savedInstanceState?.getParcelable(DATABASE_URI_KEY)
|
||||||
val masterPasswordChecked: Boolean = savedInstanceState?.getBoolean(MASTER_PASSWORD_CHECKED_KEY) ?: false
|
val mainCredential: MainCredential = savedInstanceState?.getParcelable(MAIN_CREDENTIAL) ?: MainCredential()
|
||||||
val masterPassword: String? = savedInstanceState?.getString(MASTER_PASSWORD_KEY)
|
|
||||||
val keyFileChecked: Boolean = savedInstanceState?.getBoolean(KEY_FILE_CHECKED_KEY) ?: false
|
|
||||||
val keyFile: Uri? = savedInstanceState?.getParcelable(KEY_FILE_URI_KEY)
|
|
||||||
|
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
@@ -60,10 +58,7 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
|||||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
mListener?.onPasswordEncodingValidateListener(
|
mListener?.onPasswordEncodingValidateListener(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
masterPasswordChecked,
|
mainCredential
|
||||||
masterPassword,
|
|
||||||
keyFileChecked,
|
|
||||||
keyFile
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
|
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() }
|
||||||
@@ -75,32 +70,20 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
fun onPasswordEncodingValidateListener(databaseUri: Uri?,
|
fun onPasswordEncodingValidateListener(databaseUri: Uri?,
|
||||||
masterPasswordChecked: Boolean,
|
mainCredential: MainCredential)
|
||||||
masterPassword: String?,
|
|
||||||
keyFileChecked: Boolean,
|
|
||||||
keyFile: Uri?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
|
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
|
||||||
private const val MASTER_PASSWORD_CHECKED_KEY = "MASTER_PASSWORD_CHECKED_KEY"
|
private const val MAIN_CREDENTIAL = "MAIN_CREDENTIAL"
|
||||||
private const val MASTER_PASSWORD_KEY = "MASTER_PASSWORD_KEY"
|
|
||||||
private const val KEY_FILE_CHECKED_KEY = "KEY_FILE_CHECKED_KEY"
|
|
||||||
private const val KEY_FILE_URI_KEY = "KEY_FILE_URI_KEY"
|
|
||||||
|
|
||||||
fun getInstance(databaseUri: Uri,
|
fun getInstance(databaseUri: Uri,
|
||||||
masterPasswordChecked: Boolean,
|
mainCredential: MainCredential): SortDialogFragment {
|
||||||
masterPassword: String?,
|
|
||||||
keyFileChecked: Boolean,
|
|
||||||
keyFile: Uri?): SortDialogFragment {
|
|
||||||
val fragment = SortDialogFragment()
|
val fragment = SortDialogFragment()
|
||||||
fragment.arguments = Bundle().apply {
|
fragment.arguments = Bundle().apply {
|
||||||
putParcelable(DATABASE_URI_KEY, databaseUri)
|
putParcelable(DATABASE_URI_KEY, databaseUri)
|
||||||
putBoolean(MASTER_PASSWORD_CHECKED_KEY, masterPasswordChecked)
|
putParcelable(MAIN_CREDENTIAL, mainCredential)
|
||||||
putString(MASTER_PASSWORD_KEY, masterPassword)
|
|
||||||
putBoolean(KEY_FILE_CHECKED_KEY, keyFileChecked)
|
|
||||||
putParcelable(KEY_FILE_URI_KEY, keyFile)
|
|
||||||
}
|
}
|
||||||
return fragment
|
return fragment
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,14 +25,13 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Dialog to confirm big file to upload
|
* Custom Dialog to confirm big file to upload
|
||||||
*/
|
*/
|
||||||
class ReplaceFileDialogFragment : DialogFragment() {
|
class ReplaceFileDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
private var mActionChooseListener: ActionChooseListener? = null
|
private var mActionChooseListener: ActionChooseListener? = null
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -30,17 +29,20 @@ import android.text.SpannableStringBuilder
|
|||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
|
import com.kunzisoft.keepass.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 : DialogFragment() {
|
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 : DialogFragment() {
|
|||||||
|
|
||||||
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 mSelectFileHelper: SelectFileHelper? = 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,20 +77,18 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AssignPasswordDialogListener {
|
interface AssignMainCredentialDialogListener {
|
||||||
fun onAssignKeyDialogPositiveClick(masterPasswordChecked: Boolean, masterPassword: String?,
|
fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential)
|
||||||
keyFileChecked: Boolean, keyFile: Uri?)
|
fun onAssignKeyDialogNegativeClick(mainCredential: MainCredential)
|
||||||
fun onAssignKeyDialogNegativeClick(masterPasswordChecked: Boolean, masterPassword: String?,
|
|
||||||
keyFileChecked: Boolean, keyFile: Uri?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttach(activity: Context) {
|
override fun onAttach(activity: Context) {
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +103,13 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
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 ->
|
||||||
|
|
||||||
@@ -115,31 +122,39 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
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) { _, _ -> }
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
|
||||||
val credentialsInfo: ImageView? = rootView?.findViewById(R.id.credentials_information)
|
rootView?.findViewById<View>(R.id.credentials_information)?.setOnClickListener {
|
||||||
credentialsInfo?.setOnClickListener {
|
|
||||||
UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
|
UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
mSelectFileHelper = SelectFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
keyFileSelectionView?.apply {
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
setOnClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
uri?.let { pathUri ->
|
||||||
setOnLongClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
|
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
||||||
|
keyFileSelectionView?.error = null
|
||||||
|
keyFileCheckBox?.isChecked = true
|
||||||
|
keyFileSelectionView?.uri = pathUri
|
||||||
|
if (lengthFile <= 0L) {
|
||||||
|
showEmptyKeyFileConfirmationDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
|
|
||||||
val dialog = builder.create()
|
val dialog = builder.create()
|
||||||
|
|
||||||
@@ -157,21 +172,17 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
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) {
|
||||||
mListener?.onAssignKeyDialogPositiveClick(
|
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
|
||||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
|
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
|
||||||
negativeButton.setOnClickListener {
|
negativeButton.setOnClickListener {
|
||||||
mListener?.onAssignKeyDialogNegativeClick(
|
mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
|
||||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
|
||||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,26 +194,32 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
return super.onCreateDialog(savedInstanceState)
|
return super.onCreateDialog(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun retrieveMainCredential(): MainCredential {
|
||||||
|
val masterPassword = if (passwordCheckBox!!.isChecked) mMasterPassword else null
|
||||||
|
val keyFile = if (keyFileCheckBox!!.isChecked) mKeyFile else null
|
||||||
|
return MainCredential(masterPassword, keyFile)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
// 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
|
||||||
@@ -212,7 +229,11 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
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()
|
||||||
}
|
}
|
||||||
@@ -242,10 +263,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
builder.setMessage(R.string.warning_empty_password)
|
builder.setMessage(R.string.warning_empty_password)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
if (!verifyKeyFile()) {
|
if (!verifyKeyFile()) {
|
||||||
mListener?.onAssignKeyDialogPositiveClick(
|
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
this@SetMainCredentialDialogFragment.dismiss()
|
||||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
|
||||||
this@AssignMasterKeyDialogFragment.dismiss()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
@@ -259,10 +278,8 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
val builder = AlertDialog.Builder(it)
|
val builder = AlertDialog.Builder(it)
|
||||||
builder.setMessage(R.string.warning_no_encryption_key)
|
builder.setMessage(R.string.warning_no_encryption_key)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
mListener?.onAssignKeyDialogPositiveClick(
|
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||||
passwordCheckBox!!.isChecked, mMasterPassword,
|
this@SetMainCredentialDialogFragment.dismiss()
|
||||||
keyFileCheckBox!!.isChecked, mKeyFile)
|
|
||||||
this@AssignMasterKeyDialogFragment.dismiss()
|
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
mNoKeyConfirmationDialog = builder.create()
|
mNoKeyConfirmationDialog = builder.create()
|
||||||
@@ -290,29 +307,12 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
|
|
||||||
mSelectFileHelper?.onActivityResultCallback(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
|
||||||
@@ -29,12 +29,8 @@ import android.view.MotionEvent
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.AdapterView
|
import android.widget.*
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.Spinner
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.BuildConfig
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
@@ -49,14 +45,16 @@ import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
|
|||||||
import com.kunzisoft.keepass.otp.OtpTokenType
|
import com.kunzisoft.keepass.otp.OtpTokenType
|
||||||
import com.kunzisoft.keepass.otp.OtpType
|
import com.kunzisoft.keepass.otp.OtpType
|
||||||
import com.kunzisoft.keepass.otp.TokenCalculator
|
import com.kunzisoft.keepass.otp.TokenCalculator
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class SetOTPDialogFragment : DialogFragment() {
|
class SetOTPDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
private var mCreateOTPElementListener: CreateOtpListener? = null
|
private var mCreateOTPElementListener: CreateOtpListener? = null
|
||||||
|
|
||||||
private var mOtpElement: OtpElement = OtpElement()
|
private var mOtpElement: OtpElement = OtpElement()
|
||||||
|
|
||||||
|
private var otpTypeMessage: TextView? = null
|
||||||
private var otpTypeSpinner: Spinner? = null
|
private var otpTypeSpinner: Spinner? = null
|
||||||
private var otpTokenTypeSpinner: Spinner? = null
|
private var otpTokenTypeSpinner: Spinner? = null
|
||||||
private var otpSecretContainer: TextInputLayout? = null
|
private var otpSecretContainer: TextInputLayout? = null
|
||||||
@@ -74,16 +72,22 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
private var totpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
private var totpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
||||||
private var hotpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
private var hotpTokenTypeAdapter: ArrayAdapter<OtpTokenType>? = null
|
||||||
private var otpAlgorithmAdapter: ArrayAdapter<TokenCalculator.HashAlgorithm>? = null
|
private var otpAlgorithmAdapter: ArrayAdapter<TokenCalculator.HashAlgorithm>? = null
|
||||||
|
private var mHotpTokenTypeArray: Array<OtpTokenType>? = null
|
||||||
|
private var mTotpTokenTypeArray: Array<OtpTokenType>? = null
|
||||||
|
|
||||||
private var mManualEvent = false
|
private var mManualEvent = false
|
||||||
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
|
private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus ->
|
||||||
if (!isFocus)
|
if (!isFocus)
|
||||||
mManualEvent = true
|
mManualEvent = true
|
||||||
|
else
|
||||||
|
resetAppTimeout()
|
||||||
}
|
}
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
private var mOnTouchListener = View.OnTouchListener { _, event ->
|
private var mOnTouchListener = View.OnTouchListener { _, event ->
|
||||||
when (event.action) {
|
when (event.action) {
|
||||||
MotionEvent.ACTION_DOWN -> {
|
MotionEvent.ACTION_DOWN -> {
|
||||||
mManualEvent = true
|
mManualEvent = true
|
||||||
|
resetAppTimeout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
@@ -94,6 +98,10 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
private var mPeriodWellFormed = false
|
private var mPeriodWellFormed = false
|
||||||
private var mDigitsWellFormed = false
|
private var mDigitsWellFormed = false
|
||||||
|
|
||||||
|
override fun overrideTimeoutTouchAndFocusEvents(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
// Verify that the host activity implements the callback interface
|
// Verify that the host activity implements the callback interface
|
||||||
@@ -134,6 +142,7 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val root = activity.layoutInflater.inflate(R.layout.fragment_set_otp, null) as ViewGroup?
|
val root = activity.layoutInflater.inflate(R.layout.fragment_set_otp, null) as ViewGroup?
|
||||||
|
otpTypeMessage = root?.findViewById(R.id.setup_otp_type_message)
|
||||||
otpTypeSpinner = root?.findViewById(R.id.setup_otp_type)
|
otpTypeSpinner = root?.findViewById(R.id.setup_otp_type)
|
||||||
otpTokenTypeSpinner = root?.findViewById(R.id.setup_otp_token_type)
|
otpTokenTypeSpinner = root?.findViewById(R.id.setup_otp_token_type)
|
||||||
otpSecretContainer = root?.findViewById(R.id.setup_otp_secret_label)
|
otpSecretContainer = root?.findViewById(R.id.setup_otp_secret_label)
|
||||||
@@ -183,23 +192,24 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
// HOTP / TOTP Type selection
|
// HOTP / TOTP Type selection
|
||||||
val otpTypeArray = OtpType.values()
|
val otpTypeArray = OtpType.values()
|
||||||
otpTypeAdapter = ArrayAdapter<OtpType>(activity,
|
otpTypeAdapter = ArrayAdapter(activity,
|
||||||
android.R.layout.simple_spinner_item, otpTypeArray).apply {
|
android.R.layout.simple_spinner_item, otpTypeArray).apply {
|
||||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
}
|
}
|
||||||
otpTypeSpinner?.adapter = otpTypeAdapter
|
otpTypeSpinner?.adapter = otpTypeAdapter
|
||||||
|
|
||||||
// Otp Token type selection
|
// Otp Token type selection
|
||||||
val hotpTokenTypeArray = OtpTokenType.getHotpTokenTypeValues()
|
mHotpTokenTypeArray = OtpTokenType.getHotpTokenTypeValues()
|
||||||
hotpTokenTypeAdapter = ArrayAdapter(activity,
|
hotpTokenTypeAdapter = ArrayAdapter(activity,
|
||||||
android.R.layout.simple_spinner_item, hotpTokenTypeArray).apply {
|
android.R.layout.simple_spinner_item, mHotpTokenTypeArray!!).apply {
|
||||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
}
|
}
|
||||||
// Proprietary only on closed and full version
|
// Proprietary only on full version
|
||||||
val totpTokenTypeArray = 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, totpTokenTypeArray).apply {
|
android.R.layout.simple_spinner_item, mTotpTokenTypeArray!!).apply {
|
||||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
}
|
}
|
||||||
otpTokenTypeAdapter = hotpTokenTypeAdapter
|
otpTokenTypeAdapter = hotpTokenTypeAdapter
|
||||||
@@ -207,7 +217,7 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
// OTP Algorithm
|
// OTP Algorithm
|
||||||
val otpAlgorithmArray = TokenCalculator.HashAlgorithm.values()
|
val otpAlgorithmArray = TokenCalculator.HashAlgorithm.values()
|
||||||
otpAlgorithmAdapter = ArrayAdapter<TokenCalculator.HashAlgorithm>(activity,
|
otpAlgorithmAdapter = ArrayAdapter(activity,
|
||||||
android.R.layout.simple_spinner_item, otpAlgorithmArray).apply {
|
android.R.layout.simple_spinner_item, otpAlgorithmArray).apply {
|
||||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
}
|
}
|
||||||
@@ -222,11 +232,17 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
builder.apply {
|
builder.apply {
|
||||||
setTitle(R.string.entry_setup_otp)
|
|
||||||
setView(root)
|
setView(root)
|
||||||
.setPositiveButton(android.R.string.ok) {_, _ -> }
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
resetAppTimeout()
|
||||||
}
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
resetAppTimeout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root?.findViewById<View>(R.id.otp_information)?.setOnClickListener {
|
||||||
|
UriUtil.gotoUrl(activity, R.string.otp_explanation_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.create()
|
return builder.create()
|
||||||
@@ -294,7 +310,7 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
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)
|
||||||
@@ -372,24 +388,40 @@ class SetOTPDialogFragment : DialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun upgradeTokenType() {
|
private fun upgradeTokenType() {
|
||||||
|
val tokenType = mOtpElement.tokenType
|
||||||
when (mOtpElement.type) {
|
when (mOtpElement.type) {
|
||||||
OtpType.HOTP -> {
|
OtpType.HOTP -> {
|
||||||
otpPeriodContainer?.visibility = View.GONE
|
otpPeriodContainer?.visibility = View.GONE
|
||||||
otpCounterContainer?.visibility = View.VISIBLE
|
otpCounterContainer?.visibility = View.VISIBLE
|
||||||
otpTokenTypeSpinner?.adapter = hotpTokenTypeAdapter
|
otpTokenTypeSpinner?.adapter = hotpTokenTypeAdapter
|
||||||
otpTokenTypeSpinner?.setSelection(OtpTokenType
|
mHotpTokenTypeArray?.let { otpTokenTypeArray ->
|
||||||
.getHotpTokenTypeValues().indexOf(mOtpElement.tokenType))
|
defineOtpTokenTypeSpinner(otpTokenTypeArray, tokenType, OtpTokenType.RFC4226)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
OtpType.TOTP -> {
|
OtpType.TOTP -> {
|
||||||
otpPeriodContainer?.visibility = View.VISIBLE
|
otpPeriodContainer?.visibility = View.VISIBLE
|
||||||
otpCounterContainer?.visibility = View.GONE
|
otpCounterContainer?.visibility = View.GONE
|
||||||
otpTokenTypeSpinner?.adapter = totpTokenTypeAdapter
|
otpTokenTypeSpinner?.adapter = totpTokenTypeAdapter
|
||||||
otpTokenTypeSpinner?.setSelection(OtpTokenType
|
mTotpTokenTypeArray?.let { otpTokenTypeArray ->
|
||||||
.getTotpTokenTypeValues().indexOf(mOtpElement.tokenType))
|
defineOtpTokenTypeSpinner(otpTokenTypeArray, tokenType, OtpTokenType.RFC6238)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun defineOtpTokenTypeSpinner(otpTokenTypeArray: Array<OtpTokenType>,
|
||||||
|
tokenType: OtpTokenType,
|
||||||
|
defaultTokenType: OtpTokenType) {
|
||||||
|
val formTokenType = if (otpTokenTypeArray.contains(tokenType)) {
|
||||||
|
otpTypeMessage?.visibility = View.GONE
|
||||||
|
tokenType
|
||||||
|
} else {
|
||||||
|
otpTypeMessage?.visibility = View.VISIBLE
|
||||||
|
defaultTokenType
|
||||||
|
}
|
||||||
|
otpTokenTypeSpinner?.setSelection(otpTokenTypeArray.indexOf(formTokenType))
|
||||||
|
}
|
||||||
|
|
||||||
private fun upgradeParameters() {
|
private fun upgradeParameters() {
|
||||||
otpAlgorithmSpinner?.setSelection(TokenCalculator.HashAlgorithm.values()
|
otpAlgorithmSpinner?.setSelection(TokenCalculator.HashAlgorithm.values()
|
||||||
.indexOf(mOtpElement.algorithm))
|
.indexOf(mOtpElement.algorithm))
|
||||||
|
|||||||
@@ -22,16 +22,15 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.annotation.IdRes
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import android.widget.RadioGroup
|
import android.widget.RadioGroup
|
||||||
|
import androidx.annotation.IdRes
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||||
|
|
||||||
class SortDialogFragment : DialogFragment() {
|
class SortDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
private var mListener: SortSelectionListener? = null
|
private var mListener: SortSelectionListener? = null
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.os.Bundle
|
|||||||
import android.text.format.DateFormat
|
import android.text.format.DateFormat
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
|
|
||||||
|
// Not as DatabaseDialogFragment because crash on KitKat
|
||||||
class TimePickerFragment : DialogFragment() {
|
class TimePickerFragment : DialogFragment() {
|
||||||
|
|
||||||
private var defaultHour: Int = 0
|
private var defaultHour: Int = 0
|
||||||
|
|||||||
@@ -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(" ")
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
|
||||||
|
abstract class DatabaseFragment : StylishFragment(), DatabaseRetrieval {
|
||||||
|
|
||||||
|
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||||
|
protected var mDatabase: Database? = null
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
mDatabaseViewModel.database.observe(viewLifecycleOwner) { database ->
|
||||||
|
if (mDatabase == null || mDatabase != database) {
|
||||||
|
this.mDatabase = database
|
||||||
|
onDatabaseRetrieved(database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) { result ->
|
||||||
|
onDatabaseActionFinished(result.database, result.actionTask, result.result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) {
|
||||||
|
context?.let {
|
||||||
|
view?.resetAppTimeoutWhenViewTouchedOrFocused(it, mDatabase?.loaded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseActionFinished(
|
||||||
|
database: Database,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
// Can be overridden by a subclass
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun buildNewBinaryAttachment(): BinaryData? {
|
||||||
|
return mDatabase?.buildNewBinaryAttachment()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,404 @@
|
|||||||
|
/*
|
||||||
|
* 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.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
||||||
|
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.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.template.Template
|
||||||
|
import com.kunzisoft.keepass.model.AttachmentState
|
||||||
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
|
import com.kunzisoft.keepass.view.*
|
||||||
|
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
||||||
|
import com.tokenautocomplete.FilteredArrayAdapter
|
||||||
|
|
||||||
|
|
||||||
|
class EntryEditFragment: DatabaseFragment() {
|
||||||
|
|
||||||
|
private val mEntryEditViewModel: EntryEditViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private lateinit var rootView: View
|
||||||
|
private lateinit var templateView: TemplateEditView
|
||||||
|
private lateinit var attachmentsContainerView: ViewGroup
|
||||||
|
private lateinit var attachmentsListView: RecyclerView
|
||||||
|
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 mAllowMultipleAttachments: Boolean = false
|
||||||
|
|
||||||
|
private var mIconColor: Int = 0
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?): View? {
|
||||||
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
|
// Retrieve the textColor to tint the icon
|
||||||
|
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
|
mIconColor = taIconColor?.getColor(0, Color.BLACK) ?: Color.BLACK
|
||||||
|
taIconColor?.recycle()
|
||||||
|
|
||||||
|
return inflater.cloneInContext(contextThemed)
|
||||||
|
.inflate(R.layout.fragment_entry_edit, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View,
|
||||||
|
savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
rootView = view
|
||||||
|
// Hide only the first time
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
view.isVisible = false
|
||||||
|
}
|
||||||
|
templateView = view.findViewById(R.id.template_view)
|
||||||
|
attachmentsContainerView = view.findViewById(R.id.entry_attachments_container)
|
||||||
|
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())
|
||||||
|
attachmentsListView.apply {
|
||||||
|
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||||
|
adapter = attachmentsAdapter
|
||||||
|
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
|
}
|
||||||
|
|
||||||
|
templateView.apply {
|
||||||
|
setOnIconClickListener {
|
||||||
|
mEntryEditViewModel.requestIconSelection(templateView.getIcon())
|
||||||
|
}
|
||||||
|
setOnBackgroundColorClickListener {
|
||||||
|
mEntryEditViewModel.requestBackgroundColorSelection(templateView.getBackgroundColor())
|
||||||
|
}
|
||||||
|
setOnForegroundColorClickListener {
|
||||||
|
mEntryEditViewModel.requestForegroundColorSelection(templateView.getForegroundColor())
|
||||||
|
}
|
||||||
|
setOnCustomEditionActionClickListener { field ->
|
||||||
|
mEntryEditViewModel.requestCustomFieldEdition(field)
|
||||||
|
}
|
||||||
|
setOnPasswordGenerationActionClickListener { field ->
|
||||||
|
mEntryEditViewModel.requestPasswordSelection(field)
|
||||||
|
}
|
||||||
|
setOnDateInstantClickListener { dateInstant ->
|
||||||
|
mEntryEditViewModel.requestDateTimeSelection(dateInstant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
val attachments: List<Attachment> =
|
||||||
|
savedInstanceState.getParcelableArrayList(ATTACHMENTS_TAG) ?: listOf()
|
||||||
|
setAttachments(attachments)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onTemplateChanged.observe(viewLifecycleOwner) { template ->
|
||||||
|
this.mTemplate = template
|
||||||
|
templateView.setTemplate(template)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.templatesEntry.observe(viewLifecycleOwner) { templateEntry ->
|
||||||
|
if (templateEntry != null) {
|
||||||
|
val selectedTemplate = if (mTemplate != null)
|
||||||
|
mTemplate
|
||||||
|
else
|
||||||
|
templateEntry.defaultTemplate
|
||||||
|
templateView.setTemplate(selectedTemplate)
|
||||||
|
// Load entry info only the first time to keep change locally
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
assignEntryInfo(templateEntry.entryInfo)
|
||||||
|
}
|
||||||
|
// To prevent flickering
|
||||||
|
rootView.showByFading()
|
||||||
|
// Apply timeout reset
|
||||||
|
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.requestEntryInfoUpdate.observe(viewLifecycleOwner) {
|
||||||
|
val entryInfo = retrieveEntryInfo()
|
||||||
|
mEntryEditViewModel.saveEntryInfo(it.database, it.entry, it.parent, entryInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onIconSelected.observe(viewLifecycleOwner) { 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 ->
|
||||||
|
templateView.setPasswordField(passwordField)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onDateSelected.observe(viewLifecycleOwner) { viewModelDate ->
|
||||||
|
// Save the date
|
||||||
|
templateView.setCurrentDateTimeValue(viewModelDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onTimeSelected.observe(viewLifecycleOwner) { viewModelTime ->
|
||||||
|
// Save the time
|
||||||
|
templateView.setCurrentTimeValue(viewModelTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onCustomFieldEdited.observe(viewLifecycleOwner) { fieldAction ->
|
||||||
|
val oldField = fieldAction.oldField
|
||||||
|
val newField = fieldAction.newField
|
||||||
|
// Field to add
|
||||||
|
if (oldField == null) {
|
||||||
|
newField?.let {
|
||||||
|
if (!templateView.putCustomField(it)) {
|
||||||
|
mEntryEditViewModel.showCustomFieldEditionError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Field to replace
|
||||||
|
oldField?.let {
|
||||||
|
newField?.let {
|
||||||
|
if (!templateView.replaceCustomField(oldField, newField)) {
|
||||||
|
mEntryEditViewModel.showCustomFieldEditionError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Field to remove
|
||||||
|
if (newField == null) {
|
||||||
|
oldField?.let {
|
||||||
|
templateView.removeCustomField(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.requestSetupOtp.observe(viewLifecycleOwner) {
|
||||||
|
// Retrieve the current otpElement if exists
|
||||||
|
// and open the dialog to set up the OTP
|
||||||
|
SetOTPDialogFragment.build(templateView.getEntryInfo().otpModel)
|
||||||
|
.show(parentFragmentManager, "addOTPDialog")
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onOtpCreated.observe(viewLifecycleOwner) {
|
||||||
|
// Update the otp field with otpauth:// url
|
||||||
|
templateView.putOtpElement(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onBuildNewAttachment.observe(viewLifecycleOwner) {
|
||||||
|
val attachmentToUploadUri = it.attachmentToUploadUri
|
||||||
|
val fileName = it.fileName
|
||||||
|
|
||||||
|
buildNewBinaryAttachment()?.let { binaryAttachment ->
|
||||||
|
val entryAttachment = Attachment(fileName, binaryAttachment)
|
||||||
|
// Ask to replace the current attachment
|
||||||
|
if ((!mAllowMultipleAttachments
|
||||||
|
&& containsAttachment()) ||
|
||||||
|
containsAttachment(EntryAttachmentState(entryAttachment, StreamDirection.UPLOAD))) {
|
||||||
|
ReplaceFileDialogFragment.build(attachmentToUploadUri, entryAttachment)
|
||||||
|
.show(parentFragmentManager, "replacementFileFragment")
|
||||||
|
} else {
|
||||||
|
mEntryEditViewModel.startUploadAttachment(attachmentToUploadUri, entryAttachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryEditViewModel.onAttachmentAction.observe(viewLifecycleOwner) { entryAttachmentState ->
|
||||||
|
when (entryAttachmentState?.downloadState) {
|
||||||
|
AttachmentState.START -> {
|
||||||
|
putAttachment(entryAttachmentState)
|
||||||
|
getAttachmentViewPosition(entryAttachmentState) { attachment, position ->
|
||||||
|
mEntryEditViewModel.binaryPreviewLoaded(attachment, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AttachmentState.IN_PROGRESS -> {
|
||||||
|
putAttachment(entryAttachmentState)
|
||||||
|
}
|
||||||
|
AttachmentState.COMPLETE -> {
|
||||||
|
putAttachment(entryAttachmentState) { entryAttachment ->
|
||||||
|
getAttachmentViewPosition(entryAttachment) { attachment, position ->
|
||||||
|
mEntryEditViewModel.binaryPreviewLoaded(attachment, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mEntryEditViewModel.onAttachmentAction(null)
|
||||||
|
}
|
||||||
|
AttachmentState.CANCELED,
|
||||||
|
AttachmentState.ERROR -> {
|
||||||
|
removeAttachment(entryAttachmentState)
|
||||||
|
mEntryEditViewModel.onAttachmentAction(null)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
|
||||||
|
templateView.populateIconMethod = { imageView, icon ->
|
||||||
|
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
mAllowMultipleAttachments = database?.allowMultipleAttachments == true
|
||||||
|
|
||||||
|
attachmentsAdapter?.database = database
|
||||||
|
attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize ->
|
||||||
|
if (previousSize > 0 && newSize == 0) {
|
||||||
|
attachmentsContainerView.collapse(true)
|
||||||
|
} else if (previousSize == 0 && newSize == 1) {
|
||||||
|
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?) {
|
||||||
|
// Populate entry views
|
||||||
|
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
|
||||||
|
setAttachments(entryInfo?.attachments ?: listOf())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun retrieveEntryInfo(): EntryInfo {
|
||||||
|
val entryInfo = templateView.getEntryInfo()
|
||||||
|
entryInfo.tags = tagsCompletionView.getTags()
|
||||||
|
entryInfo.attachments = getAttachments().toMutableList()
|
||||||
|
return entryInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------
|
||||||
|
* Attachments
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
private fun getAttachments(): List<Attachment> {
|
||||||
|
return attachmentsAdapter?.itemsList?.map { it.attachment } ?: listOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAttachments(attachments: List<Attachment>) {
|
||||||
|
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
attachmentsAdapter?.assignItems(attachments.map {
|
||||||
|
EntryAttachmentState(it, StreamDirection.UPLOAD)
|
||||||
|
})
|
||||||
|
attachmentsAdapter?.onDeleteButtonClickListener = { item ->
|
||||||
|
val attachment = item.attachment
|
||||||
|
removeAttachment(EntryAttachmentState(attachment, StreamDirection.DOWNLOAD))
|
||||||
|
mEntryEditViewModel.deleteAttachment(attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun containsAttachment(): Boolean {
|
||||||
|
return attachmentsAdapter?.isEmpty() != true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun containsAttachment(attachment: EntryAttachmentState): Boolean {
|
||||||
|
return attachmentsAdapter?.contains(attachment) ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun putAttachment(attachment: EntryAttachmentState,
|
||||||
|
onPreviewLoaded: ((attachment: EntryAttachmentState) -> Unit)? = null) {
|
||||||
|
// When only one attachment is allowed
|
||||||
|
if (!mAllowMultipleAttachments
|
||||||
|
&& attachment.downloadState == AttachmentState.START) {
|
||||||
|
attachmentsAdapter?.clear()
|
||||||
|
}
|
||||||
|
attachmentsContainerView.visibility = View.VISIBLE
|
||||||
|
attachmentsAdapter?.putItem(attachment)
|
||||||
|
attachmentsAdapter?.onBinaryPreviewLoaded = {
|
||||||
|
onPreviewLoaded?.invoke(attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeAttachment(attachment: EntryAttachmentState) {
|
||||||
|
attachmentsAdapter?.removeItem(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAttachmentViewPosition(attachment: EntryAttachmentState,
|
||||||
|
position: (attachment: EntryAttachmentState, Float) -> Unit) {
|
||||||
|
attachmentsListView.postDelayed({
|
||||||
|
attachmentsAdapter?.indexOf(attachment)?.let { index ->
|
||||||
|
position.invoke(attachment,
|
||||||
|
attachmentsContainerView.y
|
||||||
|
+ attachmentsListView.y
|
||||||
|
+ (attachmentsListView.getChildAt(index)?.y
|
||||||
|
?: 0F)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.putParcelableArrayList(ATTACHMENTS_TAG, ArrayList(getAttachments()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------
|
||||||
|
* Education
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun getActionImageView(): View? {
|
||||||
|
return templateView.getActionImageView()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchGeneratePasswordEductionAction() {
|
||||||
|
mEntryEditViewModel.requestPasswordSelection(templateView.getPasswordField())
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = EntryEditFragment::class.java.name
|
||||||
|
|
||||||
|
private const val ATTACHMENTS_TAG = "ATTACHMENTS_TAG"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
|
import com.kunzisoft.keepass.view.TemplateView
|
||||||
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
|
import com.kunzisoft.keepass.view.showByFading
|
||||||
|
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class EntryFragment: DatabaseFragment() {
|
||||||
|
|
||||||
|
private lateinit var rootView: View
|
||||||
|
private lateinit var mainSection: View
|
||||||
|
private lateinit var advancedSection: View
|
||||||
|
|
||||||
|
private lateinit var templateView: TemplateView
|
||||||
|
|
||||||
|
private lateinit var creationDateView: TextView
|
||||||
|
private lateinit var modificationDateView: TextView
|
||||||
|
|
||||||
|
private lateinit var attachmentsContainerView: View
|
||||||
|
private lateinit var attachmentsListView: RecyclerView
|
||||||
|
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
|
||||||
|
|
||||||
|
private lateinit var customDataView: TextView
|
||||||
|
|
||||||
|
private lateinit var uuidContainerView: View
|
||||||
|
private lateinit var uuidReferenceView: TextView
|
||||||
|
|
||||||
|
private var mClipboardHelper: ClipboardHelper? = null
|
||||||
|
|
||||||
|
private val mEntryViewModel: EntryViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?): View? {
|
||||||
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
|
return inflater.cloneInContext(contextThemed)
|
||||||
|
.inflate(R.layout.fragment_entry, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View,
|
||||||
|
savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
context?.let { context ->
|
||||||
|
mClipboardHelper = ClipboardHelper(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootView = view
|
||||||
|
// Hide only the first time
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
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)
|
||||||
|
loadTemplateSettings()
|
||||||
|
|
||||||
|
attachmentsContainerView = view.findViewById(R.id.entry_attachments_container)
|
||||||
|
attachmentsListView = view.findViewById(R.id.entry_attachments_list)
|
||||||
|
attachmentsListView.apply {
|
||||||
|
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||||
|
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||||
|
}
|
||||||
|
|
||||||
|
creationDateView = view.findViewById(R.id.entry_created)
|
||||||
|
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.apply {
|
||||||
|
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
uuidReferenceView = view.findViewById(R.id.entry_UUID_reference)
|
||||||
|
|
||||||
|
mEntryViewModel.entryInfoHistory.observe(viewLifecycleOwner) { entryInfoHistory ->
|
||||||
|
if (entryInfoHistory != null) {
|
||||||
|
templateView.setTemplate(entryInfoHistory.template)
|
||||||
|
assignEntryInfo(entryInfoHistory.entryInfo)
|
||||||
|
// Smooth appearing
|
||||||
|
rootView.showByFading()
|
||||||
|
resetAppTimeoutWhenViewFocusedOrChanged(rootView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryViewModel.onAttachmentAction.observe(viewLifecycleOwner) { entryAttachmentState ->
|
||||||
|
entryAttachmentState?.let {
|
||||||
|
if (it.streamDirection != StreamDirection.UPLOAD) {
|
||||||
|
putAttachment(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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?) {
|
||||||
|
context?.let { context ->
|
||||||
|
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
|
||||||
|
attachmentsAdapter?.database = database
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentsListView.adapter = attachmentsAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadTemplateSettings() {
|
||||||
|
context?.let { context ->
|
||||||
|
templateView.setFirstTimeAskAllowCopyProtectedFields(PreferencesUtil.isFirstTimeAskAllowCopyProtectedFields(context))
|
||||||
|
templateView.setAllowCopyProtectedFields(PreferencesUtil.allowCopyProtectedFields(context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assignEntryInfo(entryInfo: EntryInfo?) {
|
||||||
|
// Set copy buttons
|
||||||
|
templateView.apply {
|
||||||
|
setOnAskCopySafeClickListener {
|
||||||
|
showClipboardDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnCopyActionClickListener { field ->
|
||||||
|
mClipboardHelper?.timeoutCopyToClipboard(
|
||||||
|
field.protectedValue.stringValue,
|
||||||
|
getString(
|
||||||
|
R.string.copy_field,
|
||||||
|
TemplateField.getLocalizedName(context, field.name)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate entry views
|
||||||
|
templateView.setEntryInfo(entryInfo)
|
||||||
|
|
||||||
|
// OTP timer updated
|
||||||
|
templateView.setOnOtpElementUpdated { otpElementUpdated ->
|
||||||
|
mEntryViewModel.onOtpElementUpdated(otpElementUpdated)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manage attachments
|
||||||
|
assignAttachments(entryInfo?.attachments ?: listOf())
|
||||||
|
|
||||||
|
// Assign dates
|
||||||
|
creationDateView.text = entryInfo?.creationTime?.getDateTimeString(resources)
|
||||||
|
modificationDateView.text = entryInfo?.lastModificationTime?.getDateTimeString(resources)
|
||||||
|
|
||||||
|
// TODO Custom data
|
||||||
|
// customDataView.text = entryInfo?.customData?.toString()
|
||||||
|
|
||||||
|
// Assign special data
|
||||||
|
uuidReferenceView.text = UuidUtil.toHexString(entryInfo?.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showClipboardDialog() {
|
||||||
|
context?.let {
|
||||||
|
AlertDialog.Builder(it)
|
||||||
|
.setMessage(
|
||||||
|
getString(R.string.allow_copy_password_warning) +
|
||||||
|
"\n\n" +
|
||||||
|
getString(R.string.clipboard_warning)
|
||||||
|
)
|
||||||
|
.create().apply {
|
||||||
|
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) { dialog, _ ->
|
||||||
|
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(context, true)
|
||||||
|
finishDialog(dialog)
|
||||||
|
}
|
||||||
|
setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ ->
|
||||||
|
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(context, false)
|
||||||
|
finishDialog(dialog)
|
||||||
|
}
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun finishDialog(dialog: DialogInterface) {
|
||||||
|
dialog.dismiss()
|
||||||
|
loadTemplateSettings()
|
||||||
|
templateView.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------
|
||||||
|
* Attachments
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
private fun assignAttachments(attachments: List<Attachment>) {
|
||||||
|
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
|
||||||
|
attachmentsAdapter?.assignItems(attachments.map {
|
||||||
|
EntryAttachmentState(it, StreamDirection.DOWNLOAD)
|
||||||
|
})
|
||||||
|
attachmentsAdapter?.onItemClickListener = { item ->
|
||||||
|
mEntryViewModel.onAttachmentSelected(item.attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putAttachment(attachmentToDownload: EntryAttachmentState) {
|
||||||
|
attachmentsAdapter?.putItem(attachmentToDownload)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------
|
||||||
|
* Education
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun firstEntryFieldCopyView(): View? {
|
||||||
|
return try {
|
||||||
|
templateView.getActionImageView()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchEntryCopyEducationAction() {
|
||||||
|
val appNameString = getString(R.string.app_name)
|
||||||
|
mClipboardHelper?.timeoutCopyToClipboard(appNameString,
|
||||||
|
getString(R.string.copy_field, appNameString))
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun getInstance(): EntryFragment {
|
||||||
|
return EntryFragment().apply {
|
||||||
|
arguments = Bundle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||||
|
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||||
|
|
||||||
|
class EntryHistoryFragment: StylishFragment() {
|
||||||
|
|
||||||
|
private lateinit var historyContainerView: View
|
||||||
|
private lateinit var historyListView: RecyclerView
|
||||||
|
private var historyAdapter: EntryHistoryAdapter? = null
|
||||||
|
|
||||||
|
private val mEntryViewModel: EntryViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
|
return inflater.cloneInContext(contextThemed)
|
||||||
|
.inflate(R.layout.fragment_entry_history, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
context?.let { context ->
|
||||||
|
historyAdapter = EntryHistoryAdapter(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
historyContainerView = view.findViewById(R.id.entry_history_container)
|
||||||
|
historyListView = view.findViewById(R.id.entry_history_list)
|
||||||
|
historyListView.apply {
|
||||||
|
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
|
||||||
|
adapter = historyAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
mEntryViewModel.entryHistory.observe(viewLifecycleOwner) {
|
||||||
|
assignHistory(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------
|
||||||
|
* History
|
||||||
|
* -------------
|
||||||
|
*/
|
||||||
|
private fun assignHistory(history: List<EntryInfo>?) {
|
||||||
|
historyAdapter?.clear()
|
||||||
|
history?.let {
|
||||||
|
historyAdapter?.entryHistoryList?.addAll(history)
|
||||||
|
}
|
||||||
|
historyAdapter?.onItemClickListener = { item, position ->
|
||||||
|
mEntryViewModel.onHistorySelected(item, position)
|
||||||
|
}
|
||||||
|
historyContainerView.visibility = if (historyAdapter?.entryHistoryList?.isEmpty() != false)
|
||||||
|
View.GONE
|
||||||
|
else
|
||||||
|
View.VISIBLE
|
||||||
|
historyAdapter?.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,40 +17,46 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
|
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.RecyclerView.SCROLL_STATE_IDLE
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.EntryEditActivity
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
import com.kunzisoft.keepass.adapters.NodesAdapter
|
||||||
import com.kunzisoft.keepass.adapters.NodeAdapter
|
|
||||||
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.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.viewmodels.GroupViewModel
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener {
|
class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListener {
|
||||||
|
|
||||||
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
|
||||||
var mainGroup: Group? = null
|
private var mLayoutManager: LinearLayoutManager? = null
|
||||||
private set
|
private var mAdapter: NodesAdapter? = null
|
||||||
private var mAdapter: NodeAdapter? = null
|
|
||||||
|
private val mGroupViewModel: GroupViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private var mCurrentGroup: Group? = null
|
||||||
|
|
||||||
var nodeActionSelectionMode = false
|
var nodeActionSelectionMode = false
|
||||||
private set
|
private set
|
||||||
@@ -62,21 +68,47 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
private var notFoundView: View? = null
|
private var notFoundView: View? = null
|
||||||
private var isASearchResult: Boolean = false
|
private var isASearchResult: Boolean = false
|
||||||
|
|
||||||
|
|
||||||
private var readOnly: Boolean = false
|
|
||||||
private var specialMode: SpecialMode = SpecialMode.DEFAULT
|
private var specialMode: SpecialMode = SpecialMode.DEFAULT
|
||||||
|
|
||||||
val isEmpty: Boolean
|
private var mRecycleBinEnable: Boolean = false
|
||||||
get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0
|
private var mRecycleBin: Group? = null
|
||||||
|
|
||||||
|
var mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) { entryId ->
|
||||||
|
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() {
|
||||||
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||||
|
super.onScrollStateChanged(recyclerView, newState)
|
||||||
|
if (newState == SCROLL_STATE_IDLE) {
|
||||||
|
mGroupViewModel.assignPosition(getFirstVisiblePosition())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
|
onScrollListener?.onScrolled(dy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@@ -84,14 +116,24 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
} 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,129 +141,140 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
|
}
|
||||||
|
|
||||||
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, arguments)
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
mRecycleBinEnable = database?.isRecycleBinEnabled == true
|
||||||
arguments?.let { args ->
|
mRecycleBin = database?.recycleBin
|
||||||
// Contains all the group in element
|
|
||||||
if (args.containsKey(GROUP_KEY)) {
|
|
||||||
mainGroup = args.getParcelable(GROUP_KEY)
|
|
||||||
}
|
|
||||||
if (args.containsKey(IS_SEARCH)) {
|
|
||||||
isASearchResult = args.getBoolean(IS_SEARCH)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contextThemed?.let { context ->
|
contextThemed?.let { context ->
|
||||||
mAdapter = NodeAdapter(context)
|
database?.let { database ->
|
||||||
mAdapter?.apply {
|
mAdapter = NodesAdapter(context, database).apply {
|
||||||
setOnNodeClickListener(object : NodeAdapter.NodeClickCallback {
|
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
|
||||||
override fun onNodeClick(node: Node) {
|
override fun onNodeClick(database: Database, node: Node) {
|
||||||
if (nodeActionSelectionMode) {
|
if (mCurrentGroup?.isVirtual == false
|
||||||
if (listActionNodes.contains(node)) {
|
&& nodeActionSelectionMode) {
|
||||||
// Remove selected item if already selected
|
if (listActionNodes.contains(node)) {
|
||||||
listActionNodes.remove(node)
|
// Remove selected item if already selected
|
||||||
|
listActionNodes.remove(node)
|
||||||
|
} else {
|
||||||
|
// Add selected item if not already selected
|
||||||
|
listActionNodes.add(node)
|
||||||
|
}
|
||||||
|
nodeClickListener?.onNodeSelected(database, listActionNodes)
|
||||||
|
setActionNodes(listActionNodes)
|
||||||
|
notifyNodeChanged(node)
|
||||||
} else {
|
} else {
|
||||||
// Add selected item if not already selected
|
nodeClickListener?.onNodeClick(database, node)
|
||||||
listActionNodes.add(node)
|
|
||||||
}
|
}
|
||||||
nodeClickListener?.onNodeSelected(listActionNodes)
|
|
||||||
setActionNodes(listActionNodes)
|
|
||||||
notifyNodeChanged(node)
|
|
||||||
} else {
|
|
||||||
nodeClickListener?.onNodeClick(node)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNodeLongClick(node: Node): Boolean {
|
override fun onNodeLongClick(database: Database, node: Node): Boolean {
|
||||||
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
|
if (mCurrentGroup?.isVirtual == false
|
||||||
// Select the first item after a long click
|
&& nodeActionPasteMode == PasteMode.UNDEFINED) {
|
||||||
if (!listActionNodes.contains(node))
|
// Select the first item after a long click
|
||||||
listActionNodes.add(node)
|
if (!listActionNodes.contains(node))
|
||||||
|
listActionNodes.add(node)
|
||||||
|
|
||||||
nodeClickListener?.onNodeSelected(listActionNodes)
|
nodeClickListener?.onNodeSelected(database, listActionNodes)
|
||||||
|
|
||||||
setActionNodes(listActionNodes)
|
setActionNodes(listActionNodes)
|
||||||
notifyNodeChanged(node)
|
notifyNodeChanged(node)
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
return true
|
})
|
||||||
}
|
}
|
||||||
})
|
mNodesRecyclerView?.adapter = mAdapter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onDatabaseActionFinished(
|
||||||
ReadOnlyHelper.onSaveInstanceState(outState, readOnly)
|
database: Database,
|
||||||
super.onSaveInstanceState(outState)
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
|
|
||||||
|
// Too many special cases to make specific additions or deletions,
|
||||||
|
// rebuilt the list works well.
|
||||||
|
if (result.isSuccess) {
|
||||||
|
rebuildList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
super.onCreateView(inflater, container, savedInstanceState)
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
// To apply theme
|
// To apply theme
|
||||||
val rootView = inflater.cloneInContext(contextThemed)
|
return inflater.cloneInContext(contextThemed)
|
||||||
.inflate(R.layout.fragment_list_nodes, container, false)
|
.inflate(R.layout.fragment_nodes, container, false)
|
||||||
mNodesRecyclerView = rootView.findViewById(R.id.nodes_list)
|
}
|
||||||
notFoundView = rootView.findViewById(R.id.not_found_container)
|
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
mNodesRecyclerView = view.findViewById(R.id.nodes_list)
|
||||||
|
notFoundView = view.findViewById(R.id.not_found_container)
|
||||||
|
|
||||||
|
mLayoutManager = LinearLayoutManager(context)
|
||||||
mNodesRecyclerView?.apply {
|
mNodesRecyclerView?.apply {
|
||||||
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
||||||
layoutManager = LinearLayoutManager(context)
|
layoutManager = mLayoutManager
|
||||||
adapter = mAdapter
|
adapter = mAdapter
|
||||||
}
|
}
|
||||||
|
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||||
|
|
||||||
onScrollListener?.let { onScrollListener ->
|
mGroupViewModel.group.observe(viewLifecycleOwner) {
|
||||||
mNodesRecyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
mCurrentGroup = it.group
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
isASearchResult = it.group.isVirtual
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
rebuildList()
|
||||||
onScrollListener.onScrolled(dy)
|
it.showFromPosition?.let { position ->
|
||||||
}
|
mNodesRecyclerView?.scrollToPosition(position)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return rootView
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
|
mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener)
|
||||||
activity?.intent?.let {
|
activity?.intent?.let {
|
||||||
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
|
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh data
|
rebuildList()
|
||||||
try {
|
|
||||||
rebuildList()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to rebuild the list during resume")
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) {
|
|
||||||
// To show the " no search entry found "
|
|
||||||
mNodesRecyclerView?.visibility = View.GONE
|
|
||||||
notFoundView?.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
mNodesRecyclerView?.visibility = View.VISIBLE
|
|
||||||
notFoundView?.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class)
|
override fun onPause() {
|
||||||
fun rebuildList() {
|
|
||||||
// Add elements to the list
|
mNodesRecyclerView?.removeOnScrollListener(mRecycleViewScrollListener)
|
||||||
mainGroup?.let { mainGroup ->
|
super.onPause()
|
||||||
mAdapter?.apply {
|
}
|
||||||
|
|
||||||
|
fun getFirstVisiblePosition(): Int {
|
||||||
|
return mLayoutManager?.findFirstVisibleItemPosition() ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rebuildList() {
|
||||||
|
try {
|
||||||
|
// Add elements to the list
|
||||||
|
mCurrentGroup?.let { currentGroup ->
|
||||||
// Thrown an exception when sort cannot be performed
|
// Thrown an exception when sort cannot be performed
|
||||||
rebuildList(mainGroup)
|
mAdapter?.rebuildList(currentGroup)
|
||||||
// To visually change the elements
|
|
||||||
if (PreferencesUtil.APPEARANCE_CHANGED) {
|
|
||||||
notifyDataSetChanged()
|
|
||||||
PreferencesUtil.APPEARANCE_CHANGED = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch (e:Exception) {
|
||||||
|
Log.e(TAG, "Unable to rebuild the list", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isASearchResult && mAdapter != null && mAdapter!!.isEmpty) {
|
||||||
|
// To show the " no search entry found "
|
||||||
|
notFoundView?.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
notFoundView?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
groupRefreshed?.onGroupRefreshed()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSortSelected(sortNodeEnum: SortNodeEnum,
|
override fun onSortSelected(sortNodeEnum: SortNodeEnum,
|
||||||
@@ -236,8 +289,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters)
|
mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters)
|
||||||
rebuildList()
|
rebuildList()
|
||||||
} catch (e:Exception) {
|
} catch (e:Exception) {
|
||||||
Log.e(TAG, "Unable to rebuild the list with the sort")
|
Log.e(TAG, "Unable to sort the list", e)
|
||||||
e.printStackTrace()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,17 +305,19 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
R.id.menu_sort -> {
|
R.id.menu_sort -> {
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
val sortDialogFragment: SortDialogFragment =
|
val sortDialogFragment: SortDialogFragment =
|
||||||
if (Database.getInstance().isRecycleBinEnabled) {
|
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")
|
||||||
@@ -275,34 +329,32 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun actionNodesCallback(nodes: List<Node>,
|
fun actionNodesCallback(database: Database,
|
||||||
|
nodes: List<Node>,
|
||||||
menuListener: NodesActionMenuListener?,
|
menuListener: NodesActionMenuListener?,
|
||||||
actionModeCallback: ActionMode.Callback) : ActionMode.Callback {
|
onDestroyActionMode: (mode: ActionMode?) -> Unit) : ActionMode.Callback {
|
||||||
|
|
||||||
return object : ActionMode.Callback {
|
return object : ActionMode.Callback {
|
||||||
|
|
||||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||||
nodeActionSelectionMode = false
|
nodeActionSelectionMode = false
|
||||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||||
return actionModeCallback.onCreateActionMode(mode, menu)
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||||
menu?.clear()
|
menu?.clear()
|
||||||
|
|
||||||
if (nodeActionPasteMode != PasteMode.UNDEFINED) {
|
if (nodeActionPasteMode != PasteMode.UNDEFINED) {
|
||||||
mode?.menuInflater?.inflate(R.menu.node_paste_menu, menu)
|
mode?.menuInflater?.inflate(R.menu.node_paste_menu, menu)
|
||||||
} else {
|
} else {
|
||||||
nodeActionSelectionMode = true
|
nodeActionSelectionMode = true
|
||||||
mode?.menuInflater?.inflate(R.menu.node_menu, menu)
|
mode?.menuInflater?.inflate(R.menu.node_menu, menu)
|
||||||
|
|
||||||
val database = Database.getInstance()
|
|
||||||
|
|
||||||
// Open and Edit for a single item
|
// Open and Edit for a single item
|
||||||
if (nodes.size == 1) {
|
if (nodes.size == 1) {
|
||||||
// Edition
|
// Edition
|
||||||
if (readOnly
|
if (database.isReadOnly
|
||||||
|| (database.isRecycleBinEnabled && nodes[0] == database.recycleBin)) {
|
|| (mRecycleBinEnable && nodes[0] == mRecycleBin)) {
|
||||||
menu?.removeItem(R.id.menu_edit)
|
menu?.removeItem(R.id.menu_edit)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -310,56 +362,59 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
menu?.removeItem(R.id.menu_edit)
|
menu?.removeItem(R.id.menu_edit)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy and Move (not for groups)
|
// Move
|
||||||
if (readOnly
|
if (database.isReadOnly
|
||||||
|| isASearchResult
|
|| isASearchResult) {
|
||||||
|| nodes.any { it.type == Type.GROUP }) {
|
|
||||||
// TODO Copy For Group
|
|
||||||
menu?.removeItem(R.id.menu_copy)
|
|
||||||
menu?.removeItem(R.id.menu_move)
|
menu?.removeItem(R.id.menu_move)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy (not allowed for group)
|
||||||
|
if (database.isReadOnly
|
||||||
|
|| isASearchResult
|
||||||
|
|| nodes.any { it.type == Type.GROUP }) {
|
||||||
|
menu?.removeItem(R.id.menu_copy)
|
||||||
|
}
|
||||||
|
|
||||||
// Deletion
|
// Deletion
|
||||||
if (readOnly
|
if (database.isReadOnly
|
||||||
|| (database.isRecycleBinEnabled && nodes.any { it == database.recycleBin })) {
|
|| (mRecycleBinEnable && nodes.any { it == mRecycleBin })) {
|
||||||
menu?.removeItem(R.id.menu_delete)
|
menu?.removeItem(R.id.menu_delete)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the number of items selected in title
|
// Add the number of items selected in title
|
||||||
mode?.title = nodes.size.toString()
|
mode?.title = nodes.size.toString()
|
||||||
|
return true
|
||||||
return actionModeCallback.onPrepareActionMode(mode, menu)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
||||||
if (menuListener == null)
|
if (menuListener == null)
|
||||||
return false
|
return false
|
||||||
return when (item?.itemId) {
|
return when (item?.itemId) {
|
||||||
R.id.menu_open -> menuListener.onOpenMenuClick(nodes[0])
|
R.id.menu_open -> menuListener.onOpenMenuClick(database, nodes[0])
|
||||||
R.id.menu_edit -> menuListener.onEditMenuClick(nodes[0])
|
R.id.menu_edit -> menuListener.onEditMenuClick(database, nodes[0])
|
||||||
R.id.menu_copy -> {
|
R.id.menu_copy -> {
|
||||||
nodeActionPasteMode = PasteMode.PASTE_FROM_COPY
|
nodeActionPasteMode = PasteMode.PASTE_FROM_COPY
|
||||||
mAdapter?.unselectActionNodes()
|
mAdapter?.unselectActionNodes()
|
||||||
val returnValue = menuListener.onCopyMenuClick(nodes)
|
val returnValue = menuListener.onCopyMenuClick(database, nodes)
|
||||||
nodeActionSelectionMode = false
|
nodeActionSelectionMode = false
|
||||||
returnValue
|
returnValue
|
||||||
}
|
}
|
||||||
R.id.menu_move -> {
|
R.id.menu_move -> {
|
||||||
nodeActionPasteMode = PasteMode.PASTE_FROM_MOVE
|
nodeActionPasteMode = PasteMode.PASTE_FROM_MOVE
|
||||||
mAdapter?.unselectActionNodes()
|
mAdapter?.unselectActionNodes()
|
||||||
val returnValue = menuListener.onMoveMenuClick(nodes)
|
val returnValue = menuListener.onMoveMenuClick(database, nodes)
|
||||||
nodeActionSelectionMode = false
|
nodeActionSelectionMode = false
|
||||||
returnValue
|
returnValue
|
||||||
}
|
}
|
||||||
R.id.menu_delete -> menuListener.onDeleteMenuClick(nodes)
|
R.id.menu_delete -> menuListener.onDeleteMenuClick(database, nodes)
|
||||||
R.id.menu_paste -> {
|
R.id.menu_paste -> {
|
||||||
val returnValue = menuListener.onPasteMenuClick(nodeActionPasteMode, nodes)
|
val returnValue = menuListener.onPasteMenuClick(database, nodeActionPasteMode, nodes)
|
||||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||||
nodeActionSelectionMode = false
|
nodeActionSelectionMode = false
|
||||||
returnValue
|
returnValue
|
||||||
}
|
}
|
||||||
else -> actionModeCallback.onActionItemClicked(mode, item)
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,83 +424,29 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
mAdapter?.unselectActionNodes()
|
mAdapter?.unselectActionNodes()
|
||||||
nodeActionPasteMode = PasteMode.UNDEFINED
|
nodeActionPasteMode = PasteMode.UNDEFINED
|
||||||
nodeActionSelectionMode = false
|
nodeActionSelectionMode = false
|
||||||
actionModeCallback.onDestroyActionMode(mode)
|
onDestroyActionMode(mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
|
|
||||||
when (requestCode) {
|
|
||||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
|
||||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE
|
|
||||||
|| resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) {
|
|
||||||
data?.getParcelableExtra<Node>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { changedNode ->
|
|
||||||
if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE)
|
|
||||||
addNode(changedNode)
|
|
||||||
if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE)
|
|
||||||
mAdapter?.notifyDataSetChanged()
|
|
||||||
} ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun contains(node: Node): Boolean {
|
|
||||||
return mAdapter?.contains(node) ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addNode(newNode: Node) {
|
|
||||||
mAdapter?.addNode(newNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addNodes(newNodes: List<Node>) {
|
|
||||||
mAdapter?.addNodes(newNodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateNode(oldNode: Node, newNode: Node? = null) {
|
|
||||||
mAdapter?.updateNode(oldNode, newNode ?: oldNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateNodes(oldNodes: List<Node>, newNodes: List<Node>) {
|
|
||||||
mAdapter?.updateNodes(oldNodes, newNodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeNode(pwNode: Node) {
|
|
||||||
mAdapter?.removeNode(pwNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeNodes(nodes: List<Node>) {
|
|
||||||
mAdapter?.removeNodes(nodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeNodeAt(position: Int) {
|
|
||||||
mAdapter?.removeNodeAt(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeNodesAt(positions: IntArray) {
|
|
||||||
mAdapter?.removeNodesAt(positions)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback listener to redefine to do an action when a node is click
|
* Callback listener to redefine to do an action when a node is click
|
||||||
*/
|
*/
|
||||||
interface NodeClickListener {
|
interface NodeClickListener {
|
||||||
fun onNodeClick(node: Node)
|
fun onNodeClick(database: Database, node: Node)
|
||||||
fun onNodeSelected(nodes: List<Node>): Boolean
|
fun onNodeSelected(database: Database, nodes: List<Node>): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Menu listener to redefine to do an action in menu
|
* Menu listener to redefine to do an action in menu
|
||||||
*/
|
*/
|
||||||
interface NodesActionMenuListener {
|
interface NodesActionMenuListener {
|
||||||
fun onOpenMenuClick(node: Node): Boolean
|
fun onOpenMenuClick(database: Database, node: Node): Boolean
|
||||||
fun onEditMenuClick(node: Node): Boolean
|
fun onEditMenuClick(database: Database, node: Node): Boolean
|
||||||
fun onCopyMenuClick(nodes: List<Node>): Boolean
|
fun onCopyMenuClick(database: Database, nodes: List<Node>): Boolean
|
||||||
fun onMoveMenuClick(nodes: List<Node>): Boolean
|
fun onMoveMenuClick(database: Database, nodes: List<Node>): Boolean
|
||||||
fun onDeleteMenuClick(nodes: List<Node>): Boolean
|
fun onDeleteMenuClick(database: Database, nodes: List<Node>): Boolean
|
||||||
fun onPasteMenuClick(pasteMode: PasteMode?, nodes: List<Node>): Boolean
|
fun onPasteMenuClick(database: Database, pasteMode: PasteMode?, nodes: List<Node>): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class PasteMode {
|
enum class PasteMode {
|
||||||
@@ -463,23 +464,11 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis
|
|||||||
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 = ListNodesFragment::class.java.name
|
|
||||||
|
|
||||||
private const val GROUP_KEY = "GROUP_KEY"
|
|
||||||
private const val IS_SEARCH = "IS_SEARCH"
|
|
||||||
|
|
||||||
fun newInstance(group: Group?, readOnly: Boolean, isASearch: Boolean): ListNodesFragment {
|
|
||||||
val bundle = Bundle()
|
|
||||||
if (group != null) {
|
|
||||||
bundle.putParcelable(GROUP_KEY, group)
|
|
||||||
}
|
|
||||||
bundle.putBoolean(IS_SEARCH, isASearch)
|
|
||||||
ReadOnlyHelper.putReadOnlyInBundle(bundle, readOnly)
|
|
||||||
val listNodesFragment = ListNodesFragment()
|
|
||||||
listNodesFragment.arguments = bundle
|
|
||||||
return listNodesFragment
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
|
|
||||||
|
|
||||||
|
class IconCustomFragment : IconFragment<IconImageCustom>() {
|
||||||
|
|
||||||
|
override fun retrieveMainLayoutId(): Int {
|
||||||
|
return R.layout.fragment_icon_grid
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun defineIconList(database: Database?) {
|
||||||
|
database?.doForEachCustomIcons { customIcon, _ ->
|
||||||
|
iconPickerAdapter.addIcon(customIcon, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
iconPickerViewModel.customIconsSelected.observe(viewLifecycleOwner) { customIconsSelected ->
|
||||||
|
if (customIconsSelected.isEmpty()) {
|
||||||
|
iconActionSelectionMode = false
|
||||||
|
iconPickerAdapter.deselectAllIcons()
|
||||||
|
} else {
|
||||||
|
iconActionSelectionMode = true
|
||||||
|
iconPickerAdapter.updateIconSelectedState(customIconsSelected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iconPickerViewModel.customIconAdded.observe(viewLifecycleOwner) { iconCustomAdded ->
|
||||||
|
if (!iconCustomAdded.error) {
|
||||||
|
iconCustomAdded?.iconCustom?.let { icon ->
|
||||||
|
iconPickerAdapter.addIcon(icon)
|
||||||
|
iconCustomAdded.iconCustom = null
|
||||||
|
try {
|
||||||
|
iconsGridView.smoothScrollToPosition(iconPickerAdapter.lastPosition)
|
||||||
|
} catch (ignore: Exception) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iconPickerViewModel.customIconRemoved.observe(viewLifecycleOwner) { iconCustomRemoved ->
|
||||||
|
if (!iconCustomRemoved.error) {
|
||||||
|
iconCustomRemoved?.iconCustom?.let { icon ->
|
||||||
|
iconPickerAdapter.removeIcon(icon)
|
||||||
|
iconCustomRemoved.iconCustom = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iconPickerViewModel.customIconUpdated.observe(viewLifecycleOwner) { iconCustomUpdated ->
|
||||||
|
if (!iconCustomUpdated.error) {
|
||||||
|
iconCustomUpdated?.iconCustom?.let { icon ->
|
||||||
|
iconPickerAdapter.updateIcon(icon)
|
||||||
|
iconCustomUpdated.iconCustom = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIconClickListener(icon: IconImageCustom) {
|
||||||
|
if (iconActionSelectionMode) {
|
||||||
|
// Same long click behavior after each single click
|
||||||
|
onIconLongClickListener(icon)
|
||||||
|
} else {
|
||||||
|
iconPickerViewModel.pickCustomIcon(icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIconLongClickListener(icon: IconImageCustom) {
|
||||||
|
// Select or deselect item if already selected
|
||||||
|
icon.selected = !icon.selected
|
||||||
|
iconPickerAdapter.updateIcon(icon)
|
||||||
|
iconActionSelectionMode = iconPickerAdapter.containsAnySelectedIcon()
|
||||||
|
iconPickerViewModel.selectCustomIcons(iconPickerAdapter.getSelectedIcons())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.adapters.IconPickerAdapter
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
|
||||||
|
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
|
||||||
|
IconPickerAdapter.IconPickerListener<T> {
|
||||||
|
|
||||||
|
protected lateinit var iconsGridView: RecyclerView
|
||||||
|
protected lateinit var iconPickerAdapter: IconPickerAdapter<T>
|
||||||
|
protected var iconActionSelectionMode = false
|
||||||
|
|
||||||
|
protected val iconPickerViewModel: IconPickerViewModel by activityViewModels()
|
||||||
|
|
||||||
|
abstract fun retrieveMainLayoutId(): Int
|
||||||
|
|
||||||
|
abstract fun defineIconList(database: Database?)
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?): View {
|
||||||
|
return inflater.inflate(retrieveMainLayoutId(), container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
// Retrieve the textColor to tint the icon
|
||||||
|
val ta = contextThemed?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
|
val tintColor = ta?.getColor(0, Color.BLACK) ?: Color.BLACK
|
||||||
|
ta?.recycle()
|
||||||
|
|
||||||
|
iconsGridView = view.findViewById(R.id.icons_grid_view)
|
||||||
|
iconPickerAdapter = IconPickerAdapter(requireContext(), tintColor)
|
||||||
|
iconPickerAdapter.iconPickerListener = this
|
||||||
|
iconsGridView.adapter = iconPickerAdapter
|
||||||
|
|
||||||
|
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val populateList = launch {
|
||||||
|
iconPickerAdapter.clear()
|
||||||
|
defineIconList(database)
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
populateList.join()
|
||||||
|
iconPickerAdapter.notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onIconDeleteClicked() {
|
||||||
|
iconActionSelectionMode = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
|
import com.google.android.material.tabs.TabLayout
|
||||||
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.adapters.IconPickerPagerAdapter
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||||
|
|
||||||
|
class IconPickerFragment : DatabaseFragment() {
|
||||||
|
|
||||||
|
private var iconPickerPagerAdapter: IconPickerPagerAdapter? = null
|
||||||
|
private lateinit var viewPager: ViewPager2
|
||||||
|
private lateinit var tabLayout: TabLayout
|
||||||
|
|
||||||
|
private val iconPickerViewModel: IconPickerViewModel by activityViewModels()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
viewPager = view.findViewById(R.id.tabs_view_pager)
|
||||||
|
tabLayout = view.findViewById(R.id.tabs_layout)
|
||||||
|
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||||
|
|
||||||
|
arguments?.apply {
|
||||||
|
if (containsKey(ICON_TAB_ARG)) {
|
||||||
|
viewPager.currentItem = getInt(ICON_TAB_ARG)
|
||||||
|
}
|
||||||
|
remove(ICON_TAB_ARG)
|
||||||
|
}
|
||||||
|
|
||||||
|
iconPickerViewModel.customIconAdded.observe(viewLifecycleOwner) { _ ->
|
||||||
|
viewPager.currentItem = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
|
||||||
|
if (database?.allowCustomIcons == true) 2 else 1)
|
||||||
|
viewPager.adapter = iconPickerPagerAdapter
|
||||||
|
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
|
||||||
|
tab.text = when (position) {
|
||||||
|
1 -> getString(R.string.icon_section_custom)
|
||||||
|
else -> getString(R.string.icon_section_standard)
|
||||||
|
}
|
||||||
|
}.attach()
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class IconTab {
|
||||||
|
STANDARD, CUSTOM
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val ICON_TAB_ARG = "ICON_TAB_ARG"
|
||||||
|
|
||||||
|
fun getInstance(iconTab: IconTab): IconPickerFragment {
|
||||||
|
val fragment = IconPickerFragment()
|
||||||
|
fragment.arguments = Bundle().apply {
|
||||||
|
putInt(ICON_TAB_ARG, iconTab.ordinal)
|
||||||
|
}
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
|
|
||||||
|
|
||||||
|
class IconStandardFragment : IconFragment<IconImageStandard>() {
|
||||||
|
|
||||||
|
override fun retrieveMainLayoutId(): Int {
|
||||||
|
return R.layout.fragment_icon_grid
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun defineIconList(database: Database?) {
|
||||||
|
database?.doForEachStandardIcons { standardIcon ->
|
||||||
|
iconPickerAdapter.addIcon(standardIcon, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIconClickListener(icon: IconImageStandard) {
|
||||||
|
iconPickerViewModel.pickStandardIcon(icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIconLongClickListener(icon: IconImageStandard) {}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.activities.helpers
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
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.FragmentActivity
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
|
||||||
|
class ExternalFileHelper {
|
||||||
|
|
||||||
|
private var activity: FragmentActivity? = null
|
||||||
|
private var fragment: Fragment? = null
|
||||||
|
|
||||||
|
private var getContentResultLauncher: ActivityResultLauncher<String>? = null
|
||||||
|
private var openDocumentResultLauncher: ActivityResultLauncher<Array<String>>? = null
|
||||||
|
private var createDocumentResultLauncher: ActivityResultLauncher<String>? = null
|
||||||
|
|
||||||
|
constructor(context: FragmentActivity) {
|
||||||
|
this.activity = context
|
||||||
|
this.fragment = null
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Fragment) {
|
||||||
|
this.activity = context.activity
|
||||||
|
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,
|
||||||
|
typeString: String = "*/*") {
|
||||||
|
try {
|
||||||
|
if (getContent) {
|
||||||
|
getContentResultLauncher?.launch(typeString)
|
||||||
|
} else {
|
||||||
|
openDocumentResultLauncher?.launch(arrayOf(typeString))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to open document", e)
|
||||||
|
showFileManagerDialogFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createDocument(titleString: String) {
|
||||||
|
try {
|
||||||
|
createDocumentResultLauncher?.launch(titleString)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to create document", e)
|
||||||
|
showFileManagerDialogFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show Browser dialog to select file picker app
|
||||||
|
*/
|
||||||
|
private fun showFileManagerDialogFragment() {
|
||||||
|
try {
|
||||||
|
if (fragment != null) {
|
||||||
|
fragment?.parentFragmentManager
|
||||||
|
} else {
|
||||||
|
activity?.supportFragmentManager
|
||||||
|
}?.let { fragmentManager ->
|
||||||
|
FileManagerDialogFragment().show(fragmentManager, "browserDialog")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Can't open BrowserDialog", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OpenDocument : ActivityResultContracts.OpenDocument() {
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
override fun createIntent(context: Context, input: Array<out String>): Intent {
|
||||||
|
return super.createIntent(context, input).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GetContent : ActivityResultContracts.GetContent() {
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
override fun createIntent(context: Context, input: String): Intent {
|
||||||
|
return super.createIntent(context, input).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
|
||||||
|
private const val TAG = "OpenFileHelper"
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
fun allowCreateDocumentByStorageAccessFramework(packageManager: PackageManager,
|
||||||
|
typeString: String = "application/octet-stream"): Boolean {
|
||||||
|
return when {
|
||||||
|
// To check if a custom file manager can manage the ACTION_CREATE_DOCUMENT
|
||||||
|
Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT -> {
|
||||||
|
packageManager.queryIntentActivities(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||||
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
type = typeString
|
||||||
|
}, PackageManager.MATCH_DEFAULT_ONLY).isNotEmpty()
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.setOpenDocumentClickListener(externalFileHelper: ExternalFileHelper?) {
|
||||||
|
externalFileHelper?.let { fileHelper ->
|
||||||
|
setOnClickListener {
|
||||||
|
fileHelper.openDocument(false)
|
||||||
|
}
|
||||||
|
setOnLongClickListener {
|
||||||
|
fileHelper.openDocument(true)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} ?: kotlin.run {
|
||||||
|
setOnClickListener(null)
|
||||||
|
setOnLongClickListener(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.activities.helpers
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
|
|
||||||
object ReadOnlyHelper {
|
|
||||||
|
|
||||||
private const val READ_ONLY_KEY = "READ_ONLY_KEY"
|
|
||||||
|
|
||||||
const val READ_ONLY_DEFAULT = false
|
|
||||||
|
|
||||||
fun retrieveReadOnlyFromIntent(intent: Intent): Boolean {
|
|
||||||
return intent.getBooleanExtra(READ_ONLY_KEY, READ_ONLY_DEFAULT)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun retrieveReadOnlyFromInstanceStateOrPreference(context: Context, savedInstanceState: Bundle?): Boolean {
|
|
||||||
return if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) {
|
|
||||||
savedInstanceState.getBoolean(READ_ONLY_KEY)
|
|
||||||
} else {
|
|
||||||
PreferencesUtil.enableReadOnlyDatabase(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState: Bundle?, arguments: Bundle?): Boolean {
|
|
||||||
var readOnly = READ_ONLY_DEFAULT
|
|
||||||
if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) {
|
|
||||||
readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY)
|
|
||||||
} else if (arguments != null && arguments.containsKey(READ_ONLY_KEY)) {
|
|
||||||
readOnly = arguments.getBoolean(READ_ONLY_KEY)
|
|
||||||
}
|
|
||||||
return readOnly
|
|
||||||
}
|
|
||||||
|
|
||||||
fun retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState: Bundle?, intent: Intent?): Boolean {
|
|
||||||
var readOnly = READ_ONLY_DEFAULT
|
|
||||||
if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) {
|
|
||||||
readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY)
|
|
||||||
} else {
|
|
||||||
if (intent != null)
|
|
||||||
readOnly = intent.getBooleanExtra(READ_ONLY_KEY, READ_ONLY_DEFAULT)
|
|
||||||
}
|
|
||||||
return readOnly
|
|
||||||
}
|
|
||||||
|
|
||||||
fun putReadOnlyInIntent(intent: Intent, readOnly: Boolean) {
|
|
||||||
intent.putExtra(READ_ONLY_KEY, readOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun putReadOnlyInBundle(bundle: Bundle, readOnly: Boolean) {
|
|
||||||
bundle.putBoolean(READ_ONLY_KEY, readOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSaveInstanceState(outState: Bundle, readOnly: Boolean) {
|
|
||||||
outState.putBoolean(READ_ONLY_KEY, readOnly)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.activities.helpers
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.Activity.RESULT_OK
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
|
||||||
|
|
||||||
class SelectFileHelper {
|
|
||||||
|
|
||||||
private var activity: Activity? = null
|
|
||||||
private var fragment: Fragment? = null
|
|
||||||
|
|
||||||
val selectFileOnClickViewListener: SelectFileOnClickViewListener
|
|
||||||
get() = SelectFileOnClickViewListener()
|
|
||||||
|
|
||||||
constructor(context: Activity) {
|
|
||||||
this.activity = context
|
|
||||||
this.fragment = null
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context: Fragment) {
|
|
||||||
this.activity = context.activity
|
|
||||||
this.fragment = context
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class SelectFileOnClickViewListener :
|
|
||||||
View.OnClickListener,
|
|
||||||
View.OnLongClickListener,
|
|
||||||
MenuItem.OnMenuItemClickListener {
|
|
||||||
|
|
||||||
private fun onAbstractClick(longClick: Boolean = false) {
|
|
||||||
try {
|
|
||||||
if (longClick) {
|
|
||||||
try {
|
|
||||||
openActivityWithActionGetContent()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
openActivityWithActionOpenDocument()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
openActivityWithActionOpenDocument()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
openActivityWithActionGetContent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Enable to start the file picker activity", e)
|
|
||||||
// Open browser dialog
|
|
||||||
if (lookForOpenIntentsFilePicker())
|
|
||||||
showBrowserDialog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
|
||||||
onAbstractClick()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLongClick(v: View?): Boolean {
|
|
||||||
onAbstractClick(true)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
|
||||||
onAbstractClick()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
private fun openActivityWithActionOpenDocument() {
|
|
||||||
val intentOpenDocument = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
type = "*/*"
|
|
||||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
if (fragment != null)
|
|
||||||
fragment?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
|
||||||
else
|
|
||||||
activity?.startActivityForResult(intentOpenDocument, OPEN_DOC)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
private fun openActivityWithActionGetContent() {
|
|
||||||
val intentGetContent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
type = "*/*"
|
|
||||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
if (fragment != null)
|
|
||||||
fragment?.startActivityForResult(intentGetContent, GET_CONTENT)
|
|
||||||
else
|
|
||||||
activity?.startActivityForResult(intentGetContent, GET_CONTENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun lookForOpenIntentsFilePicker(): Boolean {
|
|
||||||
var showBrowser = false
|
|
||||||
try {
|
|
||||||
if (isIntentAvailable(activity!!, OPEN_INTENTS_FILE_BROWSE)) {
|
|
||||||
val intent = Intent(OPEN_INTENTS_FILE_BROWSE)
|
|
||||||
if (fragment != null)
|
|
||||||
fragment?.startActivityForResult(intent, FILE_BROWSE)
|
|
||||||
else
|
|
||||||
activity?.startActivityForResult(intent, FILE_BROWSE)
|
|
||||||
} else {
|
|
||||||
showBrowser = true
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, "Enable to start OPEN_INTENTS_FILE_BROWSE", e)
|
|
||||||
showBrowser = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return showBrowser
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates whether the specified action can be used as an intent. This
|
|
||||||
* method queries the package manager for installed packages that can
|
|
||||||
* respond to an intent with the specified action. If no suitable package is
|
|
||||||
* found, this method returns false.
|
|
||||||
*
|
|
||||||
* @param context The application's environment.
|
|
||||||
* @param action The Intent action to check for availability.
|
|
||||||
*
|
|
||||||
* @return True if an Intent with the specified action can be sent and
|
|
||||||
* responded to, false otherwise.
|
|
||||||
*/
|
|
||||||
private fun isIntentAvailable(context: Context, action: String): Boolean {
|
|
||||||
val packageManager = context.packageManager
|
|
||||||
val intent = Intent(action)
|
|
||||||
val list = packageManager.queryIntentActivities(intent,
|
|
||||||
PackageManager.MATCH_DEFAULT_ONLY)
|
|
||||||
return list.size > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show Browser dialog to select file picker app
|
|
||||||
*/
|
|
||||||
private fun showBrowserDialog() {
|
|
||||||
try {
|
|
||||||
val fileManagerDialogFragment = FileManagerDialogFragment()
|
|
||||||
fragment?.let {
|
|
||||||
fileManagerDialogFragment.show(it.parentFragmentManager, "browserDialog")
|
|
||||||
} ?: fileManagerDialogFragment.show((activity as FragmentActivity).supportFragmentManager, "browserDialog")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Can't open BrowserDialog", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To use in onActivityResultCallback in Fragment or Activity
|
|
||||||
* @param keyFileCallback Callback retrieve from data
|
|
||||||
* @return true if requestCode was captured, false elsechere
|
|
||||||
*/
|
|
||||||
fun onActivityResultCallback(
|
|
||||||
requestCode: Int,
|
|
||||||
resultCode: Int,
|
|
||||||
data: Intent?,
|
|
||||||
keyFileCallback: ((uri: Uri?) -> Unit)?): Boolean {
|
|
||||||
|
|
||||||
when (requestCode) {
|
|
||||||
FILE_BROWSE -> {
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
val filename = data?.dataString
|
|
||||||
var keyUri: Uri? = null
|
|
||||||
if (filename != null) {
|
|
||||||
keyUri = UriUtil.parse(filename)
|
|
||||||
}
|
|
||||||
keyFileCallback?.invoke(keyUri)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
GET_CONTENT, OPEN_DOC -> {
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
if (data != null) {
|
|
||||||
val uri = data.data
|
|
||||||
if (uri != null) {
|
|
||||||
try {
|
|
||||||
// try to persist read and write permissions
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
|
||||||
activity?.contentResolver?.apply {
|
|
||||||
takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// nop
|
|
||||||
}
|
|
||||||
keyFileCallback?.invoke(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val TAG = "OpenFileHelper"
|
|
||||||
|
|
||||||
const val OPEN_INTENTS_FILE_BROWSE = "org.openintents.action.PICK_FILE"
|
|
||||||
|
|
||||||
private const val GET_CONTENT = 25745
|
|
||||||
private const val OPEN_DOC = 25845
|
|
||||||
private const val FILE_BROWSE = 25645
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package com.kunzisoft.keepass.activities.legacy
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
|
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
|
||||||
|
abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
||||||
|
|
||||||
|
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
|
||||||
|
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
||||||
|
protected var mDatabase: Database? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
mDatabaseTaskProvider = DatabaseTaskProvider(this)
|
||||||
|
|
||||||
|
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
||||||
|
val databaseWasReloaded = database?.wasReloaded == true
|
||||||
|
if (databaseWasReloaded && finishActivityIfReloadRequested()) {
|
||||||
|
finish()
|
||||||
|
} else if (mDatabase == null || mDatabase != database || databaseWasReloaded) {
|
||||||
|
database?.wasReloaded = false
|
||||||
|
onDatabaseRetrieved(database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mDatabaseTaskProvider?.onActionFinish = { database, actionTask, result ->
|
||||||
|
onDatabaseActionFinished(database, actionTask, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
mDatabase = database
|
||||||
|
mDatabaseViewModel.defineDatabase(database)
|
||||||
|
// optional method implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseActionFinished(
|
||||||
|
database: Database,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
mDatabaseViewModel.onActionFinished(database, actionTask, result)
|
||||||
|
// optional method implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createDatabase(databaseUri: Uri,
|
||||||
|
mainCredential: MainCredential) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseCreate(databaseUri, mainCredential)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadDatabase(databaseUri: Uri,
|
||||||
|
mainCredential: MainCredential,
|
||||||
|
readOnly: Boolean,
|
||||||
|
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||||
|
fixDuplicateUuid: Boolean) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEncryptDatabase, fixDuplicateUuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun closeDatabase() {
|
||||||
|
mDatabase?.clearAndClose(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
mDatabaseTaskProvider?.registerProgressTask()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
mDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,514 @@
|
|||||||
|
/*
|
||||||
|
* 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.legacy
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
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.PasswordEncodingDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
|
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.node.Node
|
||||||
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
|
import com.kunzisoft.keepass.model.MainCredential
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
import com.kunzisoft.keepass.utils.*
|
||||||
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
|
import com.kunzisoft.keepass.viewmodels.NodesViewModel
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||||
|
PasswordEncodingDialogFragment.Listener {
|
||||||
|
|
||||||
|
private val mNodesViewModel: NodesViewModel by viewModels()
|
||||||
|
|
||||||
|
protected var mTimeoutEnable: Boolean = true
|
||||||
|
|
||||||
|
private var mLockReceiver: LockReceiver? = null
|
||||||
|
private var mExitLock: Boolean = false
|
||||||
|
|
||||||
|
protected var mDatabaseReadOnly: Boolean = true
|
||||||
|
protected var mMergeDataAllowed: Boolean = false
|
||||||
|
private var mAutoSaveEnable: Boolean = true
|
||||||
|
|
||||||
|
protected var mIconDrawableFactory: IconDrawableFactory? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
if (savedInstanceState != null
|
||||||
|
&& savedInstanceState.containsKey(TIMEOUT_ENABLE_KEY)
|
||||||
|
) {
|
||||||
|
mTimeoutEnable = savedInstanceState.getBoolean(TIMEOUT_ENABLE_KEY)
|
||||||
|
} else {
|
||||||
|
if (intent != null)
|
||||||
|
mTimeoutEnable =
|
||||||
|
intent.getBooleanExtra(TIMEOUT_ENABLE_KEY, TIMEOUT_ENABLE_KEY_DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
|
mNodesViewModel.nodesToPermanentlyDelete.observe(this) { nodes ->
|
||||||
|
deleteDatabaseNodes(nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveDatabase.observe(this) { save ->
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSave(save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.mergeDatabase.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseMerge()
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
|
||||||
|
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveName.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveDescription.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveDescription(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveDefaultUsername.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveDefaultUsername(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveColor.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveColor(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveCompression.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveCompression(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.removeUnlinkData.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseRemoveUnlinkedData(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveRecycleBin.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveRecycleBin(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveTemplatesGroup.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveTemplatesGroup(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveMaxHistoryItems.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveMaxHistoryItems(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveMaxHistorySize.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveEncryption.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveEncryption(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveKeyDerivation.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveKeyDerivation(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveIterations.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveIterations(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveMemoryUsage.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseViewModel.saveParallelism.observe(this) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseSaveParallelism(it.oldValue, it.newValue, it.save)
|
||||||
|
}
|
||||||
|
|
||||||
|
mExitLock = false
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun finishActivityIfDatabaseNotLoaded(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: Database?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
|
// End activity if database not loaded
|
||||||
|
if (finishActivityIfDatabaseNotLoaded() && (database == null || !database.loaded)) {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus view to reinitialize timeout,
|
||||||
|
// view is not necessary loaded so retry later in resume
|
||||||
|
viewToInvalidateTimeout()
|
||||||
|
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database?.loaded)
|
||||||
|
|
||||||
|
database?.let {
|
||||||
|
// check timeout
|
||||||
|
if (mTimeoutEnable) {
|
||||||
|
if (mLockReceiver == null) {
|
||||||
|
mLockReceiver = LockReceiver {
|
||||||
|
mDatabase = null
|
||||||
|
closeDatabase(database)
|
||||||
|
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
|
||||||
|
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
|
||||||
|
mExitLock = true
|
||||||
|
closeOptionsMenu()
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
registerLockReceiver(mLockReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
// After the first creation
|
||||||
|
// or If simply swipe with another application
|
||||||
|
// If the time is out -> close the Activity
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
||||||
|
// If onCreate already record time
|
||||||
|
if (!mExitLock)
|
||||||
|
TimeoutHelper.recordTime(this, database.loaded)
|
||||||
|
}
|
||||||
|
|
||||||
|
mDatabaseReadOnly = database.isReadOnly
|
||||||
|
mMergeDataAllowed = database.isMergeDataAllowed()
|
||||||
|
mIconDrawableFactory = database.iconDrawableFactory
|
||||||
|
|
||||||
|
checkRegister()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun viewToInvalidateTimeout(): View?
|
||||||
|
|
||||||
|
override fun onDatabaseActionFinished(
|
||||||
|
database: Database,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
|
when (actionTask) {
|
||||||
|
DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK,
|
||||||
|
DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
|
||||||
|
// Reload the current activity
|
||||||
|
if (result.isSuccess) {
|
||||||
|
reloadActivity()
|
||||||
|
} else {
|
||||||
|
this.showActionErrorIfNeeded(result)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPasswordEncodingValidateListener(databaseUri: Uri?,
|
||||||
|
mainCredential: MainCredential) {
|
||||||
|
assignDatabasePassword(databaseUri, mainCredential)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assignDatabasePassword(databaseUri: Uri?,
|
||||||
|
mainCredential: MainCredential) {
|
||||||
|
if (databaseUri != null) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseAssignPassword(databaseUri, mainCredential)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assignPassword(mainCredential: MainCredential) {
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
database.fileUri?.let { databaseUri ->
|
||||||
|
// Show the progress dialog now or after dialog confirmation
|
||||||
|
if (database.validatePasswordEncoding(mainCredential)) {
|
||||||
|
assignDatabasePassword(databaseUri, mainCredential)
|
||||||
|
} else {
|
||||||
|
PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
|
||||||
|
.show(supportFragmentManager, "passwordEncodingTag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveDatabase() {
|
||||||
|
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() {
|
||||||
|
mDatabaseTaskProvider?.askToStartDatabaseReload(mDatabase?.dataModifiedSinceLastLoading != false) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseReload(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createEntry(newEntry: Entry,
|
||||||
|
parent: Group) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseCreateEntry(newEntry, parent, mAutoSaveEnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateEntry(oldEntry: Entry,
|
||||||
|
entryToUpdate: Entry) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseUpdateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun copyNodes(nodesToCopy: List<Node>,
|
||||||
|
newParent: Group) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseCopyNodes(nodesToCopy, newParent, mAutoSaveEnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun moveNodes(nodesToMove: List<Node>,
|
||||||
|
newParent: Group) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseMoveNodes(nodesToMove, newParent, mAutoSaveEnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun eachNodeRecyclable(database: Database, nodes: List<Node>): Boolean {
|
||||||
|
return nodes.find { node ->
|
||||||
|
var cannotRecycle = true
|
||||||
|
if (node is Entry) {
|
||||||
|
cannotRecycle = !database.canRecycle(node)
|
||||||
|
} else if (node is Group) {
|
||||||
|
cannotRecycle = !database.canRecycle(node)
|
||||||
|
}
|
||||||
|
cannotRecycle
|
||||||
|
} == null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false) {
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
// If recycle bin enabled, ensure it exists
|
||||||
|
if (database.isRecycleBinEnabled) {
|
||||||
|
database.ensureRecycleBinExists(resources)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If recycle bin enabled and not in recycle bin, move in recycle bin
|
||||||
|
if (eachNodeRecyclable(database, nodes)) {
|
||||||
|
deleteDatabaseNodes(nodes)
|
||||||
|
}
|
||||||
|
// else open the dialog to confirm deletion
|
||||||
|
else {
|
||||||
|
DeleteNodesDialogFragment.getInstance(recycleBin)
|
||||||
|
.show(supportFragmentManager, "deleteNodesDialogFragment")
|
||||||
|
mNodesViewModel.deleteNodes(nodes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteDatabaseNodes(nodes: List<Node>) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseDeleteNodes(nodes, mAutoSaveEnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createGroup(parent: Group,
|
||||||
|
groupInfo: GroupInfo?) {
|
||||||
|
// Build the group
|
||||||
|
mDatabase?.createGroup()?.let { newGroup ->
|
||||||
|
groupInfo?.let { info ->
|
||||||
|
newGroup.setGroupInfo(info)
|
||||||
|
}
|
||||||
|
// Not really needed here because added in runnable but safe
|
||||||
|
newGroup.parent = parent
|
||||||
|
mDatabaseTaskProvider?.startDatabaseCreateGroup(newGroup, parent, mAutoSaveEnable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateGroup(oldGroup: Group,
|
||||||
|
groupInfo: GroupInfo) {
|
||||||
|
// If group updated save it in the database
|
||||||
|
val updateGroup = Group(oldGroup).let { updateGroup ->
|
||||||
|
updateGroup.apply {
|
||||||
|
// WARNING remove parent and children to keep memory
|
||||||
|
removeParent()
|
||||||
|
removeChildren()
|
||||||
|
this.setGroupInfo(groupInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mDatabaseTaskProvider?.startDatabaseUpdateGroup(oldGroup, updateGroup, mAutoSaveEnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreEntryHistory(mainEntryId: NodeId<UUID>,
|
||||||
|
entryHistoryPosition: Int) {
|
||||||
|
mDatabaseTaskProvider
|
||||||
|
?.startDatabaseRestoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteEntryHistory(mainEntryId: NodeId<UUID>,
|
||||||
|
entryHistoryPosition: Int) {
|
||||||
|
mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkRegister() {
|
||||||
|
// If in ave or registration mode, don't allow read only
|
||||||
|
if ((mSpecialMode == SpecialMode.SAVE
|
||||||
|
|| mSpecialMode == SpecialMode.REGISTRATION)
|
||||||
|
&& mDatabaseReadOnly) {
|
||||||
|
Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
|
||||||
|
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// To refresh when back to normal workflow from selection workflow
|
||||||
|
mAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(this)
|
||||||
|
|
||||||
|
// Invalidate timeout by touch
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
viewToInvalidateTimeout()
|
||||||
|
?.resetAppTimeoutWhenViewTouchedOrFocused(this, database.loaded)
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
|
||||||
|
LOCKING_ACTIVITY_UI_VISIBLE = true
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun checkTimeAndLockIfTimeoutOrResetTimeout(action: (() -> Unit)? = null) {
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
|
||||||
|
mDatabase?.loaded == true,
|
||||||
|
action)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable)
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
LOCKING_ACTIVITY_UI_VISIBLE = false
|
||||||
|
|
||||||
|
super.onPause()
|
||||||
|
|
||||||
|
if (mTimeoutEnable) {
|
||||||
|
// If the time is out during our navigation in activity -> close the Activity
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeout(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
unregisterLockReceiver(mLockReceiver)
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun lockAndExit() {
|
||||||
|
// Ask confirmation if modification not saved
|
||||||
|
if (mDatabase?.isReadOnly == false
|
||||||
|
&& mDatabase?.dataModifiedSinceLastLoading == true
|
||||||
|
&& !PreferencesUtil.isAutoSaveDatabaseEnabled(this)) {
|
||||||
|
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() {
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
|
||||||
|
mDatabase?.loaded ?: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (mTimeoutEnable) {
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
|
||||||
|
mDatabase?.loaded == true) {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val TAG = "LockingActivity"
|
||||||
|
|
||||||
|
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
||||||
|
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
||||||
|
|
||||||
|
private var LOCKING_ACTIVITY_UI_VISIBLE = false
|
||||||
|
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To reset the app timeout when a view is focused or changed
|
||||||
|
*/
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
fun View.resetAppTimeoutWhenViewTouchedOrFocused(context: Context, databaseLoaded: Boolean?) {
|
||||||
|
try {
|
||||||
|
// Log.d(DatabaseLockActivity.TAG, "View prepared to reset app timeout")
|
||||||
|
setOnTouchListener { _, event ->
|
||||||
|
when (event.action) {
|
||||||
|
MotionEvent.ACTION_DOWN -> {
|
||||||
|
// Log.d(DatabaseLockActivity.TAG, "View touched, try to reset app timeout")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("AppTimeout", "Unable to reset app timeout", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,24 @@
|
|||||||
package com.kunzisoft.keepass.activities.selection
|
package com.kunzisoft.keepass.activities.legacy
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.view.View
|
import android.view.View
|
||||||
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.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.helpers.TypeMode
|
import com.kunzisoft.keepass.activities.helpers.TypeMode
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
|
||||||
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.view.SpecialModeView
|
import com.kunzisoft.keepass.view.SpecialModeView
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity to manage special mode (ie: selection mode)
|
* Activity to manage database special mode (ie: selection mode)
|
||||||
*/
|
*/
|
||||||
abstract class SpecialModeActivity : StylishActivity() {
|
abstract class DatabaseModeActivity : DatabaseActivity() {
|
||||||
|
|
||||||
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
|
protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT
|
||||||
private var mTypeMode: TypeMode = TypeMode.DEFAULT
|
private var mTypeMode: TypeMode = TypeMode.DEFAULT
|
||||||
@@ -64,8 +67,7 @@ abstract class SpecialModeActivity : StylishActivity() {
|
|||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,8 +80,7 @@ abstract class SpecialModeActivity : StylishActivity() {
|
|||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,11 +90,17 @@ abstract class SpecialModeActivity : StylishActivity() {
|
|||||||
// 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
|
||||||
|
// Not visible as opened with FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
||||||
|
moveTaskToBack(true)
|
||||||
|
// Not finish() to prevent service kill
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -161,12 +168,17 @@ abstract class SpecialModeActivity : StylishActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||