mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
1350 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
536e038306 | ||
|
|
9e1f6d29a5 | ||
|
|
7a9469e59d | ||
|
|
c12eb3d643 | ||
|
|
da0f02e536 | ||
|
|
04bcc6631c | ||
|
|
698e3b7fb1 | ||
|
|
6de02384c1 | ||
|
|
df3bd7e0a1 | ||
|
|
c8c232639f | ||
|
|
192d6eedd0 | ||
|
|
9cae3f0794 | ||
|
|
a680db9707 | ||
|
|
fe526089d7 | ||
|
|
dfd7ade416 | ||
|
|
3cd65345c5 | ||
|
|
2d398908de | ||
|
|
756454abc3 | ||
|
|
b7619b45b1 | ||
|
|
1369a3cad9 | ||
|
|
f46c062c4e | ||
|
|
0a0abef4d4 | ||
|
|
3a8245ee74 | ||
|
|
7be554a378 | ||
|
|
6c37f7b12c | ||
|
|
3a67ec09d5 | ||
|
|
dca800b1bb | ||
|
|
70665f110d | ||
|
|
3b39cafb99 | ||
|
|
2b5ecb2f84 | ||
|
|
e397b92c36 | ||
|
|
e273eb6e03 | ||
|
|
28b624afa3 | ||
|
|
fb1e6cdc3f | ||
|
|
8b6499d040 | ||
|
|
054af507ad | ||
|
|
ac9bb9b666 | ||
|
|
809e1929e5 | ||
|
|
a1b1338d67 | ||
|
|
bd4cacfab1 | ||
|
|
e0343bdc55 | ||
|
|
b743d004e2 | ||
|
|
4b20e035b2 | ||
|
|
afe5fddc50 | ||
|
|
d68ca1b51f | ||
|
|
061b087229 | ||
|
|
bb3a379965 | ||
|
|
593b5c6338 | ||
|
|
56f8a1bf9f | ||
|
|
962b547b36 | ||
|
|
6df8ff4310 | ||
|
|
52f17140b8 | ||
|
|
75c2bb4a87 | ||
|
|
f36f6c3155 | ||
|
|
b88b92c5b0 | ||
|
|
d2c569c4f0 | ||
|
|
cb1316564e | ||
|
|
245d3f7df2 | ||
|
|
3729b3c5a0 | ||
|
|
7ce5eb3c27 | ||
|
|
43defea85e | ||
|
|
8470c4e39b | ||
|
|
a41afb7f1e | ||
|
|
32d9cfbe29 | ||
|
|
7210652567 | ||
|
|
ab15967ad7 | ||
|
|
44df4ec181 | ||
|
|
7afe356082 | ||
|
|
87597553b8 | ||
|
|
27e5f58d5e | ||
|
|
762c946d35 | ||
|
|
21a927e3e9 | ||
|
|
f93bb7436a | ||
|
|
6294fddbba | ||
|
|
c5719dfaf2 | ||
|
|
673fd67f15 | ||
|
|
25524c48e9 | ||
|
|
631b924c33 | ||
|
|
fba12bc278 | ||
|
|
e809109bb2 | ||
|
|
0e31890624 | ||
|
|
0124021ce5 | ||
|
|
74db6bf77f | ||
|
|
efde33182e | ||
|
|
ec68b22330 | ||
|
|
bf7e014f8c | ||
|
|
40e1607698 | ||
|
|
4a132f06fe | ||
|
|
0396dd975d | ||
|
|
80a38c0c54 | ||
|
|
2aa6461094 | ||
|
|
258433b3b8 | ||
|
|
79e723545c | ||
|
|
a6b20455ef | ||
|
|
9659b55bf3 | ||
|
|
ca148ef546 | ||
|
|
322b21d645 | ||
|
|
ed2ba65ecf | ||
|
|
defc8b1c57 | ||
|
|
a90ecc56d8 | ||
|
|
2faa0ac320 | ||
|
|
e391fd59fe | ||
|
|
25df86606c | ||
|
|
811f33eb3f | ||
|
|
ca7e2ed89d | ||
|
|
6f4cd79e2c | ||
|
|
da1caf4b8b | ||
|
|
4a0a8e44ca | ||
|
|
6bc2c3481b | ||
|
|
0aa89ea9ff | ||
|
|
f31a30bf47 | ||
|
|
dc75837ac7 | ||
|
|
9849b0a1da | ||
|
|
2c15a1ddd6 | ||
|
|
98eb9976cf | ||
|
|
0d9a5810b1 | ||
|
|
1adaa137a5 | ||
|
|
44a428d15a | ||
|
|
5416a7942a | ||
|
|
9e0024baf5 | ||
|
|
8d47ce38c2 | ||
|
|
80af43c0ca | ||
|
|
225f8243c2 | ||
|
|
68bc118add | ||
|
|
abbc584402 | ||
|
|
6635594639 | ||
|
|
10db77d402 | ||
|
|
000fd7e520 | ||
|
|
c8ced4ae59 | ||
|
|
9209ca9af7 | ||
|
|
c7bd90c610 | ||
|
|
fceb9c3547 | ||
|
|
030c49b571 | ||
|
|
f2d6a6a536 | ||
|
|
8f61521f05 | ||
|
|
89af7ec5d0 | ||
|
|
362f1aebed | ||
|
|
5226527cec | ||
|
|
b8464cd0e5 | ||
|
|
46e7b04d66 | ||
|
|
73111b770f | ||
|
|
995d485700 | ||
|
|
5ebbbef667 | ||
|
|
c79144400f | ||
|
|
b56556f5a2 | ||
|
|
35d5f01b8e | ||
|
|
501c647236 | ||
|
|
e77c7b84a3 | ||
|
|
d9f4e9b6ab | ||
|
|
1d7f7d2a5b | ||
|
|
df408e862b | ||
|
|
66845926d5 | ||
|
|
0a06acbf1d | ||
|
|
5d3c6798c0 | ||
|
|
b9d2b9ddc9 | ||
|
|
8a6525f45e | ||
|
|
41d89b590d | ||
|
|
8354d08ff5 | ||
|
|
16725b21f3 | ||
|
|
93ba17792f | ||
|
|
940b96cf21 | ||
|
|
157253ce24 | ||
|
|
46b3810f34 | ||
|
|
05c030dbbb | ||
|
|
5b0fd99351 | ||
|
|
2946a6e231 | ||
|
|
47896fcdc9 | ||
|
|
bc51345f0d | ||
|
|
750e1b6c43 | ||
|
|
4a2106837c | ||
|
|
39b9fc350a | ||
|
|
53560dbe29 | ||
|
|
5172dbe114 | ||
|
|
4f0ff67fdf | ||
|
|
bac971ced8 | ||
|
|
64bf5ba165 | ||
|
|
90c4a5e1b8 | ||
|
|
b08a5d9cda | ||
|
|
d5be79948d | ||
|
|
6b64be4925 | ||
|
|
e8e6eb6ca5 | ||
|
|
ee9383dd0b | ||
|
|
aee0b82cff | ||
|
|
ba5913da57 | ||
|
|
3238b9b2ce | ||
|
|
5215181c0f | ||
|
|
d3676a1454 | ||
|
|
792ce6f86e | ||
|
|
ddbd0376fc | ||
|
|
496655093c | ||
|
|
755293eff7 | ||
|
|
b6e51a1f32 | ||
|
|
08f17f3f19 | ||
|
|
14440725fc | ||
|
|
b4f84c5cd6 | ||
|
|
ad6b1cead1 | ||
|
|
e06398ff19 | ||
|
|
919ad5cfd4 | ||
|
|
b3fb721588 | ||
|
|
590497852d | ||
|
|
ebd7a9c7cf | ||
|
|
939fb2fa54 | ||
|
|
2245daffe9 | ||
|
|
784b25ada8 | ||
|
|
146d631794 | ||
|
|
cc062d7f0e | ||
|
|
298dd4af61 | ||
|
|
739ba3b14d | ||
|
|
2864ea9868 | ||
|
|
ed8d3247ca | ||
|
|
ff5de7b327 | ||
|
|
aa77552ff4 | ||
|
|
12456c0ea2 | ||
|
|
a7a93fa2a2 | ||
|
|
ff7cd29b77 | ||
|
|
f7065acc40 | ||
|
|
e4e2e5c43c | ||
|
|
48d240b010 | ||
|
|
9f2deb56b9 | ||
|
|
951257bed8 | ||
|
|
f27ce804fb | ||
|
|
1d896e83b3 | ||
|
|
a2c2925610 | ||
|
|
97e5f90603 | ||
|
|
593b7188dc | ||
|
|
4c4e61a711 | ||
|
|
140f09c77e | ||
|
|
c0fe4faf8a | ||
|
|
066fff7aca | ||
|
|
6dcbdffed4 | ||
|
|
3a07dea6d7 | ||
|
|
72ed07ef17 | ||
|
|
51512b4588 | ||
|
|
aed49e19e8 | ||
|
|
113601d09a | ||
|
|
b7d0b65715 | ||
|
|
3075591885 | ||
|
|
e9596f56db | ||
|
|
3559830738 | ||
|
|
e42beccb22 | ||
|
|
d05b7394e8 | ||
|
|
00b11ea659 | ||
|
|
99f0f096d1 | ||
|
|
416329d50d | ||
|
|
cb0d1b05d7 | ||
|
|
a2845c33f8 | ||
|
|
6d06265d94 | ||
|
|
49d03efe56 | ||
|
|
7f13a3ca76 | ||
|
|
cec9d168e3 | ||
|
|
ecf253451d | ||
|
|
89a6219659 | ||
|
|
9dec308caa | ||
|
|
fb1459de9b | ||
|
|
f3bef64461 | ||
|
|
2509caff6b | ||
|
|
232aafe2c0 | ||
|
|
ed26cb4891 | ||
|
|
0b966a6cd1 | ||
|
|
c84afd2281 | ||
|
|
1711f09547 | ||
|
|
4460a44e99 | ||
|
|
c8f7bcfb52 | ||
|
|
bd2bd842af | ||
|
|
2b0f4fe46b | ||
|
|
55c2f41c71 | ||
|
|
94985422ca | ||
|
|
eaff1aa58f | ||
|
|
c7cb9d0990 | ||
|
|
a9fdf30421 | ||
|
|
d0f45f6dfb | ||
|
|
8cdb6a3c9f | ||
|
|
b369a46431 | ||
|
|
f40ca2d5e0 | ||
|
|
400393c677 | ||
|
|
5710e3be55 | ||
|
|
af1312a92b | ||
|
|
d2a63c48b1 | ||
|
|
8f10ea7ed6 | ||
|
|
7bd701368a | ||
|
|
894e846a62 | ||
|
|
504ef5a7ab | ||
|
|
af6436da77 | ||
|
|
d031420ed3 | ||
|
|
4b6b7478de | ||
|
|
bb1b5eab96 | ||
|
|
9486b50342 | ||
|
|
fc9a5b3545 | ||
|
|
8b4d0e2541 | ||
|
|
0ef07f615c | ||
|
|
836413cff2 | ||
|
|
c71e34fee9 | ||
|
|
b389a4db92 | ||
|
|
0a1a54cb33 | ||
|
|
630228675c | ||
|
|
f7955d00fc | ||
|
|
68b08f9b9a | ||
|
|
612f136ced | ||
|
|
8c313c3d48 | ||
|
|
517a6c0062 | ||
|
|
46c61b10de | ||
|
|
edc8d27577 | ||
|
|
05354777fe | ||
|
|
98d3d2a39b | ||
|
|
f91f75912e | ||
|
|
9f21f67035 | ||
|
|
699e4da112 | ||
|
|
6458285e75 | ||
|
|
e0ddf3711f | ||
|
|
68d415375d | ||
|
|
0e11afdd8b | ||
|
|
ff2c01584f | ||
|
|
a59e20b864 | ||
|
|
d7d898896d | ||
|
|
2dde78d5e7 | ||
|
|
2ca39ff399 | ||
|
|
1f6fdaf9b3 | ||
|
|
edaf58135f | ||
|
|
724698fc51 | ||
|
|
6aabe9e12c | ||
|
|
2b5a1bb893 | ||
|
|
7403305f3c | ||
|
|
e780f8a3f0 | ||
|
|
7f2fda0327 | ||
|
|
ada8f74e2c | ||
|
|
1ddfeaf950 | ||
|
|
2ed2cc1499 | ||
|
|
be0f90f12a | ||
|
|
4b362df23b | ||
|
|
b953a1c2f6 | ||
|
|
4c30fa43d3 | ||
|
|
6866b1a3bb | ||
|
|
328030f152 | ||
|
|
fd195bd926 | ||
|
|
87e07366cd | ||
|
|
8133977e09 | ||
|
|
11199b996c | ||
|
|
eee61db189 | ||
|
|
c7c5130030 | ||
|
|
6de14a3840 | ||
|
|
55206b3dde | ||
|
|
e1a44477af | ||
|
|
23afee453e | ||
|
|
8e05309021 | ||
|
|
26fdf87070 | ||
|
|
1c95a0edc4 | ||
|
|
4723fb39e9 | ||
|
|
13d667d81c | ||
|
|
dce255dc58 | ||
|
|
f72c9704d9 | ||
|
|
e623010e91 | ||
|
|
587bfdc162 | ||
|
|
7c1c299282 | ||
|
|
f2a5c0b04b | ||
|
|
4ba77b76ec | ||
|
|
0bfce44317 | ||
|
|
13b6d6384c | ||
|
|
80838bbef0 | ||
|
|
8a557ff2fb | ||
|
|
16e394087d | ||
|
|
ee1b67b36e | ||
|
|
36b9fa2387 | ||
|
|
378169e939 | ||
|
|
70a01e559d | ||
|
|
c09e8196f3 | ||
|
|
0382c05152 | ||
|
|
75cf6e2a56 | ||
|
|
9fb4754430 | ||
|
|
0312b504a9 | ||
|
|
1d6a9651bf | ||
|
|
e36b18e85e | ||
|
|
4ae4951e0d | ||
|
|
a65f52ffba | ||
|
|
6de25ffa65 | ||
|
|
d8429bdd99 | ||
|
|
26976ae6cf | ||
|
|
53beaca563 | ||
|
|
77628e2fb9 | ||
|
|
40d2f2de96 | ||
|
|
84cdb2483f | ||
|
|
c7866bfbbf | ||
|
|
5de1d6b343 | ||
|
|
7bde363704 | ||
|
|
1f9b2ce7b9 | ||
|
|
ac2e47776a | ||
|
|
e7de5ca263 | ||
|
|
f7f079e653 | ||
|
|
f7cccb33de | ||
|
|
8177c9c34b | ||
|
|
850c46f881 | ||
|
|
ffcfe966d2 | ||
|
|
800badd2a4 | ||
|
|
0a7ffbcc8f | ||
|
|
019ec4de9a | ||
|
|
78d1e4a12a | ||
|
|
6c99fefad0 | ||
|
|
24bc1424b9 | ||
|
|
94a0e17cfc | ||
|
|
2f012d8cf2 | ||
|
|
75b800054f | ||
|
|
d7c7733315 | ||
|
|
99600ad8d8 | ||
|
|
4a4c7b8b6b | ||
|
|
8c3267b345 | ||
|
|
c0240b047b | ||
|
|
c52957ccfe | ||
|
|
fb3f057adf | ||
|
|
e333bd08a4 | ||
|
|
1844a269cb | ||
|
|
859882d24f | ||
|
|
c95543b8b0 | ||
|
|
d874125dc1 | ||
|
|
795cd099f4 | ||
|
|
66ef6fd9d8 | ||
|
|
3b0655354d | ||
|
|
76acec93fd | ||
|
|
3b60068369 | ||
|
|
a0fd0a71a2 | ||
|
|
907accbcc9 | ||
|
|
ee284abf8d | ||
|
|
8239275770 | ||
|
|
029485bace | ||
|
|
cef9f6ae3b | ||
|
|
8d88c94956 | ||
|
|
692e155117 | ||
|
|
9a9410de2b | ||
|
|
a27f1181ea | ||
|
|
ac65cadb1b | ||
|
|
4345e75b20 | ||
|
|
f64d085e7b | ||
|
|
325b878f0a | ||
|
|
330f375a30 | ||
|
|
7d6c211de1 | ||
|
|
af5b36752c | ||
|
|
e0f563befb | ||
|
|
bf1b84dfea | ||
|
|
b3f8ce9c16 | ||
|
|
499152d066 | ||
|
|
c47e7edc9e | ||
|
|
afc034b495 | ||
|
|
e1d19741af | ||
|
|
8d2de40df6 | ||
|
|
f4b7db667f | ||
|
|
4032e52317 | ||
|
|
f9db4325d8 | ||
|
|
815824f76d | ||
|
|
8534672c33 | ||
|
|
b9bb9a166a | ||
|
|
185c886472 | ||
|
|
acc565d021 | ||
|
|
64db137c6c | ||
|
|
4f2bdeb2c9 | ||
|
|
527994084b | ||
|
|
ffaf5c9475 | ||
|
|
d1e24bfcd8 | ||
|
|
aab8612c63 | ||
|
|
79c1e2d21c | ||
|
|
7ff44f4839 | ||
|
|
2de0272e3e | ||
|
|
1d22fe72d0 | ||
|
|
7e9821551c | ||
|
|
8fb3cd71b9 | ||
|
|
b0f6f1f2ba | ||
|
|
df56dc4a4a | ||
|
|
b572ec4901 | ||
|
|
75759171c1 | ||
|
|
322fe185dd | ||
|
|
effe514ecb | ||
|
|
4ff9f69548 | ||
|
|
e1c7cb17da | ||
|
|
e6defd7770 | ||
|
|
bba9f45075 | ||
|
|
de1a2d1557 | ||
|
|
e995d06a26 | ||
|
|
63f54a1318 | ||
|
|
0828c9f901 | ||
|
|
51cd38450a | ||
|
|
0a4a720fea | ||
|
|
7f7ce171e0 | ||
|
|
87622cc6ec | ||
|
|
aaf2e64fdb | ||
|
|
c6f22e7ce5 | ||
|
|
749a2d19aa | ||
|
|
ed3491fbba | ||
|
|
f5e4b7cd6a | ||
|
|
0b614e81ee | ||
|
|
0a93b660cf | ||
|
|
71e104e4bd | ||
|
|
189c5de7ea | ||
|
|
a5e141c361 | ||
|
|
e76d8d4df7 | ||
|
|
d54c093985 | ||
|
|
086709e40f | ||
|
|
9477e32ec8 | ||
|
|
a66a0628ad | ||
|
|
ea018bd4c1 | ||
|
|
8e81ee3b75 | ||
|
|
9c326f9be9 | ||
|
|
7d2a0aa793 | ||
|
|
22d1442033 | ||
|
|
ffa32a5501 | ||
|
|
f50a6a8416 | ||
|
|
7a82b75ee2 | ||
|
|
350c0585b8 | ||
|
|
5c4454d3ed | ||
|
|
dfd1ae7a50 | ||
|
|
d16fdd062f | ||
|
|
d6432698fc | ||
|
|
1a9c1e8bc1 | ||
|
|
2d05c6cb13 | ||
|
|
05f689913f | ||
|
|
9e714c4192 | ||
|
|
e1ad1e31f8 | ||
|
|
b473bf1da8 | ||
|
|
dfdc6eecb6 | ||
|
|
e9145df77a | ||
|
|
4e824990d1 | ||
|
|
5a053378e1 | ||
|
|
e09306b6f3 | ||
|
|
21e577c1d3 | ||
|
|
9d85d0979c | ||
|
|
e4fec7ea1f | ||
|
|
c998e513fc | ||
|
|
855b06c4b2 | ||
|
|
776012f02f | ||
|
|
9bff531618 | ||
|
|
4d64818da1 | ||
|
|
837c8773a8 | ||
|
|
97a199b504 | ||
|
|
9cd37be5b1 | ||
|
|
90895fed52 | ||
|
|
df4b73abbb | ||
|
|
d2bed08ae0 | ||
|
|
fd5e1472e1 | ||
|
|
6a22126006 | ||
|
|
ff5e9049a0 | ||
|
|
7570aa9f49 | ||
|
|
4fcdd3da23 | ||
|
|
f8a65b4d13 | ||
|
|
5bb70a5043 | ||
|
|
c4788159e4 | ||
|
|
403b0aedca | ||
|
|
cae12b84df | ||
|
|
40dbbbf0a0 | ||
|
|
9e0da27484 | ||
|
|
7e6c2463d6 | ||
|
|
69a76db166 | ||
|
|
efd282e326 | ||
|
|
6471fef28c | ||
|
|
5a0691b70c | ||
|
|
27ea357348 | ||
|
|
e12de29cce | ||
|
|
a236be4e38 | ||
|
|
5300643f62 | ||
|
|
469b837daf | ||
|
|
84ca3e8982 | ||
|
|
bfb8be159a | ||
|
|
13d7af9c76 | ||
|
|
66d78497b9 | ||
|
|
32743f7e19 | ||
|
|
c2cc4451ff | ||
|
|
67370bbc3d | ||
|
|
1201ae1956 | ||
|
|
0eb2786ce5 | ||
|
|
bbdeb66f8e | ||
|
|
641698a652 | ||
|
|
15880e995a | ||
|
|
1e849b106b | ||
|
|
31bc405e86 | ||
|
|
6e69eee2a3 | ||
|
|
279b095f30 | ||
|
|
9821499cdb | ||
|
|
b76c39862a | ||
|
|
7948d234f5 | ||
|
|
50fc3aa9fc | ||
|
|
79a7adb05b | ||
|
|
c02770ec2a | ||
|
|
bf24033513 | ||
|
|
d97c87e90c | ||
|
|
6d29fdc448 | ||
|
|
d16f0dd3f6 | ||
|
|
d023c97049 | ||
|
|
73c8d63f0e | ||
|
|
30709de7e2 | ||
|
|
c3e560766e | ||
|
|
7c7889e87e | ||
|
|
6547ccecdd | ||
|
|
d467a591b0 | ||
|
|
42613ff480 | ||
|
|
3a3d4b9e54 | ||
|
|
0b5c0bcf74 | ||
|
|
5937f009e1 | ||
|
|
c7f3d2a64e | ||
|
|
41263307d7 | ||
|
|
f2ff52477e | ||
|
|
6baa5a0730 | ||
|
|
5a5cb8c866 | ||
|
|
a3a78366f7 | ||
|
|
d936292ed9 | ||
|
|
a3ded18f59 | ||
|
|
0969135f7f | ||
|
|
7e1c1a6324 | ||
|
|
aa43dd2a49 | ||
|
|
81fe9b6fc2 | ||
|
|
6807fdc1cc | ||
|
|
ac1590810f | ||
|
|
9dd4d77535 | ||
|
|
c7aa6a9d96 | ||
|
|
fc56db2f83 | ||
|
|
c58e56257d | ||
|
|
b27eb280a8 | ||
|
|
78c7f4078c | ||
|
|
28ccaffcf5 | ||
|
|
78739558d6 | ||
|
|
cd195d30de | ||
|
|
03bf752284 | ||
|
|
238fab3e1d | ||
|
|
fcd9af8f84 | ||
|
|
b44491ebbe | ||
|
|
1f8018fd5b | ||
|
|
1d67656fa0 | ||
|
|
64b8023d1a | ||
|
|
cc1697e7ec | ||
|
|
28943e77e8 | ||
|
|
575109da9f | ||
|
|
a99667d471 | ||
|
|
6a7420bd3a | ||
|
|
e8dbe05615 | ||
|
|
6b6566cd29 | ||
|
|
0001d31c2c | ||
|
|
974686e698 | ||
|
|
7b7063b9be | ||
|
|
55061a9469 | ||
|
|
c433fb643c | ||
|
|
02306385b6 | ||
|
|
432ac1bcec | ||
|
|
d9480e0c9a | ||
|
|
815fb911d6 | ||
|
|
68cbdae8e0 | ||
|
|
2d8f8aeef3 | ||
|
|
479bc7be71 | ||
|
|
cbc6df2e62 | ||
|
|
d16b4cfadb | ||
|
|
a97bad1f86 | ||
|
|
7198ffff43 | ||
|
|
e173159d13 | ||
|
|
1b54b79e88 | ||
|
|
bb3436615e | ||
|
|
809db61c35 | ||
|
|
88e53fcba8 | ||
|
|
fe68b7e294 | ||
|
|
b7b76d6da7 | ||
|
|
e2eae43fc9 | ||
|
|
f41ecec09c | ||
|
|
15ad4d11ef | ||
|
|
0cf9d98f14 | ||
|
|
612e642523 | ||
|
|
0ff77eb157 | ||
|
|
2b8935a5d7 | ||
|
|
afdc5c8460 | ||
|
|
91ba2dff2d | ||
|
|
2d26079c49 | ||
|
|
f13d99e0d1 | ||
|
|
798c95d8a8 | ||
|
|
ef77c2acfb | ||
|
|
11a98267a2 | ||
|
|
b2aa1155d0 | ||
|
|
d3182b8d2a | ||
|
|
f52d139acc | ||
|
|
87e9a38548 | ||
|
|
faa70c57b3 | ||
|
|
5172c07c18 | ||
|
|
a80fa03db4 | ||
|
|
d73e02948e | ||
|
|
283657e1b7 | ||
|
|
d3c4a3a17e | ||
|
|
9184bc40e5 | ||
|
|
84bd98ebf4 | ||
|
|
4ef2cbcaeb | ||
|
|
35f8b45bf4 | ||
|
|
5d6aac2d1b | ||
|
|
c6723ecd4e | ||
|
|
838c8f48d3 | ||
|
|
65229fae1f | ||
|
|
f25ea89160 | ||
|
|
18401d5d1e | ||
|
|
1f2cf08108 | ||
|
|
74e86badba | ||
|
|
31444a823e | ||
|
|
068933f0fb | ||
|
|
f496711280 | ||
|
|
657d2420d6 | ||
|
|
2467721265 | ||
|
|
d37fbb9992 | ||
|
|
19b8b54dae | ||
|
|
84328caf3c | ||
|
|
a0bdfc973a | ||
|
|
1f91854490 | ||
|
|
1380325b66 | ||
|
|
d244eef62e | ||
|
|
8fb8d9ed37 | ||
|
|
8ce63cb5c5 | ||
|
|
9ecf2ae942 | ||
|
|
544f7003f6 | ||
|
|
6d633c9986 | ||
|
|
1e77a42c93 | ||
|
|
d1f2641e40 | ||
|
|
4b8ae154cc | ||
|
|
0c1aacdf83 | ||
|
|
5f34df3549 | ||
|
|
f2e6aa1abb | ||
|
|
866731df81 | ||
|
|
5d931e09d5 | ||
|
|
fe17c21c01 | ||
|
|
085941019e | ||
|
|
24b3758545 | ||
|
|
9083f99325 | ||
|
|
2189be9267 | ||
|
|
43218eede1 | ||
|
|
d1a176d27d | ||
|
|
cf51af91bf | ||
|
|
02ff1188b2 | ||
|
|
0fac9b6864 | ||
|
|
b550830c30 | ||
|
|
6f485dd298 | ||
|
|
b0dfde62c7 | ||
|
|
686dae0af6 | ||
|
|
ee3eabe8c8 | ||
|
|
521c8aa6a9 | ||
|
|
66207d599f | ||
|
|
e0029e0c3f | ||
|
|
3683b64721 | ||
|
|
8d4a0971f9 | ||
|
|
e4c3baa344 | ||
|
|
1e60d7e637 | ||
|
|
262b0227c1 | ||
|
|
226e461324 | ||
|
|
151eb26d56 | ||
|
|
335e767426 | ||
|
|
d212fa180b | ||
|
|
bc4ea2ec2a | ||
|
|
5d8c80fc1e | ||
|
|
a02714ff6e | ||
|
|
4bd952e223 | ||
|
|
91bbc6d84e | ||
|
|
6dbd16c5f6 | ||
|
|
76e040c585 | ||
|
|
8de6382a64 | ||
|
|
53532ead9f | ||
|
|
f4e6baeac2 | ||
|
|
5c46fdf41a | ||
|
|
22073e4bbd | ||
|
|
41e7376b7b | ||
|
|
3fc26c8c4e | ||
|
|
14f070a942 | ||
|
|
c078bd05e2 | ||
|
|
8ce9757b7c | ||
|
|
e028738dc2 | ||
|
|
9f4a302b72 | ||
|
|
2ef17e0c7a | ||
|
|
b86a8c8633 | ||
|
|
5a3be0853e | ||
|
|
99568db10c | ||
|
|
bf892f5b6a | ||
|
|
8e2c7ba1f0 | ||
|
|
fd3bb4b243 | ||
|
|
7f4a1d6896 | ||
|
|
d62734e8ac | ||
|
|
fbebc12a38 | ||
|
|
3c65be2a72 | ||
|
|
a29a9f28ef | ||
|
|
eb14dadb3c | ||
|
|
8d926a306b | ||
|
|
5699359099 | ||
|
|
e3176033dc | ||
|
|
9df6215c02 | ||
|
|
93a0e4c0a6 | ||
|
|
f55a824cdc | ||
|
|
766026d3be | ||
|
|
c64fc56496 | ||
|
|
6e2fb21431 | ||
|
|
2bb70abc39 | ||
|
|
a6cb1dbe5c | ||
|
|
5222a72cc6 | ||
|
|
5d3aa44545 | ||
|
|
61cfda93a5 | ||
|
|
b490295b90 | ||
|
|
61035ca47b | ||
|
|
1dc08bbfef | ||
|
|
9ea7c86da7 | ||
|
|
4fa3fb86cb | ||
|
|
df089f4415 | ||
|
|
6be12eb440 | ||
|
|
84efd1c497 | ||
|
|
4817654d58 | ||
|
|
70d45e0bba | ||
|
|
a4c7e3860b | ||
|
|
2a890091d7 | ||
|
|
cc593e6e1f | ||
|
|
552684fd90 | ||
|
|
a260e1d4e3 | ||
|
|
07bbf232b6 | ||
|
|
64b4a2f317 | ||
|
|
f0455fbca6 | ||
|
|
2fa973f34a | ||
|
|
470497b8ef | ||
|
|
215cc641c3 | ||
|
|
a98fa0d7bb | ||
|
|
c70841d3e5 | ||
|
|
3fdcd3c43d | ||
|
|
b32bb609de | ||
|
|
4db03fe034 | ||
|
|
68430ef62c | ||
|
|
d46f48f317 | ||
|
|
e68e4dac46 | ||
|
|
3a79f94698 | ||
|
|
b913913bf1 | ||
|
|
16f92fc895 | ||
|
|
189e98b3fe | ||
|
|
64fea078e8 | ||
|
|
4ad5d35be7 | ||
|
|
82015953b9 | ||
|
|
630318dbef | ||
|
|
d289e8acaa | ||
|
|
4632a202a9 | ||
|
|
4a9282ee53 | ||
|
|
45ec38ecab | ||
|
|
b82fc4659a | ||
|
|
aade780f92 | ||
|
|
6001f68cce | ||
|
|
d0a072554e | ||
|
|
53ccac0e9c | ||
|
|
14e2a3f79e | ||
|
|
9afe5aaaf4 | ||
|
|
b992c9eb65 | ||
|
|
fa2a79bc6c | ||
|
|
b1d4310b93 | ||
|
|
1811b02fc8 | ||
|
|
5bbf436e4e | ||
|
|
d47a12c3d9 | ||
|
|
dd7f6b389c | ||
|
|
e829e3eedf | ||
|
|
178de4dd99 | ||
|
|
a4cfc8ff49 | ||
|
|
b607cce691 | ||
|
|
5986f39041 | ||
|
|
8ab39aaea6 | ||
|
|
4114d5aeb1 | ||
|
|
2a1a5d9bd4 | ||
|
|
72b347bc85 | ||
|
|
44628f6fc4 | ||
|
|
9191fd4a21 | ||
|
|
02c4eba676 | ||
|
|
a45b5605a1 | ||
|
|
8570f47372 | ||
|
|
71fd17c0ac | ||
|
|
eb4e45cb69 | ||
|
|
3891bb1c8f | ||
|
|
a3c5237c2b | ||
|
|
e05b904e78 | ||
|
|
0423dd7295 | ||
|
|
8fa8043ff4 | ||
|
|
244174e494 | ||
|
|
db8920ae69 | ||
|
|
670e949e9f | ||
|
|
c4daafea6d | ||
|
|
c8f07ffa7c | ||
|
|
7f721427b6 | ||
|
|
ad670151c8 | ||
|
|
800335e416 | ||
|
|
2da58f5899 | ||
|
|
0850b19aec | ||
|
|
b6d32999b9 | ||
|
|
4f96a300c2 | ||
|
|
c630b9d0c0 | ||
|
|
7324f630ba | ||
|
|
519da9c4ef | ||
|
|
6fc26ef6e6 | ||
|
|
dfb7e6f3bd | ||
|
|
5a883b83a5 | ||
|
|
dfef51fd27 | ||
|
|
8bb7c7c8af | ||
|
|
98370a03b5 | ||
|
|
82875ff832 | ||
|
|
9b4b5914d6 | ||
|
|
7b9576841d | ||
|
|
59794557b3 | ||
|
|
c3e4504a1a | ||
|
|
65d7fdbf7e | ||
|
|
d5310e5c58 | ||
|
|
f02b7c16d2 | ||
|
|
7cd456ecbc | ||
|
|
1aeff9684e | ||
|
|
4b0ad75e7b | ||
|
|
055b3c6002 | ||
|
|
9ad4858224 | ||
|
|
33422f90ce | ||
|
|
b9f4f9380b | ||
|
|
56a1601d79 | ||
|
|
a7d04e573a | ||
|
|
0c4d827469 | ||
|
|
5656f6afb5 | ||
|
|
4bd12b8a95 | ||
|
|
939b319563 | ||
|
|
05865fe8c3 | ||
|
|
09cf8aabf6 | ||
|
|
111a77d984 | ||
|
|
c2cc0a77ab | ||
|
|
5b052b02a2 | ||
|
|
3b3908dd7c | ||
|
|
1b96747187 | ||
|
|
13f2003fed | ||
|
|
4d6dbc9e30 | ||
|
|
a235b21fbf | ||
|
|
158ed26f6f | ||
|
|
81930bf3a1 | ||
|
|
e1ecebb4a0 | ||
|
|
52fd576f55 | ||
|
|
6742a8893c | ||
|
|
7db0c4f7d3 | ||
|
|
372b2aebe0 | ||
|
|
b713237ec3 | ||
|
|
ee1a0a53a6 | ||
|
|
45ece887a2 | ||
|
|
0f59aaf4f7 | ||
|
|
0d2c814b3d | ||
|
|
64bdcc2f32 | ||
|
|
dfb418c12a | ||
|
|
c8868f31e6 | ||
|
|
48cc8b3f75 | ||
|
|
62777373a4 | ||
|
|
ce987237ae | ||
|
|
77b99f9ef4 | ||
|
|
049ac4cb21 | ||
|
|
41e3c60632 | ||
|
|
5b2c846f7d | ||
|
|
8a0192bcac | ||
|
|
3d2fce78c6 | ||
|
|
120bfd47e2 | ||
|
|
408ac08059 | ||
|
|
9ae2377bd9 | ||
|
|
f126a9ab09 | ||
|
|
c2d4bf67d0 | ||
|
|
d0367ea7f8 | ||
|
|
73c6c97637 | ||
|
|
3f63fa9c30 | ||
|
|
2884d12b4b | ||
|
|
24dd966eab | ||
|
|
47b2464297 | ||
|
|
08bb11a4d7 | ||
|
|
e0c2c14b98 | ||
|
|
481b818619 | ||
|
|
138efebd8c | ||
|
|
3251f02ab2 | ||
|
|
8f74cdfac0 | ||
|
|
75f795b6ab | ||
|
|
dd901f3b16 | ||
|
|
cf2d641303 | ||
|
|
489c820ab9 | ||
|
|
dd21382b29 | ||
|
|
05b2ccf0ed | ||
|
|
428efd2bb6 | ||
|
|
512b4cd16e | ||
|
|
4d648b77b9 | ||
|
|
02f6caf4dd | ||
|
|
2b23649674 | ||
|
|
1693be7776 | ||
|
|
c97be80a3a | ||
|
|
29d4d5232d | ||
|
|
90ee9d8c40 | ||
|
|
5c0a82ba38 | ||
|
|
13aee1072f | ||
|
|
b158cf1323 | ||
|
|
22be5ecbca | ||
|
|
4b16d9394a | ||
|
|
c0ad54219f | ||
|
|
4af6f776b0 | ||
|
|
76373658d8 | ||
|
|
f9f6e69a95 | ||
|
|
61354df795 | ||
|
|
3a5c42b138 | ||
|
|
55e7544739 | ||
|
|
ad0dcdd54a | ||
|
|
caa208c847 | ||
|
|
067d00376d | ||
|
|
07f0521ef9 | ||
|
|
3be3593b1b | ||
|
|
71db66d174 | ||
|
|
cd58a8e1b5 | ||
|
|
256a579b1c | ||
|
|
97a427ea24 | ||
|
|
b848587901 | ||
|
|
fd90af3757 | ||
|
|
cb1c9e1103 | ||
|
|
c14092d52b | ||
|
|
27c19bd708 | ||
|
|
13d5273314 | ||
|
|
a0f93b5441 | ||
|
|
4c4c92527e | ||
|
|
ff9999f39c | ||
|
|
45d06f269c | ||
|
|
acf92c0f8e | ||
|
|
4b7de3b85f | ||
|
|
0502bfb986 | ||
|
|
eacecd9b0d | ||
|
|
38245b4807 | ||
|
|
95228e67c2 | ||
|
|
33cb4a55f5 | ||
|
|
7beb13d859 | ||
|
|
f0e0d26923 | ||
|
|
5326322356 | ||
|
|
ebe1c0bcdd | ||
|
|
42100d4cb9 | ||
|
|
369f3d240e | ||
|
|
d40e83fb52 | ||
|
|
6776b0867f | ||
|
|
5b1e945883 | ||
|
|
2f2f5e6f43 | ||
|
|
dfc7654842 | ||
|
|
183015707f | ||
|
|
2bc4908452 | ||
|
|
213e5c40d7 | ||
|
|
e7cce21c51 | ||
|
|
128ce5657e | ||
|
|
861911ad63 | ||
|
|
9093c65235 | ||
|
|
e6ab8f82ff | ||
|
|
77ff1850f3 | ||
|
|
e985bd2a20 | ||
|
|
a1e91bb26f | ||
|
|
a6cd02d146 | ||
|
|
eb51f6712b | ||
|
|
0a19ecb715 | ||
|
|
d7e7020244 | ||
|
|
ea3349eea4 | ||
|
|
9cec64ded4 | ||
|
|
f9dc456032 | ||
|
|
6416aad823 | ||
|
|
10b5c9c261 | ||
|
|
bc0e364164 | ||
|
|
83ab5223a8 | ||
|
|
94c6710f22 | ||
|
|
a1e5266161 | ||
|
|
adbdb9a642 | ||
|
|
527f734fcf | ||
|
|
f1a29af0c6 | ||
|
|
b69a277769 | ||
|
|
d049ed39e8 | ||
|
|
ecfe767068 | ||
|
|
8d827eb562 | ||
|
|
9048f618c5 | ||
|
|
66377ded62 | ||
|
|
04c4c82953 | ||
|
|
ccd8467cba | ||
|
|
4eee9e95c4 | ||
|
|
c64b4b8d62 | ||
|
|
72ef0f2f3f | ||
|
|
9b5a95a10f | ||
|
|
1a3baa6523 | ||
|
|
a745ed1c28 | ||
|
|
39ff112b42 | ||
|
|
b3aab27c9b | ||
|
|
9b3c751a49 | ||
|
|
d29f4e0097 | ||
|
|
0645fbe938 | ||
|
|
ee6c8fc041 | ||
|
|
d93e3a1c2d | ||
|
|
d4e0c008b8 | ||
|
|
0ca392f312 | ||
|
|
c6c14c2354 | ||
|
|
652616226b | ||
|
|
d2101fd3e5 | ||
|
|
3b967dd4e3 | ||
|
|
e5d58721dd | ||
|
|
1495c442ac | ||
|
|
2206184bcb | ||
|
|
9e0b6fa800 | ||
|
|
b11533f9fe | ||
|
|
50343193e1 | ||
|
|
7d7e3f4ad6 | ||
|
|
e22d9f6bdf | ||
|
|
affcc28f13 | ||
|
|
7a151bc2fe | ||
|
|
5349c4783e | ||
|
|
96a72d9842 | ||
|
|
04eae1ae3d | ||
|
|
67afa55f1d | ||
|
|
efa8fd9f17 | ||
|
|
64128991a6 | ||
|
|
3fa38c29f6 | ||
|
|
86a335768c | ||
|
|
2d29092c52 | ||
|
|
2d7e76a279 | ||
|
|
1514dbb1de | ||
|
|
1707d3c3ba | ||
|
|
3fee162c4d | ||
|
|
78fd9d616b | ||
|
|
46828c3317 | ||
|
|
5126bd4fb6 | ||
|
|
6440e5e054 | ||
|
|
0474e5b1fe | ||
|
|
15b84739e7 | ||
|
|
4bfe296e1a | ||
|
|
3fcc0db4f8 | ||
|
|
a8238565f3 | ||
|
|
53114462b3 | ||
|
|
094da79cea | ||
|
|
f093206a1c | ||
|
|
5b4940d017 | ||
|
|
aee36eeec6 | ||
|
|
cc783f8be1 | ||
|
|
a20f491cf1 | ||
|
|
f5ff9bf263 | ||
|
|
dcba54b499 | ||
|
|
d1e103c1d7 | ||
|
|
542daba206 | ||
|
|
94b61b1bbd | ||
|
|
718e590bfd | ||
|
|
9ce5e184c0 | ||
|
|
dbc477765f | ||
|
|
92fcadf3f3 | ||
|
|
c437fd96a8 | ||
|
|
10d33ecb82 | ||
|
|
d506c0cb27 | ||
|
|
acac7f7540 | ||
|
|
c853bd282a | ||
|
|
ecc4550261 | ||
|
|
c26ece7166 | ||
|
|
88b50c7902 | ||
|
|
7ba2cdd6ff | ||
|
|
589d9a2f1d | ||
|
|
a92411b95b | ||
|
|
472051bd24 | ||
|
|
e5109a1f43 | ||
|
|
f7ae9e3574 | ||
|
|
170ec3c636 | ||
|
|
1ab3fa8b3b | ||
|
|
8b046512e3 | ||
|
|
228a10c8e0 | ||
|
|
9c53bea190 | ||
|
|
11cf991498 | ||
|
|
a88c3721b2 | ||
|
|
0b4b6d4d91 | ||
|
|
941f9bcd48 | ||
|
|
09988a858d | ||
|
|
f1bf9fb25c | ||
|
|
1751fa49c0 | ||
|
|
6b4fc9a4fa | ||
|
|
7c8d85e428 | ||
|
|
e335140f23 | ||
|
|
d85f398b5f | ||
|
|
a16082a59d | ||
|
|
456269a343 | ||
|
|
eb8e1e20eb | ||
|
|
ed3c84fec0 | ||
|
|
be40416a2d | ||
|
|
5b5476a513 | ||
|
|
dc64dd6400 | ||
|
|
eca02d3bde | ||
|
|
176b6c2936 | ||
|
|
5b22350bdf | ||
|
|
6e1e011234 | ||
|
|
ac65ef6a5c | ||
|
|
fc198dde74 | ||
|
|
15ac51b2fc | ||
|
|
34214432e1 | ||
|
|
361ca92493 | ||
|
|
e367051b80 | ||
|
|
a2a4a50c5e | ||
|
|
afc74b2f2a | ||
|
|
fc756d1eaf | ||
|
|
eb8a4b1e49 | ||
|
|
8d258b3538 | ||
|
|
a59cfa3477 | ||
|
|
f1e513006e | ||
|
|
9df5c8f439 | ||
|
|
3ae099accf | ||
|
|
bb3e9396f2 | ||
|
|
1628749bde | ||
|
|
f2006b5e42 | ||
|
|
80d387d9e7 | ||
|
|
4452b4d599 | ||
|
|
dfeaeb9888 | ||
|
|
7e45a20ee7 | ||
|
|
f3fe92e4de | ||
|
|
b606909c65 | ||
|
|
2882bb30d7 | ||
|
|
5b62227e3f | ||
|
|
8b6af6fd8a | ||
|
|
99e9a92953 | ||
|
|
9f626309c3 | ||
|
|
3fe7cf2bfd | ||
|
|
9b5c274b49 | ||
|
|
46b350e7ac | ||
|
|
22a4aeb108 | ||
|
|
332e116ba7 | ||
|
|
8b594a1a1f | ||
|
|
ab23ec6d4d | ||
|
|
0ef574d675 | ||
|
|
6d15a2462d | ||
|
|
24fcdeb7aa | ||
|
|
13905db732 | ||
|
|
6e1c8e5bec | ||
|
|
9aa1d11b94 | ||
|
|
6c9f359fae | ||
|
|
531ebcae85 | ||
|
|
fe9601b510 | ||
|
|
bdf7cc6ea0 | ||
|
|
1cfe02af6f | ||
|
|
647e3f9383 | ||
|
|
597f52799d | ||
|
|
a59e052ed8 | ||
|
|
11da0a4500 | ||
|
|
fd736bd1c2 | ||
|
|
ca6a4bfeef | ||
|
|
02e9debc42 | ||
|
|
9bc9bd8b95 | ||
|
|
d810f79b7a | ||
|
|
f3468951f1 | ||
|
|
7ec5badabb | ||
|
|
1ff2f501ca | ||
|
|
cfcb49e233 | ||
|
|
467df2020e | ||
|
|
a961b41de0 | ||
|
|
40e8d5225e | ||
|
|
bc755ae1df | ||
|
|
b1cb0c3786 | ||
|
|
090d0fa2db | ||
|
|
27918a12b0 | ||
|
|
ba1498b0b2 | ||
|
|
cbde96dd82 | ||
|
|
344118a755 | ||
|
|
259c8a4bd9 | ||
|
|
fe92e41e91 | ||
|
|
e58c2f2a99 | ||
|
|
f4d5bd1bea | ||
|
|
20b352cabe | ||
|
|
20e35f1a69 | ||
|
|
d963f56d0f | ||
|
|
aecfbc7728 | ||
|
|
5734df89f0 | ||
|
|
bdf9b864d4 | ||
|
|
1c0f1a036b | ||
|
|
327c9de464 | ||
|
|
8b2f994769 | ||
|
|
a5e53d872b | ||
|
|
1868d90693 | ||
|
|
da0c19e068 | ||
|
|
d1103d8db4 | ||
|
|
b2e92646a1 | ||
|
|
19bc2444bc | ||
|
|
831b649cbb | ||
|
|
ded3c204b9 | ||
|
|
23eec5f066 | ||
|
|
6c167090e1 | ||
|
|
7d9eca0d46 | ||
|
|
c551aff474 | ||
|
|
e627745358 | ||
|
|
5a30d9d2b5 | ||
|
|
0a46817bbc | ||
|
|
a4134fa8c8 | ||
|
|
683535a5a6 | ||
|
|
edb53112c2 | ||
|
|
83a77af520 | ||
|
|
df3ae17c7b | ||
|
|
4a1624a443 | ||
|
|
a8de9f9f9f | ||
|
|
3aa5b40acd | ||
|
|
8400f3e874 | ||
|
|
b40bca1913 | ||
|
|
7100257f31 | ||
|
|
17df1a4d8a | ||
|
|
d7a5209c68 | ||
|
|
076220eacd | ||
|
|
99a50f271a | ||
|
|
63d265da06 | ||
|
|
30e3624eb1 | ||
|
|
88f3713e28 | ||
|
|
90f0c22545 | ||
|
|
8deed8468d | ||
|
|
923ad26b1b | ||
|
|
3bc858e4c2 | ||
|
|
f5a7fa41a7 | ||
|
|
bf71d5508b | ||
|
|
b44c9cfc51 | ||
|
|
5b4338abae | ||
|
|
aa5adc28cb | ||
|
|
2dad013cc0 | ||
|
|
7ade66f3ac | ||
|
|
ed75a64b46 | ||
|
|
e156b80d91 | ||
|
|
e8f79ae467 | ||
|
|
90e4862280 | ||
|
|
438080d3d6 | ||
|
|
3c17605764 | ||
|
|
3f68bc0eda | ||
|
|
ecbee73eae | ||
|
|
3e4452da00 | ||
|
|
549c690b56 | ||
|
|
aabe06f29b | ||
|
|
82693c5cd3 | ||
|
|
37a4f26d2f | ||
|
|
ca94063c7b | ||
|
|
eadc4bf6c2 | ||
|
|
b1c307c86b | ||
|
|
1874a0056d | ||
|
|
48331f9552 | ||
|
|
f907aa578a | ||
|
|
41e2620cc1 | ||
|
|
e7a82b167a | ||
|
|
088c556b00 | ||
|
|
c80343b6d4 | ||
|
|
4e52a8cf60 | ||
|
|
1ed1d4233f | ||
|
|
6e4626bc02 | ||
|
|
2608ae247f | ||
|
|
785586bfe9 | ||
|
|
bdcbb177ae | ||
|
|
15ac365d79 | ||
|
|
debbcb753b | ||
|
|
69d73aeaa4 | ||
|
|
dffe53370f | ||
|
|
4334e6dcdf | ||
|
|
c2c6c093d5 | ||
|
|
77e539eec2 | ||
|
|
a57970210e | ||
|
|
1b31a46fb7 | ||
|
|
87f19c74fc | ||
|
|
bd157a9724 | ||
|
|
5a327eb0db | ||
|
|
4b9c0b0109 | ||
|
|
df6b75cdbb | ||
|
|
0b4f8c122b | ||
|
|
2a87eaf3e5 | ||
|
|
c52266f5cf | ||
|
|
3b21f8add2 | ||
|
|
63d426503f | ||
|
|
ffb7f80b26 | ||
|
|
a4fe92562f | ||
|
|
b9bd1d9d4b | ||
|
|
3b6c28488a | ||
|
|
875eb3500d | ||
|
|
3a88a2451c | ||
|
|
6800b73a4f | ||
|
|
983404e6d8 | ||
|
|
b95c0a18a7 | ||
|
|
36b317cad8 | ||
|
|
35d74888fb | ||
|
|
279bd16b74 | ||
|
|
2e0081b66c |
13
.github/FUNDING.yml
vendored
Normal file
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']
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -80,6 +80,9 @@ art/screen*.png
|
|||||||
art/logo_512.png
|
art/logo_512.png
|
||||||
art/store_screens/
|
art/store_screens/
|
||||||
|
|
||||||
|
# Release
|
||||||
|
releases/*
|
||||||
|
|
||||||
# Dir linux
|
# Dir linux
|
||||||
.directory
|
.directory
|
||||||
*/.directory
|
*/.directory
|
||||||
|
|||||||
121
CHANGELOG
121
CHANGELOG
@@ -1,3 +1,124 @@
|
|||||||
|
KeePassDX(4.1.4)
|
||||||
|
* Fix UnlockManager #2098 #2101
|
||||||
|
* Auto device unlock prompt #2105
|
||||||
|
* Small fixes ##2066
|
||||||
|
|
||||||
|
KeePassDX(4.1.3)
|
||||||
|
* Fix Autofill Registration #2089
|
||||||
|
* Fix Biometric errors #2081
|
||||||
|
* Fixed timestamp in copy file #1981 #1983
|
||||||
|
* Fix Template Email #1986
|
||||||
|
* Fix Search #2096
|
||||||
|
|
||||||
|
KeePassDX(4.1.2)
|
||||||
|
* Fix URL search #1940 #1946 #2003 #2040 #2044
|
||||||
|
* Fix Autofill popup #2054
|
||||||
|
* Fix Group notes #2053
|
||||||
|
* Fix Dialog background #2005 #2004 (Thx @codokie)
|
||||||
|
* Fix OTP configuration #2042 #2065 (Thx @Dev-ClayP)
|
||||||
|
* Fix small UI elements #1987 #2007 (Thx @ymcx)
|
||||||
|
* RTL layout support #2021 (Thx @codokie)
|
||||||
|
* App Metadata to translation #1823
|
||||||
|
|
||||||
|
KeePassDX(4.1.1)
|
||||||
|
* Fix date parser #1933
|
||||||
|
* Fix domain search #1820 #1936
|
||||||
|
|
||||||
|
KeePassDX(4.1.0)
|
||||||
|
* Generate keyfile #1290
|
||||||
|
* Hide template group #1894
|
||||||
|
* Group count sum recursively #421
|
||||||
|
* Fix date fields #1695 #1710
|
||||||
|
* Fix distinct domain names #1105 #1820
|
||||||
|
* Resets the advanced unlock expiration #1600
|
||||||
|
* Password entropy #1490 #1355
|
||||||
|
* Upgrade to API 34 (Android 14) #1730
|
||||||
|
* Small fixes #1711 #1831 #1780 #1821 #1863 #1889 #1289 #1600 #1467 #1870
|
||||||
|
|
||||||
|
KeePassDX(4.0.8)
|
||||||
|
* Fix graphical bug that prevented databases from being opened on some versions of Android #1848 #1850
|
||||||
|
|
||||||
|
KeePassDX(4.0.7)
|
||||||
|
* Prevent 0 Byte file with cache during a save exception #1620 #1594 #1680
|
||||||
|
* Fix inline suggestions in keyboard #1840
|
||||||
|
* Fix broken links by default #1755
|
||||||
|
* Fix UX by allowing validation in entry edition #1770
|
||||||
|
* Fix small bugs #1709
|
||||||
|
|
||||||
|
KeePassDX(4.0.6)
|
||||||
|
* Fix form filled recognition #1508 #1735 #1508 #1790 #1783 #1797 #1801 #1802 #1804 #1665
|
||||||
|
* Fix translations #1707 #1683 #1712
|
||||||
|
* Update APK verifier #1810
|
||||||
|
|
||||||
|
KeePassDX(4.0.5)
|
||||||
|
* Fix form filled recognition #1572 #1508
|
||||||
|
* Rollback password color #1686 #1490
|
||||||
|
|
||||||
|
KeePassDX(4.0.4)
|
||||||
|
* Fix form filled recognition #1572 #1677
|
||||||
|
* Fix device unlock #1682
|
||||||
|
* Fix password color #1490
|
||||||
|
|
||||||
|
KeePassDX(4.0.3)
|
||||||
|
* Fix "Save as" in Read Only mode #1666
|
||||||
|
* Fix username autofill #1665 #530 #1572 #1426 #1523 #1556 #1653 #1658 #1508 #1667
|
||||||
|
* Fix regex OTP recognition #1596
|
||||||
|
* Change password color dynamically #1490
|
||||||
|
* Small fixes #1641 #1656 #1649 #1400 #1674
|
||||||
|
|
||||||
|
KeePassDX(4.0.2)
|
||||||
|
* Fix Autofill with API 33
|
||||||
|
|
||||||
|
KeePassDX(4.0.1)
|
||||||
|
* Fix back lock #1635 #1629 #1634
|
||||||
|
* Fix lock button in settings #1630
|
||||||
|
* Improve theme translation #1631
|
||||||
|
|
||||||
|
KeePassDX(4.0.0)
|
||||||
|
* New UX/UI with Material 3 #1183 #1529 #1428 #1441 #1607
|
||||||
|
* Material You theme (follow system colors) #1469
|
||||||
|
* Refactoring inner code #1371
|
||||||
|
* Migration to API 33
|
||||||
|
* Cut, copy and delete from search #891 #1308 #1263
|
||||||
|
* Fix behaviors #1351 #874 #1327
|
||||||
|
* Fix bugs #1589 #1584 #1545 #1563 #1371 #1609
|
||||||
|
|
||||||
|
KeePassDX(3.5.1)
|
||||||
|
* Fix action dialog with YubiKey challenge-response #1506
|
||||||
|
|
||||||
|
KeePassDX(3.5.0)
|
||||||
|
* Support YubiKey challenge-response #8 #137
|
||||||
|
* Better exception management during database save #1346
|
||||||
|
* Add "Screenshot mode" setting #459 #1377 #1354 (Thx @GianpaMX)
|
||||||
|
* Hide clipboard sensitive text when copy entry field #1386
|
||||||
|
* Fix attachment download button #1401
|
||||||
|
* Add monochrome icon #1403 #1404 (Thx @Sandelinos)
|
||||||
|
* Fix lock with back button #1412 #1414 (Thx @ryg-git)
|
||||||
|
* Vanadium compatibility #1447 (Thx @flawedworld)
|
||||||
|
|
||||||
|
KeePassDX(3.4.5)
|
||||||
|
* Fix custom data in group (fix KeeShare) #1335
|
||||||
|
* Fix device credential unlocking #1344
|
||||||
|
* New clipboard manager #1343
|
||||||
|
* Keep screen on by default when viewing an entry
|
||||||
|
* Change the order of the search filters
|
||||||
|
* Fix searchable selection
|
||||||
|
|
||||||
|
KeePassDX(3.4.4)
|
||||||
|
* Fix crash in New Android 13 #1321
|
||||||
|
* Better backstack management for selection mode
|
||||||
|
* Prevent Tapjacking #1318
|
||||||
|
* Small changes #1298
|
||||||
|
|
||||||
|
KeePassDX(3.4.3)
|
||||||
|
* Remove "Select share info" setting for Magikeyboard #1304
|
||||||
|
* Fix quick search and better loadGroup implementation #1302
|
||||||
|
* Fix small bugs
|
||||||
|
|
||||||
|
KeePassDX(3.4.2)
|
||||||
|
* Fix service parameter and workflow to remove notification when service is killed
|
||||||
|
* Fix color
|
||||||
|
|
||||||
KeePassDX(3.4.1)
|
KeePassDX(3.4.1)
|
||||||
* Fix search mode with Magikeyboard #1292
|
* Fix search mode with Magikeyboard #1292
|
||||||
* Fix select another entry with Magikeyboard #1293
|
* Fix select another entry with Magikeyboard #1293
|
||||||
|
|||||||
10
Gemfile
Normal file
10
Gemfile
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Autogenerated by fastlane
|
||||||
|
#
|
||||||
|
# Ensure this file is checked in to source control!
|
||||||
|
|
||||||
|
source "https://rubygems.org"
|
||||||
|
|
||||||
|
gem 'fastlane'
|
||||||
|
|
||||||
|
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
|
||||||
|
eval_gemfile(plugins_path) if File.exist?(plugins_path)
|
||||||
230
Gemfile.lock
Normal file
230
Gemfile.lock
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
GEM
|
||||||
|
remote: https://rubygems.org/
|
||||||
|
specs:
|
||||||
|
CFPropertyList (3.0.7)
|
||||||
|
base64
|
||||||
|
nkf
|
||||||
|
rexml
|
||||||
|
addressable (2.8.7)
|
||||||
|
public_suffix (>= 2.0.2, < 7.0)
|
||||||
|
artifactory (3.0.17)
|
||||||
|
atomos (0.1.3)
|
||||||
|
aws-eventstream (1.4.0)
|
||||||
|
aws-partitions (1.1146.0)
|
||||||
|
aws-sdk-core (3.229.0)
|
||||||
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
|
aws-sigv4 (~> 1.9)
|
||||||
|
base64
|
||||||
|
bigdecimal
|
||||||
|
jmespath (~> 1, >= 1.6.1)
|
||||||
|
logger
|
||||||
|
aws-sdk-kms (1.110.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.228.0)
|
||||||
|
aws-sigv4 (~> 1.5)
|
||||||
|
aws-sdk-s3 (1.196.1)
|
||||||
|
aws-sdk-core (~> 3, >= 3.228.0)
|
||||||
|
aws-sdk-kms (~> 1)
|
||||||
|
aws-sigv4 (~> 1.5)
|
||||||
|
aws-sigv4 (1.12.1)
|
||||||
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
|
babosa (1.0.4)
|
||||||
|
base64 (0.3.0)
|
||||||
|
bigdecimal (3.2.2)
|
||||||
|
claide (1.1.0)
|
||||||
|
colored (1.2)
|
||||||
|
colored2 (3.1.2)
|
||||||
|
commander (4.6.0)
|
||||||
|
highline (~> 2.0.0)
|
||||||
|
declarative (0.0.20)
|
||||||
|
digest-crc (0.7.0)
|
||||||
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
|
domain_name (0.6.20240107)
|
||||||
|
dotenv (2.8.1)
|
||||||
|
emoji_regex (3.2.3)
|
||||||
|
excon (0.112.0)
|
||||||
|
faraday (1.10.4)
|
||||||
|
faraday-em_http (~> 1.0)
|
||||||
|
faraday-em_synchrony (~> 1.0)
|
||||||
|
faraday-excon (~> 1.1)
|
||||||
|
faraday-httpclient (~> 1.0)
|
||||||
|
faraday-multipart (~> 1.0)
|
||||||
|
faraday-net_http (~> 1.0)
|
||||||
|
faraday-net_http_persistent (~> 1.0)
|
||||||
|
faraday-patron (~> 1.0)
|
||||||
|
faraday-rack (~> 1.0)
|
||||||
|
faraday-retry (~> 1.0)
|
||||||
|
ruby2_keywords (>= 0.0.4)
|
||||||
|
faraday-cookie_jar (0.0.7)
|
||||||
|
faraday (>= 0.8.0)
|
||||||
|
http-cookie (~> 1.0.0)
|
||||||
|
faraday-em_http (1.0.0)
|
||||||
|
faraday-em_synchrony (1.0.1)
|
||||||
|
faraday-excon (1.1.0)
|
||||||
|
faraday-httpclient (1.0.1)
|
||||||
|
faraday-multipart (1.1.1)
|
||||||
|
multipart-post (~> 2.0)
|
||||||
|
faraday-net_http (1.0.2)
|
||||||
|
faraday-net_http_persistent (1.2.0)
|
||||||
|
faraday-patron (1.0.0)
|
||||||
|
faraday-rack (1.0.0)
|
||||||
|
faraday-retry (1.0.3)
|
||||||
|
faraday_middleware (1.2.1)
|
||||||
|
faraday (~> 1.0)
|
||||||
|
fastimage (2.4.0)
|
||||||
|
fastlane (2.228.0)
|
||||||
|
CFPropertyList (>= 2.3, < 4.0.0)
|
||||||
|
addressable (>= 2.8, < 3.0.0)
|
||||||
|
artifactory (~> 3.0)
|
||||||
|
aws-sdk-s3 (~> 1.0)
|
||||||
|
babosa (>= 1.0.3, < 2.0.0)
|
||||||
|
bundler (>= 1.12.0, < 3.0.0)
|
||||||
|
colored (~> 1.2)
|
||||||
|
commander (~> 4.6)
|
||||||
|
dotenv (>= 2.1.1, < 3.0.0)
|
||||||
|
emoji_regex (>= 0.1, < 4.0)
|
||||||
|
excon (>= 0.71.0, < 1.0.0)
|
||||||
|
faraday (~> 1.0)
|
||||||
|
faraday-cookie_jar (~> 0.0.6)
|
||||||
|
faraday_middleware (~> 1.0)
|
||||||
|
fastimage (>= 2.1.0, < 3.0.0)
|
||||||
|
fastlane-sirp (>= 1.0.0)
|
||||||
|
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||||
|
google-apis-androidpublisher_v3 (~> 0.3)
|
||||||
|
google-apis-playcustomapp_v1 (~> 0.1)
|
||||||
|
google-cloud-env (>= 1.6.0, < 2.0.0)
|
||||||
|
google-cloud-storage (~> 1.31)
|
||||||
|
highline (~> 2.0)
|
||||||
|
http-cookie (~> 1.0.5)
|
||||||
|
json (< 3.0.0)
|
||||||
|
jwt (>= 2.1.0, < 3)
|
||||||
|
mini_magick (>= 4.9.4, < 5.0.0)
|
||||||
|
multipart-post (>= 2.0.0, < 3.0.0)
|
||||||
|
naturally (~> 2.2)
|
||||||
|
optparse (>= 0.1.1, < 1.0.0)
|
||||||
|
plist (>= 3.1.0, < 4.0.0)
|
||||||
|
rubyzip (>= 2.0.0, < 3.0.0)
|
||||||
|
security (= 0.1.5)
|
||||||
|
simctl (~> 1.6.3)
|
||||||
|
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||||
|
terminal-table (~> 3)
|
||||||
|
tty-screen (>= 0.6.3, < 1.0.0)
|
||||||
|
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||||
|
word_wrap (~> 1.0.0)
|
||||||
|
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||||
|
xcpretty (~> 0.4.1)
|
||||||
|
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||||
|
fastlane-plugin-versioning_android (0.1.1)
|
||||||
|
fastlane-sirp (1.0.0)
|
||||||
|
sysrandom (~> 1.0)
|
||||||
|
gh_inspector (1.1.3)
|
||||||
|
google-apis-androidpublisher_v3 (0.54.0)
|
||||||
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
|
google-apis-core (0.11.3)
|
||||||
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
|
httpclient (>= 2.8.1, < 3.a)
|
||||||
|
mini_mime (~> 1.0)
|
||||||
|
representable (~> 3.0)
|
||||||
|
retriable (>= 2.0, < 4.a)
|
||||||
|
rexml
|
||||||
|
google-apis-iamcredentials_v1 (0.17.0)
|
||||||
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
|
google-apis-playcustomapp_v1 (0.13.0)
|
||||||
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
|
google-apis-storage_v1 (0.31.0)
|
||||||
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
|
google-cloud-core (1.8.0)
|
||||||
|
google-cloud-env (>= 1.0, < 3.a)
|
||||||
|
google-cloud-errors (~> 1.0)
|
||||||
|
google-cloud-env (1.6.0)
|
||||||
|
faraday (>= 0.17.3, < 3.0)
|
||||||
|
google-cloud-errors (1.5.0)
|
||||||
|
google-cloud-storage (1.47.0)
|
||||||
|
addressable (~> 2.8)
|
||||||
|
digest-crc (~> 0.4)
|
||||||
|
google-apis-iamcredentials_v1 (~> 0.1)
|
||||||
|
google-apis-storage_v1 (~> 0.31.0)
|
||||||
|
google-cloud-core (~> 1.6)
|
||||||
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
|
mini_mime (~> 1.0)
|
||||||
|
googleauth (1.8.1)
|
||||||
|
faraday (>= 0.17.3, < 3.a)
|
||||||
|
jwt (>= 1.4, < 3.0)
|
||||||
|
multi_json (~> 1.11)
|
||||||
|
os (>= 0.9, < 2.0)
|
||||||
|
signet (>= 0.16, < 2.a)
|
||||||
|
highline (2.0.3)
|
||||||
|
http-cookie (1.0.8)
|
||||||
|
domain_name (~> 0.5)
|
||||||
|
httpclient (2.9.0)
|
||||||
|
mutex_m
|
||||||
|
jmespath (1.6.2)
|
||||||
|
json (2.13.2)
|
||||||
|
jwt (2.10.2)
|
||||||
|
base64
|
||||||
|
logger (1.7.0)
|
||||||
|
mini_magick (4.13.2)
|
||||||
|
mini_mime (1.1.5)
|
||||||
|
multi_json (1.17.0)
|
||||||
|
multipart-post (2.4.1)
|
||||||
|
mutex_m (0.3.0)
|
||||||
|
nanaimo (0.4.0)
|
||||||
|
naturally (2.3.0)
|
||||||
|
nkf (0.2.0)
|
||||||
|
optparse (0.6.0)
|
||||||
|
os (1.1.4)
|
||||||
|
plist (3.7.2)
|
||||||
|
public_suffix (6.0.2)
|
||||||
|
rake (13.3.0)
|
||||||
|
representable (3.2.0)
|
||||||
|
declarative (< 0.1.0)
|
||||||
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
|
uber (< 0.2.0)
|
||||||
|
retriable (3.1.2)
|
||||||
|
rexml (3.4.1)
|
||||||
|
rouge (3.28.0)
|
||||||
|
ruby2_keywords (0.0.5)
|
||||||
|
rubyzip (2.4.1)
|
||||||
|
security (0.1.5)
|
||||||
|
signet (0.20.0)
|
||||||
|
addressable (~> 2.8)
|
||||||
|
faraday (>= 0.17.5, < 3.a)
|
||||||
|
jwt (>= 1.5, < 3.0)
|
||||||
|
multi_json (~> 1.10)
|
||||||
|
simctl (1.6.10)
|
||||||
|
CFPropertyList
|
||||||
|
naturally
|
||||||
|
sysrandom (1.0.5)
|
||||||
|
terminal-notifier (2.0.0)
|
||||||
|
terminal-table (3.0.2)
|
||||||
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
|
trailblazer-option (0.1.2)
|
||||||
|
tty-cursor (0.7.1)
|
||||||
|
tty-screen (0.8.2)
|
||||||
|
tty-spinner (0.9.3)
|
||||||
|
tty-cursor (~> 0.7)
|
||||||
|
uber (0.1.0)
|
||||||
|
unicode-display_width (2.6.0)
|
||||||
|
word_wrap (1.0.0)
|
||||||
|
xcodeproj (1.27.0)
|
||||||
|
CFPropertyList (>= 2.3.3, < 4.0)
|
||||||
|
atomos (~> 0.1.3)
|
||||||
|
claide (>= 1.0.2, < 2.0)
|
||||||
|
colored2 (~> 3.1)
|
||||||
|
nanaimo (~> 0.4.0)
|
||||||
|
rexml (>= 3.3.6, < 4.0)
|
||||||
|
xcpretty (0.4.1)
|
||||||
|
rouge (~> 3.28.0)
|
||||||
|
xcpretty-travis-formatter (1.0.1)
|
||||||
|
xcpretty (~> 0.2, >= 0.0.7)
|
||||||
|
|
||||||
|
PLATFORMS
|
||||||
|
ruby
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
fastlane
|
||||||
|
fastlane-plugin-versioning_android
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
2.6.9
|
||||||
48
README.md
48
README.md
@@ -1,14 +1,14 @@
|
|||||||
# Android KeePassDX
|
# Android KeePassDX
|
||||||
|
|
||||||
<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 alt="KeePassDX Icon" src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/icon.png"> **Lightweight password safe and 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 alt="KeePassDX Screenshot" src="https://raw.githubusercontent.com/Kunzisoft/KeePassDX/master/art/screen.jpg" width="220">
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- Create database files / entries and groups.
|
- Create database files / entries and groups.
|
||||||
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
|
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
|
||||||
- **Compatible** with the majority of alternative programs (KeePass, KeePassX, KeePassXC, …).
|
- **Compatible** with the majority of alternative programs (KeePass, KeePassXC, KeeWeb, …).
|
||||||
- Allows opening and **copying URI / URL fields quickly**.
|
- Allows opening and **copying URI / URL fields quickly**.
|
||||||
- **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*.
|
- **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*.
|
||||||
- **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA).
|
- **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA).
|
||||||
@@ -48,16 +48,40 @@ Optional visual styles are accessible after a contribution (and a congratulatory
|
|||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
*[F-Droid](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/) is the recommended way of installing, a libre software project that verifies that all the libraries and app code is libre software.*
|
*[F-Droid](https://f-droid.org/packages/com.kunzisoft.keepass.libre/) is the recommended way of installing, a libre software project that verifies all the libraries and app code is libre software.*
|
||||||
|
|
||||||
[<img src="https://f-droid.org/badge/get-it-on.png"
|
| Source | Status | [Version](https://github.com/Kunzisoft/KeePassDX/wiki/FAQ#why-a-libre-and-free-version) |
|
||||||
alt="Get it on F-Droid"
|
|--------|--------|---------|
|
||||||
height="80">](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/)
|
| [Google Play](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free) |  | Free + [Pro](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.pro) |
|
||||||
|
| [F-Droid](https://f-droid.org/en/packages/com.kunzisoft.keepass.libre/) |  | Libre |
|
||||||
|
| [IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/com.kunzisoft.keepass.free) |  | Free & [Libre](https://apt.izzysoft.de/fdroid/index/apk/com.kunzisoft.keepass.libre) |
|
||||||
|
| [GitHub](https://github.com/Kunzisoft/KeePassDX/releases) / [Obtainium](https://github.com/ImranR98/Obtainium) |  | Free & Libre |
|
||||||
|
|
||||||
|
## Package authenticity from GitHub
|
||||||
|
- Download the app from [GitHub releases](https://github.com/Kunzisoft/KeePassDX/releases/latest)
|
||||||
|
- Install [`apksigner`](https://developer.android.com/tools/apksigner) from [Android Studio](https://developer.android.com/studio)
|
||||||
|
- Open the directory where you saved the downloaded file in the Terminal
|
||||||
|
- Make sure that you have `apksigner` installed by running:
|
||||||
|
```shell
|
||||||
|
apksigner --version
|
||||||
|
```
|
||||||
|
- Depending on the APK file you downloaded, run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
apksigner verify --verbose --print-certs -min-sdk-version 24 KeePassDX-*.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
You should get this output :
|
||||||
|
```shell
|
||||||
|
Verified using v2 scheme (APK Signature Scheme v2): true
|
||||||
|
...
|
||||||
|
Number of signers: 1
|
||||||
|
Signer #1 certificate SHA-256 digest: 7d55b8af210381aabf960f07e17cf7857b6d2a642ca2da6bf0bdf1b200362f04
|
||||||
|
...
|
||||||
|
Signer #1 public key SHA-256 digest: 5d261d3176db1e077b80112824d9390167f3be0561827e42112ed6b71192db81
|
||||||
|
```
|
||||||
|
If it's the case, this means that the APK was well built by the author of KeePassDX.
|
||||||
|
|
||||||
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
|
|
||||||
alt="Get it on Google Play"
|
|
||||||
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
|
|
||||||
|
|
||||||
## Frequently Asked Questions
|
## Frequently Asked Questions
|
||||||
|
|
||||||
Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/wiki/FAQ)
|
Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/wiki/FAQ)
|
||||||
@@ -72,7 +96,7 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright © 2022 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
Copyright © 2025 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
|
||||||
|
|
||||||
This file is part of KeePassDX.
|
This file is part of KeePassDX.
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,15 @@ apply plugin: 'kotlin-parcelize'
|
|||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 31
|
namespace 'com.kunzisoft.keepass'
|
||||||
buildToolsVersion "31.0.0"
|
compileSdkVersion 34
|
||||||
ndkVersion "21.4.7075529"
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 15
|
minSdkVersion 15
|
||||||
targetSdkVersion 31
|
targetSdkVersion 34
|
||||||
versionCode = 110
|
versionCode = 136
|
||||||
versionName = "3.4.1"
|
versionName = "4.1.4"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
@@ -33,10 +32,16 @@ android {
|
|||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled = false
|
minifyEnabled = false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependenciesInfo {
|
||||||
|
// Disables dependency metadata when building APKs.
|
||||||
|
includeInApk = false
|
||||||
|
// Disables dependency metadata when building Android App Bundles.
|
||||||
|
includeInBundle = false
|
||||||
|
}
|
||||||
|
|
||||||
flavorDimensions "version"
|
flavorDimensions "version"
|
||||||
productFlavors {
|
productFlavors {
|
||||||
libre {
|
libre {
|
||||||
@@ -50,7 +55,9 @@ android {
|
|||||||
"\"KeepassDXStyle_Reply\"," +
|
"\"KeepassDXStyle_Reply\"," +
|
||||||
"\"KeepassDXStyle_Reply_Night\"," +
|
"\"KeepassDXStyle_Reply_Night\"," +
|
||||||
"\"KeepassDXStyle_Purple\"," +
|
"\"KeepassDXStyle_Purple\"," +
|
||||||
"\"KeepassDXStyle_Purple_Dark\"}"
|
"\"KeepassDXStyle_Purple_Dark\"," +
|
||||||
|
"\"KeepassDXStyle_Dynamic_Light\"," +
|
||||||
|
"\"KeepassDXStyle_Dynamic_Night\"}"
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||||
}
|
}
|
||||||
free {
|
free {
|
||||||
@@ -59,16 +66,16 @@ android {
|
|||||||
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
buildConfigField "String", "BUILD_VERSION", "\"free\""
|
||||||
buildConfigField "boolean", "CLOSED_STORE", "true"
|
buildConfigField "boolean", "CLOSED_STORE", "true"
|
||||||
buildConfigField "String[]", "STYLES_DISABLED",
|
buildConfigField "String[]", "STYLES_DISABLED",
|
||||||
"{\"KeepassDXStyle_Simple\"," +
|
"{\"KeepassDXStyle_Blue\"," +
|
||||||
"\"KeepassDXStyle_Simple_Night\"," +
|
|
||||||
"\"KeepassDXStyle_Blue\"," +
|
|
||||||
"\"KeepassDXStyle_Blue_Night\"," +
|
"\"KeepassDXStyle_Blue_Night\"," +
|
||||||
"\"KeepassDXStyle_Red\"," +
|
"\"KeepassDXStyle_Red\"," +
|
||||||
"\"KeepassDXStyle_Red_Night\"," +
|
"\"KeepassDXStyle_Red_Night\"," +
|
||||||
"\"KeepassDXStyle_Reply\"," +
|
"\"KeepassDXStyle_Reply\"," +
|
||||||
"\"KeepassDXStyle_Reply_Night\"," +
|
"\"KeepassDXStyle_Reply_Night\"," +
|
||||||
"\"KeepassDXStyle_Purple\"," +
|
"\"KeepassDXStyle_Purple\"," +
|
||||||
"\"KeepassDXStyle_Purple_Dark\"}"
|
"\"KeepassDXStyle_Purple_Dark\"," +
|
||||||
|
"\"KeepassDXStyle_Dynamic_Light\"," +
|
||||||
|
"\"KeepassDXStyle_Dynamic_Night\"}"
|
||||||
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
buildConfigField "String[]", "ICON_PACKS_DISABLED", "{}"
|
||||||
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
|
manifestPlaceholders = [ googleAndroidBackupAPIKey:"AEdPqrEAAAAIbRfbV8fHLItXo8OcHwrO0sSNblqhPwkc0DPTqg" ]
|
||||||
}
|
}
|
||||||
@@ -84,16 +91,19 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def room_version = "2.4.2"
|
def room_version = "2.5.1"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
@@ -101,25 +111,25 @@ dependencies {
|
|||||||
implementation "androidx.appcompat:appcompat:$android_appcompat_version"
|
implementation "androidx.appcompat:appcompat:$android_appcompat_version"
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
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.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
||||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||||
implementation 'androidx.biometric:biometric:1.1.0'
|
implementation 'androidx.biometric:biometric:1.1.0'
|
||||||
implementation 'androidx.media:media:1.5.0'
|
implementation 'androidx.media:media:1.6.0'
|
||||||
// Lifecycle - LiveData - ViewModel - Coroutines
|
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||||
implementation "androidx.core:core-ktx:$android_core_version"
|
implementation "androidx.core:core-ktx:$android_core_version"
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.4.1'
|
implementation 'androidx.fragment:fragment-ktx:1.6.0'
|
||||||
implementation "com.google.android.material:material:$android_material_version"
|
implementation "com.google.android.material:material:$android_material_version"
|
||||||
// Token auto complete
|
// Token auto complete
|
||||||
// From sources until https://github.com/splitwise/TokenAutoComplete/pull/422 fixed
|
// From sources until https://github.com/splitwise/TokenAutoComplete/pull/422 fixed
|
||||||
// implementation "com.splitwise:tokenautocomplete:4.0.0-beta04"
|
implementation "com.splitwise:tokenautocomplete:4.0.0-beta05"
|
||||||
// Database
|
// Database
|
||||||
implementation "androidx.room:room-runtime:$room_version"
|
implementation "androidx.room:room-runtime:$room_version"
|
||||||
kapt "androidx.room:room-compiler:$room_version"
|
kapt "androidx.room:room-compiler:$room_version"
|
||||||
// Autofill
|
// Autofill
|
||||||
implementation "androidx.autofill:autofill:1.1.0"
|
implementation "androidx.autofill:autofill:1.1.0"
|
||||||
// Time
|
// Time
|
||||||
implementation 'joda-time:joda-time:2.10.13'
|
implementation 'joda-time:joda-time:2.13.0'
|
||||||
// Color
|
// Color
|
||||||
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.6'
|
implementation 'com.github.Kunzisoft:AndroidClearChroma:2.6'
|
||||||
// Education
|
// Education
|
||||||
@@ -129,13 +139,11 @@ dependencies {
|
|||||||
implementation 'commons-codec:commons-codec:1.15'
|
implementation 'commons-codec:commons-codec:1.15'
|
||||||
// Password generator
|
// Password generator
|
||||||
implementation 'me.gosimple:nbvcxz:1.5.0'
|
implementation 'me.gosimple:nbvcxz:1.5.0'
|
||||||
// Encrypt lib
|
|
||||||
implementation project(path: ':crypto')
|
// Modules import
|
||||||
// Icon pack
|
implementation project(path: ':database')
|
||||||
implementation project(path: ':icon-pack-classic')
|
implementation project(path: ':icon-pack')
|
||||||
implementation project(path: ':icon-pack-material')
|
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
androidTestImplementation "androidx.test:runner:$android_test_version"
|
androidTestImplementation "androidx.test:runner:$android_test_version"
|
||||||
androidTestImplementation "androidx.test:rules:$android_test_version"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 2,
|
||||||
|
"identityHash": "f8fb4aed546de19ae7ca0797f49b26a4",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "file_database_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `database_alias` TEXT NOT NULL, `keyfile_uri` TEXT, `hardware_key` TEXT, `updated` INTEGER NOT NULL, PRIMARY KEY(`database_uri`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "databaseUri",
|
||||||
|
"columnName": "database_uri",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "databaseAlias",
|
||||||
|
"columnName": "database_alias",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "keyFileUri",
|
||||||
|
"columnName": "keyfile_uri",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "hardwareKey",
|
||||||
|
"columnName": "hardware_key",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "updated",
|
||||||
|
"columnName": "updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"database_uri"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "cipher_database",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `encrypted_value` TEXT NOT NULL, `specs_parameters` TEXT NOT NULL, PRIMARY KEY(`database_uri`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "databaseUri",
|
||||||
|
"columnName": "database_uri",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "encryptedValue",
|
||||||
|
"columnName": "encrypted_value",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "specParameters",
|
||||||
|
"columnName": "specs_parameters",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"database_uri"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f8fb4aed546de19ae7ca0797f49b26a4')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
15
app/src/free/res/drawable-v24/ic_launcher_monochrome.xml
Normal file
15
app/src/free/res/drawable-v24/ic_launcher_monochrome.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="120"
|
||||||
|
android:viewportHeight="120">
|
||||||
|
<group
|
||||||
|
android:translateX="6"
|
||||||
|
android:translateY="6">
|
||||||
|
<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 M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 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>
|
||||||
@@ -7,10 +7,6 @@
|
|||||||
<group
|
<group
|
||||||
android:translateX="-12"
|
android:translateX="-12"
|
||||||
android:translateY="-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
|
<path
|
||||||
android:fillColor="#ffffff"
|
android:fillColor="#ffffff"
|
||||||
android:strokeWidth="1.99999297"
|
android:strokeWidth="1.99999297"
|
||||||
15
app/src/free/res/drawable/ic_app_lock_white_24dp.xml
Normal file
15
app/src/free/res/drawable/ic_app_lock_white_24dp.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?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="#ffffff"
|
||||||
|
android:strokeWidth="1.99999297"
|
||||||
|
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
15
app/src/libre/res/drawable-v24/ic_launcher_monochrome.xml
Normal file
15
app/src/libre/res/drawable-v24/ic_launcher_monochrome.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="120"
|
||||||
|
android:viewportHeight="120">
|
||||||
|
<group
|
||||||
|
android:translateX="6"
|
||||||
|
android:translateY="6">
|
||||||
|
<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 M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 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>
|
||||||
@@ -7,10 +7,6 @@
|
|||||||
<group
|
<group
|
||||||
android:translateX="-12"
|
android:translateX="-12"
|
||||||
android:translateY="-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
|
<path
|
||||||
android:fillColor="#ffffff"
|
android:fillColor="#ffffff"
|
||||||
android:strokeWidth="1.99999297"
|
android:strokeWidth="1.99999297"
|
||||||
15
app/src/libre/res/drawable/ic_app_lock_white_24dp.xml
Normal file
15
app/src/libre/res/drawable/ic_app_lock_white_24dp.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?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="#ffffff"
|
||||||
|
android:strokeWidth="1.99999297"
|
||||||
|
android:pathData="M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.kunzisoft.keepass"
|
|
||||||
android:installLocation="auto">
|
android:installLocation="auto">
|
||||||
<supports-screens
|
<supports-screens
|
||||||
android:smallScreens="true"
|
android:smallScreens="true"
|
||||||
@@ -10,6 +9,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.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
@@ -20,6 +25,12 @@
|
|||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
tools:ignore="QueryAllPackagesPermission" />
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.CREATE_DOCUMENT" />
|
||||||
|
<data android:mimeType="application/octet-stream" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
@@ -32,6 +43,7 @@
|
|||||||
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:supportsRtl="true"
|
||||||
android:theme="@style/KeepassDXStyle.Night"
|
android:theme="@style/KeepassDXStyle.Night"
|
||||||
tools:targetApi="s">
|
tools:targetApi="s">
|
||||||
<meta-data
|
<meta-data
|
||||||
@@ -39,7 +51,6 @@
|
|||||||
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:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
@@ -112,7 +123,7 @@
|
|||||||
android:name="com.kunzisoft.keepass.activities.GroupActivity"
|
android:name="com.kunzisoft.keepass.activities.GroupActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:windowSoftInputMode="adjustPan">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<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"
|
||||||
@@ -150,14 +161,21 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
|
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
|
||||||
android:theme="@style/Theme.Transparent"
|
android:theme="@style/Theme.Transparent"
|
||||||
android:configChanges="keyboardHidden" />
|
android:configChanges="keyboardHidden"
|
||||||
|
android:excludeFromRecents="true"/>
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
|
android:name="com.kunzisoft.keepass.settings.AdvancedUnlockSettingsActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity"
|
||||||
|
android:theme="@style/Theme.Transparent" />
|
||||||
<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">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
@@ -168,8 +186,9 @@
|
|||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data android:scheme="otpauth" android:host="totp" />
|
<data android:scheme="otpauth"/>
|
||||||
<data android:scheme="otpauth" android:host="hotp" />
|
<data android:host="totp"/>
|
||||||
|
<data android:host="hotp"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
@@ -183,18 +202,27 @@
|
|||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.services.AttachmentFileNotificationService"
|
android:name="com.kunzisoft.keepass.services.AttachmentFileNotificationService"
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.services.ClipboardEntryNotificationService"
|
android:name="com.kunzisoft.keepass.services.ClipboardEntryNotificationService"
|
||||||
|
android:foregroundServiceType="specialUse"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false" />
|
||||||
|
<service
|
||||||
|
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
|
||||||
|
android:foregroundServiceType="specialUse"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.services.AdvancedUnlockNotificationService"
|
android:name="com.kunzisoft.keepass.services.AdvancedUnlockNotificationService"
|
||||||
|
android:foregroundServiceType="specialUse"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<!-- Receiver for Autofill -->
|
<!-- Receiver for Autofill -->
|
||||||
@@ -221,10 +249,6 @@
|
|||||||
<action android:name="android.view.InputMethod" />
|
<action android:name="android.view.InputMethod" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
|
||||||
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false" />
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
|
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
|
|||||||
@@ -38,7 +38,11 @@ import android.graphics.RectF
|
|||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.*
|
import android.view.GestureDetector
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.ScaleGestureDetector
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
import android.view.animation.DecelerateInterpolator
|
import android.view.animation.DecelerateInterpolator
|
||||||
import android.view.animation.Interpolator
|
import android.view.animation.Interpolator
|
||||||
@@ -172,16 +176,16 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
private val onScaleGestureListener: ScaleGestureDetector.OnScaleGestureListener =
|
private val onScaleGestureListener: ScaleGestureDetector.OnScaleGestureListener =
|
||||||
object : ScaleGestureDetector.OnScaleGestureListener {
|
object : ScaleGestureDetector.OnScaleGestureListener {
|
||||||
|
|
||||||
override fun onScale(detector: ScaleGestureDetector?): Boolean {
|
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||||
if (isDragging() || isBitmapTranslateAnimationRunning || isBitmapScaleAnimationRunninng) {
|
if (isDragging() || isBitmapTranslateAnimationRunning || isBitmapScaleAnimationRunninng) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val scaleFactor = detector?.scaleFactor ?: 1.0f
|
val scaleFactor = detector.scaleFactor
|
||||||
val focalX = detector?.focusX ?: bitmapBounds.centerX()
|
val focalX = detector.focusX
|
||||||
val focalY = detector?.focusY ?: bitmapBounds.centerY()
|
val focalY = detector.focusY
|
||||||
|
|
||||||
if (detector?.scaleFactor == 1.0f) {
|
if (detector.scaleFactor == 1.0f) {
|
||||||
// scale is not changing
|
// scale is not changing
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -191,22 +195,23 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScaleBegin(p0: ScaleGestureDetector?): Boolean = true
|
override fun onScaleBegin(p0: ScaleGestureDetector): Boolean = true
|
||||||
|
|
||||||
|
override fun onScaleEnd(p0: ScaleGestureDetector) {}
|
||||||
|
|
||||||
override fun onScaleEnd(p0: ScaleGestureDetector?) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val onGestureListener: GestureDetector.OnGestureListener =
|
private val onGestureListener: GestureDetector.OnGestureListener =
|
||||||
object : GestureDetector.SimpleOnGestureListener() {
|
object : GestureDetector.SimpleOnGestureListener() {
|
||||||
override fun onDown(e: MotionEvent?): Boolean = true
|
override fun onDown(e: MotionEvent): Boolean = true
|
||||||
|
|
||||||
override fun onScroll(
|
override fun onScroll(
|
||||||
e1: MotionEvent?,
|
e1: MotionEvent?,
|
||||||
e2: MotionEvent?,
|
e2: MotionEvent,
|
||||||
distanceX: Float,
|
distanceX: Float,
|
||||||
distanceY: Float
|
distanceY: Float
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (e2?.pointerCount != 1) {
|
if (e2.pointerCount != 1) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,13 +224,11 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onFling(
|
override fun onFling(
|
||||||
e1: MotionEvent?,
|
e1: MotionEvent?,
|
||||||
e2: MotionEvent?,
|
e2: MotionEvent,
|
||||||
velocityX: Float,
|
velocityX: Float,
|
||||||
velocityY: Float
|
velocityY: Float
|
||||||
): Boolean {
|
): Boolean {
|
||||||
e1 ?: return true
|
|
||||||
|
|
||||||
if (scale > minScale) {
|
if (scale > minScale) {
|
||||||
processFlingBitmap(velocityX, velocityY)
|
processFlingBitmap(velocityX, velocityY)
|
||||||
} else {
|
} else {
|
||||||
@@ -234,9 +237,7 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDoubleTap(e: MotionEvent?): Boolean {
|
override fun onDoubleTap(e: MotionEvent): Boolean {
|
||||||
e ?: return false
|
|
||||||
|
|
||||||
if (isBitmapScaleAnimationRunninng) {
|
if (isBitmapScaleAnimationRunninng) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -376,21 +377,21 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||||
}
|
}
|
||||||
.setListener(object : Animator.AnimatorListener {
|
.setListener(object : Animator.AnimatorListener {
|
||||||
override fun onAnimationStart(p0: Animator?) {
|
override fun onAnimationStart(p0: Animator) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationEnd(p0: Animator?) {
|
override fun onAnimationEnd(p0: Animator) {
|
||||||
isViewTranslateAnimationRunning = false
|
isViewTranslateAnimationRunning = false
|
||||||
onViewTranslateListener?.onDismiss(imageView)
|
onViewTranslateListener?.onDismiss(imageView)
|
||||||
cleanup()
|
cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationCancel(p0: Animator?) {
|
override fun onAnimationCancel(p0: Animator) {
|
||||||
isViewTranslateAnimationRunning = false
|
isViewTranslateAnimationRunning = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationRepeat(p0: Animator?) {
|
override fun onAnimationRepeat(p0: Animator) {
|
||||||
// no op
|
// no op
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -409,21 +410,21 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||||
}
|
}
|
||||||
addListener(object : Animator.AnimatorListener {
|
addListener(object : Animator.AnimatorListener {
|
||||||
override fun onAnimationStart(p0: Animator?) {
|
override fun onAnimationStart(p0: Animator) {
|
||||||
// no op
|
// no op
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationEnd(p0: Animator?) {
|
override fun onAnimationEnd(p0: Animator) {
|
||||||
isViewTranslateAnimationRunning = false
|
isViewTranslateAnimationRunning = false
|
||||||
onViewTranslateListener?.onDismiss(imageView)
|
onViewTranslateListener?.onDismiss(imageView)
|
||||||
cleanup()
|
cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationCancel(p0: Animator?) {
|
override fun onAnimationCancel(p0: Animator) {
|
||||||
isViewTranslateAnimationRunning = false
|
isViewTranslateAnimationRunning = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationRepeat(p0: Animator?) {
|
override fun onAnimationRepeat(p0: Animator) {
|
||||||
// no op
|
// no op
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -480,20 +481,20 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
setTransform()
|
setTransform()
|
||||||
}
|
}
|
||||||
addListener(object : Animator.AnimatorListener {
|
addListener(object : Animator.AnimatorListener {
|
||||||
override fun onAnimationStart(p0: Animator?) {
|
override fun onAnimationStart(p0: Animator) {
|
||||||
isBitmapTranslateAnimationRunning = true
|
isBitmapTranslateAnimationRunning = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationEnd(p0: Animator?) {
|
override fun onAnimationEnd(p0: Animator) {
|
||||||
isBitmapTranslateAnimationRunning = false
|
isBitmapTranslateAnimationRunning = false
|
||||||
constrainBitmapBounds()
|
constrainBitmapBounds()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationCancel(p0: Animator?) {
|
override fun onAnimationCancel(p0: Animator) {
|
||||||
isBitmapTranslateAnimationRunning = false
|
isBitmapTranslateAnimationRunning = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationRepeat(p0: Animator?) {
|
override fun onAnimationRepeat(p0: Animator) {
|
||||||
// no op
|
// no op
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -531,11 +532,11 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
setTransform()
|
setTransform()
|
||||||
}
|
}
|
||||||
addListener(object : Animator.AnimatorListener {
|
addListener(object : Animator.AnimatorListener {
|
||||||
override fun onAnimationStart(p0: Animator?) {
|
override fun onAnimationStart(p0: Animator) {
|
||||||
isBitmapScaleAnimationRunninng = true
|
isBitmapScaleAnimationRunninng = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationEnd(p0: Animator?) {
|
override fun onAnimationEnd(p0: Animator) {
|
||||||
isBitmapScaleAnimationRunninng = false
|
isBitmapScaleAnimationRunninng = false
|
||||||
if (endScale == minScale) {
|
if (endScale == minScale) {
|
||||||
zoomToTargetScale(minScale, focalX, focalY)
|
zoomToTargetScale(minScale, focalX, focalY)
|
||||||
@@ -543,11 +544,11 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationCancel(p0: Animator?) {
|
override fun onAnimationCancel(p0: Animator) {
|
||||||
isBitmapScaleAnimationRunninng = false
|
isBitmapScaleAnimationRunninng = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationRepeat(p0: Animator?) {
|
override fun onAnimationRepeat(p0: Animator) {
|
||||||
// no op
|
// no op
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -585,11 +586,11 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
setTransform()
|
setTransform()
|
||||||
}
|
}
|
||||||
addListener(object : Animator.AnimatorListener {
|
addListener(object : Animator.AnimatorListener {
|
||||||
override fun onAnimationStart(p0: Animator?) {
|
override fun onAnimationStart(p0: Animator) {
|
||||||
isBitmapScaleAnimationRunninng = true
|
isBitmapScaleAnimationRunninng = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationEnd(p0: Animator?) {
|
override fun onAnimationEnd(p0: Animator) {
|
||||||
isBitmapScaleAnimationRunninng = false
|
isBitmapScaleAnimationRunninng = false
|
||||||
if (endScale == minScale) {
|
if (endScale == minScale) {
|
||||||
scale = minScale
|
scale = minScale
|
||||||
@@ -599,11 +600,11 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationCancel(p0: Animator?) {
|
override fun onAnimationCancel(p0: Animator) {
|
||||||
isBitmapScaleAnimationRunninng = false
|
isBitmapScaleAnimationRunninng = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationRepeat(p0: Animator?) {
|
override fun onAnimationRepeat(p0: Animator) {
|
||||||
// no op
|
// no op
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -669,19 +670,19 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
onViewTranslateListener?.onViewTranslate(this, amount)
|
onViewTranslateListener?.onViewTranslate(this, amount)
|
||||||
}
|
}
|
||||||
.setListener(object : Animator.AnimatorListener {
|
.setListener(object : Animator.AnimatorListener {
|
||||||
override fun onAnimationStart(p0: Animator?) {
|
override fun onAnimationStart(p0: Animator) {
|
||||||
// no op
|
// no op
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationEnd(p0: Animator?) {
|
override fun onAnimationEnd(p0: Animator) {
|
||||||
onViewTranslateListener?.onRestore(imageView)
|
onViewTranslateListener?.onRestore(imageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationCancel(p0: Animator?) {
|
override fun onAnimationCancel(p0: Animator) {
|
||||||
// no op
|
// no op
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationRepeat(p0: Animator?) {
|
override fun onAnimationRepeat(p0: Animator) {
|
||||||
// no op
|
// no op
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -696,19 +697,19 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||||
}
|
}
|
||||||
addListener(object : Animator.AnimatorListener {
|
addListener(object : Animator.AnimatorListener {
|
||||||
override fun onAnimationStart(p0: Animator?) {
|
override fun onAnimationStart(p0: Animator) {
|
||||||
// no op
|
// no op
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationEnd(p0: Animator?) {
|
override fun onAnimationEnd(p0: Animator) {
|
||||||
onViewTranslateListener?.onRestore(imageView)
|
onViewTranslateListener?.onRestore(imageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationCancel(p0: Animator?) {
|
override fun onAnimationCancel(p0: Animator) {
|
||||||
// no op
|
// no op
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationRepeat(p0: Animator?) {
|
override fun onAnimationRepeat(p0: Animator) {
|
||||||
// no op
|
// no op
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -737,27 +738,27 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
onViewTranslateListener?.onViewTranslate(this, amount)
|
onViewTranslateListener?.onViewTranslate(this, amount)
|
||||||
}
|
}
|
||||||
.setListener(object : Animator.AnimatorListener {
|
.setListener(object : Animator.AnimatorListener {
|
||||||
override fun onAnimationStart(p0: Animator?) {
|
override fun onAnimationStart(p0: Animator) {
|
||||||
isViewTranslateAnimationRunning = true
|
isViewTranslateAnimationRunning = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationEnd(p0: Animator?) {
|
override fun onAnimationEnd(p0: Animator) {
|
||||||
isViewTranslateAnimationRunning = false
|
isViewTranslateAnimationRunning = false
|
||||||
onViewTranslateListener?.onDismiss(imageView)
|
onViewTranslateListener?.onDismiss(imageView)
|
||||||
cleanup()
|
cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationCancel(p0: Animator?) {
|
override fun onAnimationCancel(p0: Animator) {
|
||||||
isViewTranslateAnimationRunning = false
|
isViewTranslateAnimationRunning = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationRepeat(p0: Animator?) {
|
override fun onAnimationRepeat(p0: Animator) {
|
||||||
// no op
|
// no op
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, imageView.translationY.toFloat()).apply {
|
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, imageView.translationY).apply {
|
||||||
duration = dismissAnimationDuration
|
duration = dismissAnimationDuration
|
||||||
interpolator = AccelerateDecelerateInterpolator()
|
interpolator = AccelerateDecelerateInterpolator()
|
||||||
addUpdateListener {
|
addUpdateListener {
|
||||||
@@ -766,21 +767,21 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||||
}
|
}
|
||||||
addListener(object : Animator.AnimatorListener {
|
addListener(object : Animator.AnimatorListener {
|
||||||
override fun onAnimationStart(p0: Animator?) {
|
override fun onAnimationStart(p0: Animator) {
|
||||||
isViewTranslateAnimationRunning = true
|
isViewTranslateAnimationRunning = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationEnd(p0: Animator?) {
|
override fun onAnimationEnd(p0: Animator) {
|
||||||
isViewTranslateAnimationRunning = false
|
isViewTranslateAnimationRunning = false
|
||||||
onViewTranslateListener?.onDismiss(imageView)
|
onViewTranslateListener?.onDismiss(imageView)
|
||||||
cleanup()
|
cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationCancel(p0: Animator?) {
|
override fun onAnimationCancel(p0: Animator) {
|
||||||
isViewTranslateAnimationRunning = false
|
isViewTranslateAnimationRunning = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationRepeat(p0: Animator?) {
|
override fun onAnimationRepeat(p0: Animator) {
|
||||||
// no op
|
// no op
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,16 +20,20 @@
|
|||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.content.pm.PackageManager.NameNotFoundException
|
import android.content.pm.PackageManager.NameNotFoundException
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
import com.kunzisoft.keepass.BuildConfig
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
||||||
|
import com.kunzisoft.keepass.utils.getPackageInfoCompat
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
class AboutActivity : StylishActivity() {
|
class AboutActivity : StylishActivity() {
|
||||||
@@ -45,10 +49,16 @@ class AboutActivity : StylishActivity() {
|
|||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
|
val appName = if (this.isContributingUser())
|
||||||
|
getString(R.string.app_name) + " " + getString(R.string.app_name_part3)
|
||||||
|
else
|
||||||
|
getString(R.string.app_name)
|
||||||
|
findViewById<TextView>(R.id.activity_about_app_name).text = appName
|
||||||
|
|
||||||
var version: String
|
var version: String
|
||||||
var build: String
|
var build: String
|
||||||
try {
|
try {
|
||||||
version = packageManager.getPackageInfo(packageName, 0).versionName
|
version = packageManager.getPackageInfoCompat(packageName).versionName
|
||||||
build = BuildConfig.BUILD_VERSION
|
build = BuildConfig.BUILD_VERSION
|
||||||
} catch (e: NameNotFoundException) {
|
} catch (e: NameNotFoundException) {
|
||||||
Log.w(javaClass.simpleName, "Unable to get the app or the build version", e)
|
Log.w(javaClass.simpleName, "Unable to get the app or the build version", e)
|
||||||
@@ -68,6 +78,15 @@ class AboutActivity : StylishActivity() {
|
|||||||
movementMethod = LinkMovementMethod.getInstance()
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
text = HtmlCompat.fromHtml(getString(R.string.html_about_licence, DateTime().year),
|
text = HtmlCompat.fromHtml(getString(R.string.html_about_licence, DateTime().year),
|
||||||
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
|
textDirection = View.TEXT_DIRECTION_ANY_RTL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<TextView>(R.id.activity_about_privacy_text).apply {
|
||||||
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
text = HtmlCompat.fromHtml(getString(R.string.html_about_privacy),
|
||||||
|
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||||
}
|
}
|
||||||
|
|
||||||
findViewById<TextView>(R.id.activity_about_contribution_text).apply {
|
findViewById<TextView>(R.id.activity_about_contribution_text).apply {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
@@ -36,11 +37,13 @@ import com.kunzisoft.keepass.autofill.AutofillComponent
|
|||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.autofill.CompatInlineSuggestionsRequest
|
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.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.helper.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.utils.WebDomain
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class AutofillLauncherActivity : DatabaseModeActivity() {
|
class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||||
@@ -58,7 +61,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
// Retrieve selection mode
|
// Retrieve selection mode
|
||||||
@@ -69,11 +72,11 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
// To pass extra inline request
|
// To pass extra inline request
|
||||||
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
|
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
compatInlineSuggestionsRequest = bundle.getParcelable(KEY_INLINE_SUGGESTION)
|
compatInlineSuggestionsRequest = bundle.getParcelableCompat(KEY_INLINE_SUGGESTION)
|
||||||
}
|
}
|
||||||
// Build search param
|
// Build search param
|
||||||
bundle.getParcelable<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
|
bundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
|
||||||
SearchInfo.getConcreteWebDomain(
|
WebDomain.getConcreteWebDomain(
|
||||||
this,
|
this,
|
||||||
searchInfo.webDomain
|
searchInfo.webDomain
|
||||||
) { concreteWebDomain ->
|
) { concreteWebDomain ->
|
||||||
@@ -99,9 +102,9 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
}
|
}
|
||||||
SpecialMode.REGISTRATION -> {
|
SpecialMode.REGISTRATION -> {
|
||||||
// To register info
|
// To register info
|
||||||
val registerInfo = intent.getParcelableExtra<RegisterInfo>(KEY_REGISTER_INFO)
|
val registerInfo = intent.getParcelableExtraCompat<RegisterInfo>(KEY_REGISTER_INFO)
|
||||||
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
||||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||||
searchInfo.webDomain = concreteWebDomain
|
searchInfo.webDomain = concreteWebDomain
|
||||||
launchRegistration(database, searchInfo, registerInfo)
|
launchRegistration(database, searchInfo, registerInfo)
|
||||||
}
|
}
|
||||||
@@ -115,89 +118,91 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchSelection(database: Database?,
|
private fun launchSelection(database: ContextualDatabase?,
|
||||||
autofillComponent: AutofillComponent?,
|
autofillComponent: AutofillComponent?,
|
||||||
searchInfo: SearchInfo) {
|
searchInfo: SearchInfo) {
|
||||||
if (autofillComponent == null) {
|
if (autofillComponent == null) {
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(Activity.RESULT_CANCELED)
|
||||||
finish()
|
finish()
|
||||||
} else if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
|
} else if (KeeAutofillService.autofillAllowedFor(
|
||||||
PreferencesUtil.applicationIdBlocklist(this))
|
applicationId = searchInfo.applicationId,
|
||||||
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
|
webDomain = searchInfo.webDomain,
|
||||||
PreferencesUtil.webDomainBlocklist(this))) {
|
context = this
|
||||||
|
)) {
|
||||||
|
// If database is open
|
||||||
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
|
database,
|
||||||
|
searchInfo,
|
||||||
|
{ openedDatabase, items ->
|
||||||
|
// Items found
|
||||||
|
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
|
||||||
|
finish()
|
||||||
|
},
|
||||||
|
{ openedDatabase ->
|
||||||
|
// Show the database UI to select the entry
|
||||||
|
GroupActivity.launchForAutofillResult(this,
|
||||||
|
openedDatabase,
|
||||||
|
mAutofillActivityResultLauncher,
|
||||||
|
autofillComponent,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// If database not open
|
||||||
|
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
||||||
|
mAutofillActivityResultLauncher,
|
||||||
|
autofillComponent,
|
||||||
|
searchInfo)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
showBlockRestartMessage()
|
showBlockRestartMessage()
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(Activity.RESULT_CANCELED)
|
||||||
finish()
|
finish()
|
||||||
} else {
|
|
||||||
// If database is open
|
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
|
||||||
database,
|
|
||||||
searchInfo,
|
|
||||||
{ openedDatabase, items ->
|
|
||||||
// Items found
|
|
||||||
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
|
|
||||||
finish()
|
|
||||||
},
|
|
||||||
{ openedDatabase ->
|
|
||||||
// Show the database UI to select the entry
|
|
||||||
GroupActivity.launchForAutofillResult(this,
|
|
||||||
openedDatabase,
|
|
||||||
mAutofillActivityResultLauncher,
|
|
||||||
autofillComponent,
|
|
||||||
searchInfo,
|
|
||||||
false)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// If database not open
|
|
||||||
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
|
||||||
mAutofillActivityResultLauncher,
|
|
||||||
autofillComponent,
|
|
||||||
searchInfo)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchRegistration(database: Database?,
|
private fun launchRegistration(database: ContextualDatabase?,
|
||||||
searchInfo: SearchInfo,
|
searchInfo: SearchInfo,
|
||||||
registerInfo: RegisterInfo?) {
|
registerInfo: RegisterInfo?) {
|
||||||
if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId,
|
if (KeeAutofillService.autofillAllowedFor(
|
||||||
PreferencesUtil.applicationIdBlocklist(this))
|
applicationId = searchInfo.applicationId,
|
||||||
|| !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain,
|
webDomain = searchInfo.webDomain,
|
||||||
PreferencesUtil.webDomainBlocklist(this))) {
|
context = this
|
||||||
showBlockRestartMessage()
|
)) {
|
||||||
setResult(Activity.RESULT_CANCELED)
|
|
||||||
} else {
|
|
||||||
val readOnly = database?.isReadOnly != false
|
val readOnly = database?.isReadOnly != false
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(this,
|
||||||
database,
|
database,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
{ openedDatabase, _ ->
|
{ 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,
|
||||||
openedDatabase,
|
openedDatabase,
|
||||||
registerInfo)
|
registerInfo)
|
||||||
} else {
|
} else {
|
||||||
showReadOnlySaveMessage()
|
showReadOnlySaveMessage()
|
||||||
}
|
|
||||||
},
|
|
||||||
{ openedDatabase ->
|
|
||||||
if (!readOnly) {
|
|
||||||
// Show the database UI to select the entry
|
|
||||||
GroupActivity.launchForRegistration(this,
|
|
||||||
openedDatabase,
|
|
||||||
registerInfo)
|
|
||||||
} else {
|
|
||||||
showReadOnlySaveMessage()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// If database not open
|
|
||||||
FileDatabaseSelectActivity.launchForRegistration(this,
|
|
||||||
registerInfo)
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{ openedDatabase ->
|
||||||
|
if (!readOnly) {
|
||||||
|
// Show the database UI to select the entry
|
||||||
|
GroupActivity.launchForRegistration(this,
|
||||||
|
openedDatabase,
|
||||||
|
registerInfo)
|
||||||
|
} else {
|
||||||
|
showReadOnlySaveMessage()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// If database not open
|
||||||
|
FileDatabaseSelectActivity.launchForRegistration(this,
|
||||||
|
registerInfo)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
showBlockRestartMessage()
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
}
|
}
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
@@ -213,6 +218,8 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
private val TAG = AutofillLauncherActivity::class.java.name
|
||||||
|
|
||||||
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
|
private const val KEY_SELECTION_BUNDLE = "KEY_SELECTION_BUNDLE"
|
||||||
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
private const val KEY_SEARCH_INFO = "KEY_SEARCH_INFO"
|
||||||
private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION"
|
private const val KEY_INLINE_SUGGESTION = "KEY_INLINE_SUGGESTION"
|
||||||
@@ -221,37 +228,51 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
|
|
||||||
fun getPendingIntentForSelection(context: Context,
|
fun getPendingIntentForSelection(context: Context,
|
||||||
searchInfo: SearchInfo? = null,
|
searchInfo: SearchInfo? = null,
|
||||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent {
|
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null): PendingIntent? {
|
||||||
return PendingIntent.getActivity(context, 0,
|
try {
|
||||||
// Doesn't work with direct extra Parcelable (don't know why?)
|
return PendingIntent.getActivity(
|
||||||
// Wrap into a bundle to bypass the problem
|
context, 0,
|
||||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
// Doesn't work with direct extra Parcelable (don't know why?)
|
||||||
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
|
// Wrap into a bundle to bypass the problem
|
||||||
putParcelable(KEY_SEARCH_INFO, searchInfo)
|
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
putExtra(KEY_SELECTION_BUNDLE, Bundle().apply {
|
||||||
putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
|
putParcelable(KEY_SEARCH_INFO, searchInfo)
|
||||||
}
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
})
|
putParcelable(KEY_INLINE_SUGGESTION, compatInlineSuggestionsRequest)
|
||||||
},
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
})
|
||||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
},
|
||||||
} else {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
})
|
} else {
|
||||||
|
PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
Log.e(TAG, "Unable to create pending intent for selection", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPendingIntentForRegistration(context: Context,
|
fun getPendingIntentForRegistration(context: Context,
|
||||||
registerInfo: RegisterInfo): PendingIntent {
|
registerInfo: RegisterInfo): PendingIntent? {
|
||||||
return PendingIntent.getActivity(context, 0,
|
try {
|
||||||
Intent(context, AutofillLauncherActivity::class.java).apply {
|
return PendingIntent.getActivity(
|
||||||
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
context, 0,
|
||||||
putExtra(KEY_REGISTER_INFO, registerInfo)
|
Intent(context, AutofillLauncherActivity::class.java).apply {
|
||||||
},
|
EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
putExtra(KEY_REGISTER_INFO, registerInfo)
|
||||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
},
|
||||||
} else {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
})
|
} else {
|
||||||
|
PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
Log.e(TAG, "Unable to create pending intent for registration", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchForRegistration(context: Context,
|
fun launchForRegistration(context: Context,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import android.content.Intent
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
@@ -30,17 +31,22 @@ import android.util.Log
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||||
import androidx.core.graphics.BlendModeCompat
|
import androidx.core.graphics.BlendModeCompat
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
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.progressindicator.LinearProgressIndicator
|
||||||
@@ -51,8 +57,8 @@ import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
|||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.adapters.TagsAdapter
|
import com.kunzisoft.keepass.adapters.TagsAdapter
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
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.icon.IconImage
|
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
|
||||||
@@ -67,17 +73,21 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
|||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
|
||||||
import com.kunzisoft.keepass.utils.UuidUtil
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
import com.kunzisoft.keepass.view.WindowInsetPosition
|
||||||
|
import com.kunzisoft.keepass.view.applyWindowInsets
|
||||||
import com.kunzisoft.keepass.view.changeControlColor
|
import com.kunzisoft.keepass.view.changeControlColor
|
||||||
import com.kunzisoft.keepass.view.changeTitleColor
|
import com.kunzisoft.keepass.view.changeTitleColor
|
||||||
import com.kunzisoft.keepass.view.hideByFading
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
|
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
||||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
class EntryActivity : DatabaseLockActivity() {
|
class EntryActivity : DatabaseLockActivity() {
|
||||||
|
|
||||||
|
private var footer: ViewGroup? = null
|
||||||
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 appBarLayout: AppBarLayout? = null
|
||||||
@@ -98,7 +108,6 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
private var mMainEntryId: NodeId<UUID>? = null
|
private var mMainEntryId: NodeId<UUID>? = null
|
||||||
private var mHistoryPosition: Int = -1
|
private var mHistoryPosition: Int = -1
|
||||||
private var mEntryIsHistory: Boolean = false
|
private var mEntryIsHistory: Boolean = false
|
||||||
private var mUrl: String? = null
|
|
||||||
private var mEntryLoaded = false
|
private var mEntryLoaded = false
|
||||||
|
|
||||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||||
@@ -111,9 +120,9 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var mIcon: IconImage? = null
|
private var mIcon: IconImage? = null
|
||||||
private var mColorAccent: Int = 0
|
private var mColorSecondary: Int = 0
|
||||||
private var mControlColor: Int = 0
|
private var mColorSurface: Int = 0
|
||||||
private var mColorPrimary: Int = 0
|
private var mColorOnSurface: Int = 0
|
||||||
private var mColorBackground: Int = 0
|
private var mColorBackground: Int = 0
|
||||||
private var mBackgroundColor: Int? = null
|
private var mBackgroundColor: Int? = null
|
||||||
private var mForegroundColor: Int? = null
|
private var mForegroundColor: Int? = null
|
||||||
@@ -129,6 +138,7 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
// Get views
|
// Get views
|
||||||
|
footer = findViewById(R.id.activity_entry_footer)
|
||||||
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)
|
appBarLayout = findViewById(R.id.app_bar)
|
||||||
@@ -140,22 +150,30 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
lockView = findViewById(R.id.lock_button)
|
lockView = findViewById(R.id.lock_button)
|
||||||
loadingView = findViewById(R.id.loading)
|
loadingView = findViewById(R.id.loading)
|
||||||
|
|
||||||
|
// To apply fit window with transparency
|
||||||
|
setTransparentNavigationBar {
|
||||||
|
// To fix margin with API 27
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(collapsingToolbarLayout!!, null)
|
||||||
|
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP)
|
||||||
|
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
|
||||||
|
}
|
||||||
|
|
||||||
// Empty title
|
// Empty title
|
||||||
collapsingToolbarLayout?.title = " "
|
collapsingToolbarLayout?.title = " "
|
||||||
toolbar?.title = " "
|
toolbar?.title = " "
|
||||||
|
|
||||||
// Retrieve the textColor to tint the toolbar
|
// Retrieve the textColor to tint the toolbar
|
||||||
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
val taColorSecondary = theme.obtainStyledAttributes(intArrayOf(R.attr.colorSecondary))
|
||||||
val taControlColor = theme.obtainStyledAttributes(intArrayOf(R.attr.toolbarColorControl))
|
val taColorSurface = theme.obtainStyledAttributes(intArrayOf(R.attr.colorSurface))
|
||||||
val taColorPrimary = theme.obtainStyledAttributes(intArrayOf(R.attr.colorPrimary))
|
val taColorOnSurface = theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnSurface))
|
||||||
val taColorBackground = theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
|
val taColorBackground = theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
|
||||||
mColorAccent = taColorAccent.getColor(0, Color.BLACK)
|
mColorSecondary = taColorSecondary.getColor(0, Color.BLACK)
|
||||||
mControlColor = taControlColor.getColor(0, Color.BLACK)
|
mColorSurface = taColorSurface.getColor(0, Color.BLACK)
|
||||||
mColorPrimary = taColorPrimary.getColor(0, Color.BLACK)
|
mColorOnSurface = taColorOnSurface.getColor(0, Color.BLACK)
|
||||||
mColorBackground = taColorBackground.getColor(0, Color.BLACK)
|
mColorBackground = taColorBackground.getColor(0, Color.BLACK)
|
||||||
taColorAccent.recycle()
|
taColorSecondary.recycle()
|
||||||
taControlColor.recycle()
|
taColorSurface.recycle()
|
||||||
taColorPrimary.recycle()
|
taColorOnSurface.recycle()
|
||||||
taColorBackground.recycle()
|
taColorBackground.recycle()
|
||||||
|
|
||||||
// Init Tags adapter
|
// Init Tags adapter
|
||||||
@@ -180,7 +198,7 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
|
|
||||||
// Get Entry from UUID
|
// Get Entry from UUID
|
||||||
try {
|
try {
|
||||||
intent.getParcelableExtra<NodeId<UUID>?>(KEY_ENTRY)?.let { mainEntryId ->
|
intent.getParcelableExtraCompat<NodeId<UUID>>(KEY_ENTRY)?.let { mainEntryId ->
|
||||||
intent.removeExtra(KEY_ENTRY)
|
intent.removeExtra(KEY_ENTRY)
|
||||||
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
|
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
|
||||||
intent.removeExtra(KEY_ENTRY_HISTORY_POSITION)
|
intent.removeExtra(KEY_ENTRY_HISTORY_POSITION)
|
||||||
@@ -224,16 +242,16 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
this.mEntryIsHistory = entryIsHistory
|
this.mEntryIsHistory = entryIsHistory
|
||||||
// Assign history dedicated view
|
// Assign history dedicated view
|
||||||
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
|
historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE
|
||||||
|
// TODO History badge
|
||||||
|
/*
|
||||||
if (entryIsHistory) {
|
if (entryIsHistory) {
|
||||||
collapsingToolbarLayout?.contentScrim =
|
}*/
|
||||||
ColorDrawable(mColorAccent)
|
|
||||||
}
|
|
||||||
|
|
||||||
val entryInfo = entryInfoHistory.entryInfo
|
val entryInfo = entryInfoHistory.entryInfo
|
||||||
// Manage entry copy to start notification if allowed (at the first start)
|
// Manage entry copy to start notification if allowed (at the first start)
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
// Manage entry to launch copying notification if allowed
|
// Manage entry to launch copying notification if allowed
|
||||||
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
ClipboardEntryNotificationService.checkAndLaunchNotification(this, entryInfo)
|
||||||
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
||||||
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
|
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
|
||||||
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
|
MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
|
||||||
@@ -246,7 +264,6 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
if (entryInfo.title.isNotEmpty()) entryInfo.title else UuidUtil.toHexString(entryInfo.id)
|
if (entryInfo.title.isNotEmpty()) entryInfo.title else UuidUtil.toHexString(entryInfo.id)
|
||||||
collapsingToolbarLayout?.title = entryTitle
|
collapsingToolbarLayout?.title = entryTitle
|
||||||
toolbar?.title = entryTitle
|
toolbar?.title = entryTitle
|
||||||
mUrl = entryInfo.url
|
|
||||||
// Assign tags
|
// Assign tags
|
||||||
val tags = entryInfo.tags
|
val tags = entryInfo.tags
|
||||||
tagsListView?.visibility = if (tags.isEmpty()) View.GONE else View.VISIBLE
|
tagsListView?.visibility = if (tags.isEmpty()) View.GONE else View.VISIBLE
|
||||||
@@ -310,14 +327,14 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
return coordinatorLayout
|
return coordinatorLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
mEntryViewModel.loadDatabase(database)
|
mEntryViewModel.loadDatabase(database)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result
|
result: ActionRunnable.Result
|
||||||
) {
|
) {
|
||||||
@@ -365,8 +382,8 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun applyToolbarColors() {
|
private fun applyToolbarColors() {
|
||||||
appBarLayout?.setBackgroundColor(mBackgroundColor ?: mColorPrimary)
|
collapsingToolbarLayout?.setBackgroundColor(mBackgroundColor ?: mColorSurface)
|
||||||
collapsingToolbarLayout?.contentScrim = ColorDrawable(mBackgroundColor ?: mColorPrimary)
|
collapsingToolbarLayout?.contentScrim = ColorDrawable(mBackgroundColor ?: mColorSurface)
|
||||||
val backgroundDarker = if (mBackgroundColor != null) {
|
val backgroundDarker = if (mBackgroundColor != null) {
|
||||||
ColorUtils.blendARGB(mBackgroundColor!!, Color.WHITE, 0.1f)
|
ColorUtils.blendARGB(mBackgroundColor!!, Color.WHITE, 0.1f)
|
||||||
} else {
|
} else {
|
||||||
@@ -376,15 +393,15 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
.createBlendModeColorFilterCompat(backgroundDarker, BlendModeCompat.SRC_IN)
|
.createBlendModeColorFilterCompat(backgroundDarker, BlendModeCompat.SRC_IN)
|
||||||
mIcon?.let { icon ->
|
mIcon?.let { icon ->
|
||||||
titleIconView?.let { iconView ->
|
titleIconView?.let { iconView ->
|
||||||
mIconDrawableFactory?.assignDatabaseIcon(
|
mDatabase?.iconDrawableFactory?.assignDatabaseIcon(
|
||||||
iconView,
|
iconView,
|
||||||
icon,
|
icon,
|
||||||
mForegroundColor ?: mColorAccent
|
mForegroundColor ?: mColorSecondary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toolbar?.changeControlColor(mForegroundColor ?: mControlColor)
|
toolbar?.changeControlColor(mForegroundColor ?: mColorOnSurface)
|
||||||
collapsingToolbarLayout?.changeTitleColor(mForegroundColor ?: mControlColor)
|
collapsingToolbarLayout?.changeTitleColor(mForegroundColor ?: mColorOnSurface)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
@@ -408,9 +425,6 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||||
if (mUrl?.isEmpty() != false) {
|
|
||||||
menu?.findItem(R.id.menu_goto_url)?.isVisible = false
|
|
||||||
}
|
|
||||||
if (mEntryIsHistory || mDatabaseReadOnly) {
|
if (mEntryIsHistory || mDatabaseReadOnly) {
|
||||||
menu?.findItem(R.id.menu_save_database)?.isVisible = false
|
menu?.findItem(R.id.menu_save_database)?.isVisible = false
|
||||||
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
menu?.findItem(R.id.menu_merge_database)?.isVisible = false
|
||||||
@@ -471,12 +485,6 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_goto_url -> {
|
|
||||||
mUrl?.let { url ->
|
|
||||||
UriUtil.gotoUrl(this, url)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
R.id.menu_restore_entry_history -> {
|
R.id.menu_restore_entry_history -> {
|
||||||
mMainEntryId?.let { mainEntryId ->
|
mMainEntryId?.let { mainEntryId ->
|
||||||
restoreEntryHistory(
|
restoreEntryHistory(
|
||||||
@@ -526,7 +534,7 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
* Open standard Entry activity
|
* Open standard Entry activity
|
||||||
*/
|
*/
|
||||||
fun launch(activity: Activity,
|
fun launch(activity: Activity,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
entryId: NodeId<UUID>,
|
entryId: NodeId<UUID>,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
@@ -542,7 +550,7 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
* Open history Entry activity
|
* Open history Entry activity
|
||||||
*/
|
*/
|
||||||
fun launch(activity: Activity,
|
fun launch(activity: Activity,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
entryId: NodeId<UUID>,
|
entryId: NodeId<UUID>,
|
||||||
historyPosition: Int,
|
historyPosition: Int,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
|
|||||||
@@ -19,8 +19,6 @@
|
|||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.DatePickerDialog
|
|
||||||
import android.app.TimePickerDialog
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -32,7 +30,9 @@ import android.util.Log
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.*
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.Spinner
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
@@ -44,10 +44,16 @@ import androidx.core.view.isVisible
|
|||||||
import androidx.core.widget.NestedScrollView
|
import androidx.core.widget.NestedScrollView
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import com.google.android.material.datepicker.MaterialDatePicker
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.google.android.material.timepicker.MaterialTimePicker
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.*
|
import com.kunzisoft.keepass.activities.dialogs.ColorPickerDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.EntryCustomFieldDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE
|
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
|
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
@@ -55,13 +61,18 @@ import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
|||||||
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
|
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
|
||||||
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.element.*
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
|
import com.kunzisoft.keepass.database.element.Field
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.template.Template
|
import com.kunzisoft.keepass.database.element.template.Template
|
||||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||||
import com.kunzisoft.keepass.model.AttachmentState
|
import com.kunzisoft.keepass.model.AttachmentState
|
||||||
|
import com.kunzisoft.keepass.model.DataTime
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
@@ -76,22 +87,30 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
|||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.TimeUtil.datePickerToDataDate
|
||||||
import com.kunzisoft.keepass.view.*
|
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
import com.kunzisoft.keepass.view.ToolbarAction
|
||||||
|
import com.kunzisoft.keepass.view.WindowInsetPosition
|
||||||
|
import com.kunzisoft.keepass.view.applyWindowInsets
|
||||||
|
import com.kunzisoft.keepass.view.asError
|
||||||
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
|
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
||||||
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
|
import com.kunzisoft.keepass.view.updateLockPaddingStart
|
||||||
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
|
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
|
||||||
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
||||||
import org.joda.time.DateTime
|
import java.util.UUID
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class EntryEditActivity : DatabaseLockActivity(),
|
class EntryEditActivity : DatabaseLockActivity(),
|
||||||
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
|
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
|
||||||
SetOTPDialogFragment.CreateOtpListener,
|
SetOTPDialogFragment.CreateOtpListener,
|
||||||
DatePickerDialog.OnDateSetListener,
|
|
||||||
TimePickerDialog.OnTimeSetListener,
|
|
||||||
FileTooBigDialogFragment.ActionChooseListener,
|
FileTooBigDialogFragment.ActionChooseListener,
|
||||||
ReplaceFileDialogFragment.ActionChooseListener {
|
ReplaceFileDialogFragment.ActionChooseListener {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
|
private var footer: View? = null
|
||||||
|
private var container: View? = null
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
private var scrollView: NestedScrollView? = null
|
private var scrollView: NestedScrollView? = null
|
||||||
private var templateSelectorSpinner: Spinner? = null
|
private var templateSelectorSpinner: Spinner? = null
|
||||||
@@ -144,10 +163,8 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
// Bottom Bar
|
// Bottom Bar
|
||||||
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
|
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
|
||||||
setSupportActionBar(entryEditAddToolBar)
|
footer = findViewById(R.id.activity_entry_edit_footer)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
container = findViewById(R.id.activity_entry_edit_container)
|
||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
|
||||||
supportActionBar?.setDisplayShowTitleEnabled(false)
|
|
||||||
coordinatorLayout = findViewById(R.id.entry_edit_coordinator_layout)
|
coordinatorLayout = findViewById(R.id.entry_edit_coordinator_layout)
|
||||||
scrollView = findViewById(R.id.entry_edit_scroll)
|
scrollView = findViewById(R.id.entry_edit_scroll)
|
||||||
scrollView?.scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
scrollView?.scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
||||||
@@ -156,19 +173,30 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
validateButton = findViewById(R.id.entry_edit_validate)
|
validateButton = findViewById(R.id.entry_edit_validate)
|
||||||
loadingView = findViewById(R.id.loading)
|
loadingView = findViewById(R.id.loading)
|
||||||
|
|
||||||
|
setSupportActionBar(entryEditAddToolBar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowTitleEnabled(false)
|
||||||
|
|
||||||
|
// To apply fit window with transparency
|
||||||
|
setTransparentNavigationBar(applyToStatusBar = true) {
|
||||||
|
container?.applyWindowInsets(WindowInsetPosition.TOP_BOTTOM_IME)
|
||||||
|
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM_IME)
|
||||||
|
}
|
||||||
|
|
||||||
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
||||||
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
||||||
|
|
||||||
// Entry is retrieve, it's an entry to update
|
// Entry is retrieve, it's an entry to update
|
||||||
var entryId: NodeId<UUID>? = null
|
var entryId: NodeId<UUID>? = null
|
||||||
intent.getParcelableExtra<NodeId<UUID>>(KEY_ENTRY)?.let { entryToUpdate ->
|
intent.getParcelableExtraCompat<NodeId<UUID>>(KEY_ENTRY)?.let { entryToUpdate ->
|
||||||
intent.removeExtra(KEY_ENTRY)
|
intent.removeExtra(KEY_ENTRY)
|
||||||
entryId = entryToUpdate
|
entryId = entryToUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parent is retrieve, it's a new entry to create
|
// Parent is retrieve, it's a new entry to create
|
||||||
var parentId: NodeId<*>? = null
|
var parentId: NodeId<*>? = null
|
||||||
intent.getParcelableExtra<NodeId<*>>(KEY_PARENT)?.let { parent ->
|
intent.getParcelableExtraCompat<NodeId<*>>(KEY_PARENT)?.let { parent ->
|
||||||
intent.removeExtra(KEY_PARENT)
|
intent.removeExtra(KEY_PARENT)
|
||||||
parentId = parent
|
parentId = parent
|
||||||
}
|
}
|
||||||
@@ -185,7 +213,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
mExternalFileHelper = ExternalFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
uri?.let { attachmentToUploadUri ->
|
uri?.let { attachmentToUploadUri ->
|
||||||
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
|
attachmentToUploadUri.getDocumentFile(this)?.also { documentFile ->
|
||||||
documentFile.name?.let { fileName ->
|
documentFile.name?.let { fileName ->
|
||||||
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
|
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
|
||||||
FileTooBigDialogFragment.build(attachmentToUploadUri, fileName)
|
FileTooBigDialogFragment.build(attachmentToUploadUri, fileName)
|
||||||
@@ -203,7 +231,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
// Lock button
|
// Lock button
|
||||||
lockView?.setOnClickListener { lockAndExit() }
|
lockView?.setOnClickListener { lockAndExit() }
|
||||||
// Save button
|
// Save button
|
||||||
validateButton?.setOnClickListener { saveEntry() }
|
validateButton?.setOnClickListener { validateEntry() }
|
||||||
|
|
||||||
mEntryEditViewModel.onTemplateChanged.observe(this) { template ->
|
mEntryEditViewModel.onTemplateChanged.observe(this) { template ->
|
||||||
this.mTemplate = template
|
this.mTemplate = template
|
||||||
@@ -221,7 +249,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
this@EntryEditActivity,
|
this@EntryEditActivity,
|
||||||
templates
|
templates
|
||||||
).apply {
|
).apply {
|
||||||
iconDrawableFactory = mIconDrawableFactory
|
iconDrawableFactory = mDatabase?.iconDrawableFactory
|
||||||
}
|
}
|
||||||
adapter = mTemplatesSelectorAdapter
|
adapter = mTemplatesSelectorAdapter
|
||||||
val selectedTemplate = if (mTemplate != null)
|
val selectedTemplate = if (mTemplate != null)
|
||||||
@@ -272,14 +300,20 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
mEntryEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
|
mEntryEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
|
||||||
if (dateInstant.type == DateInstant.Type.TIME) {
|
if (dateInstant.type == DateInstant.Type.TIME) {
|
||||||
// Launch the time picker
|
// Launch the time picker
|
||||||
val dateTime = DateTime(dateInstant.date)
|
MaterialTimePicker.Builder().build().apply {
|
||||||
TimePickerFragment.getInstance(dateTime.hourOfDay, dateTime.minuteOfHour)
|
addOnPositiveButtonClickListener {
|
||||||
.show(supportFragmentManager, "TimePickerFragment")
|
mEntryEditViewModel.selectTime(DataTime(this.hour, this.minute))
|
||||||
|
}
|
||||||
|
show(supportFragmentManager, "TimePickerFragment")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Launch the date picker
|
// Launch the date picker
|
||||||
val dateTime = DateTime(dateInstant.date)
|
MaterialDatePicker.Builder.datePicker().build().apply {
|
||||||
DatePickerFragment.getInstance(dateTime.year, dateTime.monthOfYear - 1, dateTime.dayOfMonth)
|
addOnPositiveButtonClickListener {
|
||||||
.show(supportFragmentManager, "DatePickerFragment")
|
mEntryEditViewModel.selectDate(datePickerToDataDate(it))
|
||||||
|
}
|
||||||
|
show(supportFragmentManager, "DatePickerFragment")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,19 +402,19 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
mAllowCustomFields = database?.allowEntryCustomFields() == true
|
mAllowCustomFields = database?.allowEntryCustomFields() == true
|
||||||
mAllowOTP = database?.allowOTP == true
|
mAllowOTP = database?.allowOTP == true
|
||||||
mEntryEditViewModel.loadDatabase(database)
|
mEntryEditViewModel.loadDatabase(database)
|
||||||
mTemplatesSelectorAdapter?.apply {
|
mTemplatesSelectorAdapter?.apply {
|
||||||
iconDrawableFactory = mIconDrawableFactory
|
iconDrawableFactory = mDatabase?.iconDrawableFactory
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result
|
result: ActionRunnable.Result
|
||||||
) {
|
) {
|
||||||
@@ -433,7 +467,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
finishForEntryResult(entry)
|
finishForEntryResult(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun entryValidatedForKeyboardSelection(database: Database, entry: Entry) {
|
private fun entryValidatedForKeyboardSelection(database: ContextualDatabase, entry: Entry) {
|
||||||
// Populate Magikeyboard with entry
|
// Populate Magikeyboard with entry
|
||||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||||
this,
|
this,
|
||||||
@@ -444,7 +478,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
finishForEntryResult(entry)
|
finishForEntryResult(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun entryValidatedForAutofillSelection(database: Database, entry: Entry) {
|
private fun entryValidatedForAutofillSelection(database: ContextualDatabase, entry: Entry) {
|
||||||
// Build Autofill response with the entry selected
|
// Build Autofill response with the entry selected
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity,
|
AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity,
|
||||||
@@ -469,7 +503,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Padding if lock button visible
|
// Padding if lock button visible
|
||||||
entryEditAddToolBar?.updateLockPaddingLeft()
|
entryEditAddToolBar?.updateLockPaddingStart()
|
||||||
|
|
||||||
mAttachmentFileBinderManager?.apply {
|
mAttachmentFileBinderManager?.apply {
|
||||||
registerProgressTask()
|
registerProgressTask()
|
||||||
@@ -479,6 +513,11 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep the screen on
|
||||||
|
if (PreferencesUtil.isKeepScreenOnEnabled(this)) {
|
||||||
|
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -541,9 +580,9 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the new entry or update an existing entry in the database
|
* Validate the new entry or update an existing entry in the database
|
||||||
*/
|
*/
|
||||||
private fun saveEntry() {
|
private fun validateEntry() {
|
||||||
mAttachmentFileBinderManager?.stopUploadAllAttachments()
|
mAttachmentFileBinderManager?.stopUploadAllAttachments()
|
||||||
mEntryEditViewModel.requestEntryInfoUpdate(mDatabase)
|
mEntryEditViewModel.requestEntryInfoUpdate(mDatabase)
|
||||||
}
|
}
|
||||||
@@ -625,14 +664,29 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
)
|
)
|
||||||
if (!addAttachmentEducationPerformed) {
|
if (!addAttachmentEducationPerformed) {
|
||||||
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
|
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
|
||||||
setupOtpView != null
|
val validateEntryEducationPerformed = setupOtpView != null
|
||||||
&& setupOtpView.isVisible
|
&& setupOtpView.isVisible
|
||||||
&& mEntryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
|
&& mEntryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
|
||||||
setupOtpView,
|
setupOtpView,
|
||||||
{
|
{
|
||||||
setupOtp()
|
setupOtp()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
performedNextEducation()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if (!validateEntryEducationPerformed) {
|
||||||
|
val entryValidateView = validateButton
|
||||||
|
mAllowCustomFields
|
||||||
|
&& entryValidateView != null
|
||||||
|
&& entryValidateView.isVisible
|
||||||
|
&& mEntryEditActivityEducation.checkAndPerformedValidateEntryEducation(
|
||||||
|
entryValidateView,
|
||||||
|
{
|
||||||
|
validateEntry()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -653,28 +707,16 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
android.R.id.home -> {
|
android.R.id.home -> {
|
||||||
onBackPressed()
|
onDatabaseBackPressed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) {
|
override fun onDatabaseBackPressed() {
|
||||||
// To fix android 4.4 issue
|
|
||||||
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
|
|
||||||
if (datePicker?.isShown == true) {
|
|
||||||
mEntryEditViewModel.selectDate(year, month, day)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTimeSet(timePicker: TimePicker?, hours: Int, minutes: Int) {
|
|
||||||
mEntryEditViewModel.selectTime(hours, minutes)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
|
||||||
onApprovedBackPressed {
|
onApprovedBackPressed {
|
||||||
super@EntryEditActivity.onBackPressed()
|
super@EntryEditActivity.onDatabaseBackPressed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -729,7 +771,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
entryAddedOrUpdatedListener.invoke(
|
entryAddedOrUpdatedListener.invoke(
|
||||||
result.data?.getParcelableExtra(ADD_OR_UPDATE_ENTRY_KEY)
|
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
entryAddedOrUpdatedListener.invoke(null)
|
entryAddedOrUpdatedListener.invoke(null)
|
||||||
@@ -742,7 +784,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
return activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
entryAddedOrUpdatedListener.invoke(
|
entryAddedOrUpdatedListener.invoke(
|
||||||
result.data?.getParcelableExtra(ADD_OR_UPDATE_ENTRY_KEY)
|
result.data?.getParcelableExtraCompat(ADD_OR_UPDATE_ENTRY_KEY)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
entryAddedOrUpdatedListener.invoke(null)
|
entryAddedOrUpdatedListener.invoke(null)
|
||||||
@@ -754,7 +796,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
* Launch EntryEditActivity to update an existing entry by his [entryId]
|
* Launch EntryEditActivity to update an existing entry by his [entryId]
|
||||||
*/
|
*/
|
||||||
fun launchToUpdate(activity: Activity,
|
fun launchToUpdate(activity: Activity,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
entryId: NodeId<UUID>,
|
entryId: NodeId<UUID>,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
@@ -770,7 +812,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
* Launch EntryEditActivity to add a new entry in an existent group
|
* Launch EntryEditActivity to add a new entry in an existent group
|
||||||
*/
|
*/
|
||||||
fun launchToCreate(activity: Activity,
|
fun launchToCreate(activity: Activity,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
groupId: NodeId<*>,
|
groupId: NodeId<*>,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
@@ -783,7 +825,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun launchToUpdateForSave(context: Context,
|
fun launchToUpdateForSave(context: Context,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
entryId: NodeId<UUID>,
|
entryId: NodeId<UUID>,
|
||||||
searchInfo: SearchInfo) {
|
searchInfo: SearchInfo) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
@@ -800,7 +842,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun launchToCreateForSave(context: Context,
|
fun launchToCreateForSave(context: Context,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
groupId: NodeId<*>,
|
groupId: NodeId<*>,
|
||||||
searchInfo: SearchInfo) {
|
searchInfo: SearchInfo) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
@@ -820,7 +862,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
* Launch EntryEditActivity to add a new entry in keyboard selection
|
* Launch EntryEditActivity to add a new entry in keyboard selection
|
||||||
*/
|
*/
|
||||||
fun launchForKeyboardSelectionResult(context: Context,
|
fun launchForKeyboardSelectionResult(context: Context,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
groupId: NodeId<*>,
|
groupId: NodeId<*>,
|
||||||
searchInfo: SearchInfo? = null) {
|
searchInfo: SearchInfo? = null) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
@@ -841,7 +883,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
*/
|
*/
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
autofillComponent: AutofillComponent,
|
autofillComponent: AutofillComponent,
|
||||||
groupId: NodeId<*>,
|
groupId: NodeId<*>,
|
||||||
@@ -865,7 +907,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
* Launch EntryEditActivity to register an updated entry (from autofill)
|
* Launch EntryEditActivity to register an updated entry (from autofill)
|
||||||
*/
|
*/
|
||||||
fun launchToUpdateForRegistration(context: Context,
|
fun launchToUpdateForRegistration(context: Context,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
entryId: NodeId<UUID>,
|
entryId: NodeId<UUID>,
|
||||||
registerInfo: RegisterInfo? = null) {
|
registerInfo: RegisterInfo? = null) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
@@ -885,7 +927,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
* Launch EntryEditActivity to register a new entry (from autofill)
|
* Launch EntryEditActivity to register a new entry (from autofill)
|
||||||
*/
|
*/
|
||||||
fun launchToCreateForRegistration(context: Context,
|
fun launchToCreateForRegistration(context: Context,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
groupId: NodeId<*>,
|
groupId: NodeId<*>,
|
||||||
registerInfo: RegisterInfo? = null) {
|
registerInfo: RegisterInfo? = null) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
|
|||||||
@@ -26,12 +26,14 @@ import android.os.Bundle
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||||
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.utils.KeyboardUtil.isKeyboardActivatedInSettings
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
import com.kunzisoft.keepass.utils.WebDomain
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity to search or select entry in database,
|
* Activity to search or select entry in database,
|
||||||
@@ -44,17 +46,17 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun finishActivityIfReloadRequested(): Boolean {
|
override fun finishActivityIfReloadRequested(): Boolean {
|
||||||
return true
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
val keySelectionBundle = intent.getBundleExtra(KEY_SELECTION_BUNDLE)
|
val keySelectionBundle = intent.getBundleExtra(KEY_SELECTION_BUNDLE)
|
||||||
if (keySelectionBundle != null) {
|
if (keySelectionBundle != null) {
|
||||||
// To manage package name
|
// To manage package name
|
||||||
var searchInfo = SearchInfo()
|
var searchInfo = SearchInfo()
|
||||||
keySelectionBundle.getParcelable<SearchInfo>(KEY_SEARCH_INFO)?.let { mSearchInfo ->
|
keySelectionBundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { mSearchInfo ->
|
||||||
searchInfo = mSearchInfo
|
searchInfo = mSearchInfo
|
||||||
}
|
}
|
||||||
launch(database, searchInfo)
|
launch(database, searchInfo)
|
||||||
@@ -74,6 +76,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
sharedWebDomain = Uri.parse(extra).host
|
sharedWebDomain = Uri.parse(extra).host
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
launchSelection(database, sharedWebDomain, otpString)
|
||||||
}
|
}
|
||||||
Intent.ACTION_VIEW -> {
|
Intent.ACTION_VIEW -> {
|
||||||
// Retrieve OTP
|
// Retrieve OTP
|
||||||
@@ -81,28 +84,40 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
if (OtpEntryFields.isOTPUri(extra))
|
if (OtpEntryFields.isOTPUri(extra))
|
||||||
otpString = extra
|
otpString = extra
|
||||||
}
|
}
|
||||||
|
launchSelection(database, sharedWebDomain, otpString)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
if (database != null) {
|
||||||
|
GroupActivity.launch(this, database)
|
||||||
|
} else {
|
||||||
|
FileDatabaseSelectActivity.launch(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else -> {}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
// Build domain search param
|
private fun launchSelection(database: ContextualDatabase?,
|
||||||
val searchInfo = SearchInfo().apply {
|
sharedWebDomain: String?,
|
||||||
this.webDomain = sharedWebDomain
|
otpString: String?) {
|
||||||
this.otpString = otpString
|
// Build domain search param
|
||||||
}
|
val searchInfo = SearchInfo().apply {
|
||||||
|
this.webDomain = sharedWebDomain
|
||||||
|
this.otpString = otpString
|
||||||
|
}
|
||||||
|
|
||||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||||
searchInfo.webDomain = concreteWebDomain
|
searchInfo.webDomain = concreteWebDomain
|
||||||
launch(database, searchInfo)
|
launch(database, searchInfo)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launch(database: Database?,
|
private fun launch(database: ContextualDatabase?,
|
||||||
searchInfo: SearchInfo) {
|
searchInfo: SearchInfo) {
|
||||||
|
|
||||||
// Setting to integrate Magikeyboard
|
// Setting to integrate Magikeyboard
|
||||||
val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this)
|
val searchShareForMagikeyboard = isKeyboardActivatedInSettings()
|
||||||
|
|
||||||
// If database is open
|
// If database is open
|
||||||
val readOnly = database?.isReadOnly != false
|
val readOnly = database?.isReadOnly != false
|
||||||
@@ -162,7 +177,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
Toast.LENGTH_LONG)
|
Toast.LENGTH_LONG)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
} else if (readOnly || searchShareForMagikeyboard) {
|
} else if (searchShareForMagikeyboard) {
|
||||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
GroupActivity.launchForKeyboardSelectionResult(this,
|
||||||
openedDatabase,
|
openedDatabase,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
@@ -188,7 +203,6 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -205,8 +219,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
}
|
}
|
||||||
// New task needed because don't launch from an Activity context
|
// New task needed because don't launch from an Activity context
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||||
Intent.FLAG_ACTIVITY_CLEAR_TASK or
|
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,9 +53,10 @@ 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.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
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.services.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
@@ -64,8 +65,15 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
|||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_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.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.utils.*
|
import com.kunzisoft.keepass.utils.DexUtil
|
||||||
|
import com.kunzisoft.keepass.utils.MagikeyboardUtil
|
||||||
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
|
import com.kunzisoft.keepass.utils.allowCreateDocumentByStorageAccessFramework
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
@@ -155,8 +163,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
||||||
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
|
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
|
||||||
launchPasswordActivity(
|
launchPasswordActivity(
|
||||||
databaseFileUri,
|
databaseFileUri,
|
||||||
fileDatabaseHistoryEntityToOpen.keyFileUri
|
fileDatabaseHistoryEntityToOpen.keyFileUri,
|
||||||
|
fileDatabaseHistoryEntityToOpen.hardwareKey
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,23 +179,15 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
|
fileDatabaseHistoryRecyclerView.adapter = mAdapterDatabaseHistory
|
||||||
|
|
||||||
// Load default database if not an orientation change
|
// Load default database the first time
|
||||||
if (!(savedInstanceState != null
|
databaseFilesViewModel.doForDefaultDatabase { databaseFileUri ->
|
||||||
&& savedInstanceState.containsKey(EXTRA_STAY)
|
launchPasswordActivityWithPath(databaseFileUri)
|
||||||
&& savedInstanceState.getBoolean(EXTRA_STAY, false))) {
|
|
||||||
val databasePath = PreferencesUtil.getDefaultDatabasePath(this)
|
|
||||||
|
|
||||||
UriUtil.parse(databasePath)?.let { databaseFileUri ->
|
|
||||||
launchPasswordActivityWithPath(databaseFileUri)
|
|
||||||
} ?: run {
|
|
||||||
Log.i(TAG, "No default database to prepare")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the database URI provided by file manager after an orientation change
|
// Retrieve the database URI provided by file manager after an orientation change
|
||||||
if (savedInstanceState != null
|
if (savedInstanceState != null
|
||||||
&& savedInstanceState.containsKey(EXTRA_DATABASE_URI)) {
|
&& savedInstanceState.containsKey(EXTRA_DATABASE_URI)) {
|
||||||
mDatabaseFileUri = savedInstanceState.getParcelable(EXTRA_DATABASE_URI)
|
mDatabaseFileUri = savedInstanceState.getParcelableCompat(EXTRA_DATABASE_URI)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Observe list of databases
|
// Observe list of databases
|
||||||
@@ -225,7 +226,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
if (database != null) {
|
if (database != null) {
|
||||||
launchGroupActivityIfLoaded(database)
|
launchGroupActivityIfLoaded(database)
|
||||||
@@ -233,7 +234,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result
|
result: ActionRunnable.Result
|
||||||
) {
|
) {
|
||||||
@@ -244,13 +245,14 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_CREATE_TASK,
|
ACTION_DATABASE_CREATE_TASK,
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
result.data?.getParcelable<Uri?>(DATABASE_URI_KEY)?.let { databaseUri ->
|
result.data?.getParcelableCompat<Uri>(DATABASE_URI_KEY)?.let { databaseUri ->
|
||||||
val mainCredential =
|
val mainCredential =
|
||||||
result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY)
|
result.data?.getParcelableCompat(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY)
|
||||||
?: MainCredential()
|
?: MainCredential()
|
||||||
databaseFilesViewModel.addDatabaseFile(
|
databaseFilesViewModel.addDatabaseFile(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
mainCredential.keyFileUri
|
mainCredential.keyFileUri,
|
||||||
|
mainCredential.hardwareKey
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,18 +270,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
launchGroupActivityIfLoaded(database)
|
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()
|
|
||||||
}
|
}
|
||||||
|
coordinatorLayout.showActionErrorIfNeeded(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -297,10 +289,11 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) {
|
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
|
||||||
MainCredentialActivity.launch(this,
|
MainCredentialActivity.launch(this,
|
||||||
databaseUri,
|
databaseUri,
|
||||||
keyFile,
|
keyFile,
|
||||||
|
hardwareKey,
|
||||||
{ exception ->
|
{ exception ->
|
||||||
fileNoFoundAction(exception)
|
fileNoFoundAction(exception)
|
||||||
},
|
},
|
||||||
@@ -309,7 +302,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
mAutofillActivityResultLauncher)
|
mAutofillActivityResultLauncher)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchGroupActivityIfLoaded(database: Database) {
|
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
GroupActivity.launch(this,
|
GroupActivity.launch(this,
|
||||||
database,
|
database,
|
||||||
@@ -320,19 +313,10 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onValidateSpecialMode() {
|
|
||||||
super.onValidateSpecialMode()
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCancelSpecialMode() {
|
|
||||||
super.onCancelSpecialMode()
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
||||||
launchPasswordActivity(databaseUri, null)
|
launchPasswordActivity(databaseUri, null, null)
|
||||||
// Delete flickering for kitkat <=
|
// Delete flickering for kitkat <=
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
||||||
overridePendingTransition(0, 0)
|
overridePendingTransition(0, 0)
|
||||||
}
|
}
|
||||||
@@ -341,12 +325,12 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
// Define special title
|
// Define special title
|
||||||
specialTitle?.isVisible = UriUtil.contributingUser(this)
|
specialTitle?.isVisible = this.isContributingUser()
|
||||||
|
|
||||||
// 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 (ExternalFileHelper.allowCreateDocumentByStorageAccessFramework(packageManager)) {
|
if (packageManager.allowCreateDocumentByStorageAccessFramework()) {
|
||||||
// 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{
|
||||||
@@ -374,8 +358,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
// only to keep the current activity
|
|
||||||
outState.putBoolean(EXTRA_STAY, true)
|
|
||||||
// to retrieve the URI of a created database after an orientation change
|
// to retrieve the URI of a created database after an orientation change
|
||||||
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
outState.putParcelable(EXTRA_DATABASE_URI, mDatabaseFileUri)
|
||||||
}
|
}
|
||||||
@@ -441,7 +423,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
android.R.id.home -> UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
|
android.R.id.home -> this.openUrl(R.string.file_manager_explanation_url)
|
||||||
}
|
}
|
||||||
MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
@@ -450,7 +432,6 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val TAG = "FileDbSelectActivity"
|
private const val TAG = "FileDbSelectActivity"
|
||||||
private const val EXTRA_STAY = "EXTRA_STAY"
|
|
||||||
private const val EXTRA_DATABASE_URI = "EXTRA_DATABASE_URI"
|
private const val EXTRA_DATABASE_URI = "EXTRA_DATABASE_URI"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -19,21 +19,27 @@
|
|||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.DatePickerDialog
|
|
||||||
import android.app.SearchManager
|
import android.app.SearchManager
|
||||||
import android.app.TimePickerDialog
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.PorterDuff
|
import android.graphics.PorterDuff
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.*
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.*
|
import android.widget.ImageView
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
@@ -49,8 +55,13 @@ import androidx.core.view.WindowInsetsControllerCompat
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.drawerlayout.widget.DrawerLayout
|
import androidx.drawerlayout.widget.DrawerLayout
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.android.material.datepicker.MaterialDatePicker
|
||||||
|
import com.google.android.material.timepicker.MaterialTimePicker
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.*
|
import com.kunzisoft.keepass.activities.dialogs.GroupDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.MainCredentialDialogFragment
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.fragments.GroupFragment
|
import com.kunzisoft.keepass.activities.fragments.GroupFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
@@ -59,21 +70,25 @@ import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
|||||||
import com.kunzisoft.keepass.adapters.BreadcrumbAdapter
|
import com.kunzisoft.keepass.adapters.BreadcrumbAdapter
|
||||||
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.element.*
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
|
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||||
import com.kunzisoft.keepass.education.GroupActivityEducation
|
import com.kunzisoft.keepass.education.GroupActivityEducation
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||||
|
import com.kunzisoft.keepass.model.DataTime
|
||||||
import com.kunzisoft.keepass.model.GroupInfo
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
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.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK
|
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
@@ -81,16 +96,30 @@ import com.kunzisoft.keepass.settings.SettingsActivity
|
|||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.KeyboardUtil.showKeyboard
|
||||||
import com.kunzisoft.keepass.view.*
|
import com.kunzisoft.keepass.utils.TimeUtil.datePickerToDataDate
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableList
|
||||||
|
import com.kunzisoft.keepass.utils.putParcelableList
|
||||||
|
import com.kunzisoft.keepass.utils.readParcelableCompat
|
||||||
|
import com.kunzisoft.keepass.view.AddNodeButtonView
|
||||||
|
import com.kunzisoft.keepass.view.NavigationDatabaseView
|
||||||
|
import com.kunzisoft.keepass.view.SearchFiltersView
|
||||||
|
import com.kunzisoft.keepass.view.ToolbarAction
|
||||||
|
import com.kunzisoft.keepass.view.WindowInsetPosition
|
||||||
|
import com.kunzisoft.keepass.view.applyWindowInsets
|
||||||
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
|
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
||||||
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
|
import com.kunzisoft.keepass.view.updateLockPaddingStart
|
||||||
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
|
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
|
||||||
import com.kunzisoft.keepass.viewmodels.GroupViewModel
|
import com.kunzisoft.keepass.viewmodels.GroupViewModel
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.LocalDateTime
|
||||||
|
|
||||||
|
|
||||||
class GroupActivity : DatabaseLockActivity(),
|
class GroupActivity : DatabaseLockActivity(),
|
||||||
DatePickerDialog.OnDateSetListener,
|
|
||||||
TimePickerDialog.OnTimeSetListener,
|
|
||||||
GroupFragment.NodeClickListener,
|
GroupFragment.NodeClickListener,
|
||||||
GroupFragment.NodesActionMenuListener,
|
GroupFragment.NodesActionMenuListener,
|
||||||
GroupFragment.OnScrollListener,
|
GroupFragment.OnScrollListener,
|
||||||
@@ -99,18 +128,19 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
MainCredentialDialogFragment.AskMainCredentialDialogListener {
|
MainCredentialDialogFragment.AskMainCredentialDialogListener {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
|
private var header: ViewGroup? = null
|
||||||
|
private var footer: ViewGroup? = null
|
||||||
private var drawerLayout: DrawerLayout? = null
|
private var drawerLayout: DrawerLayout? = null
|
||||||
private var databaseNavView: NavigationDatabaseView? = null
|
private var databaseNavView: NavigationDatabaseView? = null
|
||||||
private var coordinatorLayout: CoordinatorLayout? = null
|
private var coordinatorLayout: CoordinatorLayout? = null
|
||||||
|
private var coordinatorError: CoordinatorLayout? = null
|
||||||
private var lockView: View? = null
|
private var lockView: View? = null
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
private var databaseNameContainer: ViewGroup? = null
|
|
||||||
private var databaseModifiedView: ImageView? = null
|
private var databaseModifiedView: ImageView? = null
|
||||||
private var databaseColorView: ImageView? = null
|
private var databaseColorView: ImageView? = null
|
||||||
private var databaseNameView: TextView? = null
|
private var databaseNameView: TextView? = null
|
||||||
private var searchView: SearchView? = null
|
private var searchView: SearchView? = null
|
||||||
private var searchFiltersView: SearchFiltersView? = null
|
private var searchFiltersView: SearchFiltersView? = null
|
||||||
private var toolbarBreadcrumb: Toolbar? = null
|
|
||||||
private var toolbarAction: ToolbarAction? = null
|
private var toolbarAction: ToolbarAction? = null
|
||||||
private var numberChildrenView: TextView? = null
|
private var numberChildrenView: TextView? = null
|
||||||
private var addNodeButtonView: AddNodeButtonView? = null
|
private var addNodeButtonView: AddNodeButtonView? = null
|
||||||
@@ -124,8 +154,6 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
private var mBreadcrumbAdapter: BreadcrumbAdapter? = null
|
private var mBreadcrumbAdapter: BreadcrumbAdapter? = null
|
||||||
|
|
||||||
private var mSearchMenuItem: MenuItem? = null
|
|
||||||
|
|
||||||
private var mGroupFragment: GroupFragment? = null
|
private var mGroupFragment: GroupFragment? = null
|
||||||
private var mRecyclingBinEnabled = false
|
private var mRecyclingBinEnabled = false
|
||||||
private var mRecyclingBinIsCurrentGroup = false
|
private var mRecyclingBinIsCurrentGroup = false
|
||||||
@@ -138,6 +166,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
// Manage group
|
// Manage group
|
||||||
private var mSearchState: SearchState? = null
|
private var mSearchState: SearchState? = null
|
||||||
|
private var mAutoSearch: Boolean = false // To mainly manage keyboard
|
||||||
private var mMainGroupState: GroupState? = null // Group state, not a search
|
private var mMainGroupState: GroupState? = null // Group state, not a search
|
||||||
private var mRootGroup: Group? = null // Root group in the tree
|
private var mRootGroup: Group? = null // Root group in the tree
|
||||||
private var mMainGroup: Group? = null // Main group currently in memory
|
private var mMainGroup: Group? = null // Main group currently in memory
|
||||||
@@ -177,22 +206,16 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val mOnSearchActionExpandListener = object : MenuItem.OnActionExpandListener {
|
private val mOnSearchActionExpandListener = object : MenuItem.OnActionExpandListener {
|
||||||
override fun onMenuItemActionExpand(p0: MenuItem?): Boolean {
|
override fun onMenuItemActionExpand(p0: MenuItem): Boolean {
|
||||||
searchFiltersView?.visibility = View.VISIBLE
|
searchFiltersView?.visibility = View.VISIBLE
|
||||||
searchView?.setOnQueryTextListener(mOnSearchQueryTextListener)
|
searchView?.setOnQueryTextListener(mOnSearchQueryTextListener)
|
||||||
searchFiltersView?.onParametersChangeListener = mOnSearchFiltersChangeListener
|
searchFiltersView?.onParametersChangeListener = mOnSearchFiltersChangeListener
|
||||||
|
|
||||||
addSearch()
|
addSearch()
|
||||||
//loadGroup()
|
|
||||||
|
|
||||||
// Back to previous keyboard
|
|
||||||
if (PreferencesUtil.isKeyboardPreviousSearchEnable(this@GroupActivity)) {
|
|
||||||
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemActionCollapse(p0: MenuItem?): Boolean {
|
override fun onMenuItemActionCollapse(p0: MenuItem): Boolean {
|
||||||
searchFiltersView?.onParametersChangeListener = null
|
searchFiltersView?.onParametersChangeListener = null
|
||||||
searchView?.setOnQueryTextListener(null)
|
searchView?.setOnQueryTextListener(null)
|
||||||
searchFiltersView?.visibility = View.GONE
|
searchFiltersView?.visibility = View.GONE
|
||||||
@@ -202,6 +225,22 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private val mOnSearchTextFocusChangeListener = View.OnFocusChangeListener { view, hasFocus ->
|
||||||
|
if (!mAutoSearch
|
||||||
|
&& hasFocus
|
||||||
|
&& PreferencesUtil.isKeyboardPreviousSearchEnable(this@GroupActivity)) {
|
||||||
|
// Change to the previous keyboard and show it
|
||||||
|
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
||||||
|
view.showKeyboard()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val mEntryActivityResultLauncher = EntryEditActivity.registerForEntryResult(this) { entryId ->
|
||||||
|
entryId?.let {
|
||||||
|
// Simply refresh the list when entry is updated
|
||||||
|
loadGroup()
|
||||||
|
} ?: Log.e(this.javaClass.name, "Entry cannot be retrieved in Activity Result")
|
||||||
|
}
|
||||||
|
|
||||||
private fun addSearch() {
|
private fun addSearch() {
|
||||||
finishNodeAction()
|
finishNodeAction()
|
||||||
@@ -212,7 +251,6 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun removeSearch() {
|
private fun removeSearch() {
|
||||||
finishNodeAction()
|
|
||||||
mSearchState = null
|
mSearchState = null
|
||||||
intent.removeExtra(AUTO_SEARCH_KEY)
|
intent.removeExtra(AUTO_SEARCH_KEY)
|
||||||
if (Intent.ACTION_SEARCH == intent.action) {
|
if (Intent.ACTION_SEARCH == intent.action) {
|
||||||
@@ -238,23 +276,30 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
setContentView(layoutInflater.inflate(R.layout.activity_group, null))
|
setContentView(layoutInflater.inflate(R.layout.activity_group, null))
|
||||||
|
|
||||||
// Initialize views
|
// Initialize views
|
||||||
|
header = findViewById(R.id.activity_group_header)
|
||||||
|
footer = findViewById(R.id.activity_group_footer)
|
||||||
drawerLayout = findViewById(R.id.drawer_layout)
|
drawerLayout = findViewById(R.id.drawer_layout)
|
||||||
databaseNavView = findViewById(R.id.database_nav_view)
|
databaseNavView = findViewById(R.id.database_nav_view)
|
||||||
coordinatorLayout = findViewById(R.id.group_coordinator)
|
coordinatorLayout = findViewById(R.id.group_coordinator)
|
||||||
|
coordinatorError = findViewById(R.id.error_coordinator)
|
||||||
numberChildrenView = findViewById(R.id.group_numbers)
|
numberChildrenView = findViewById(R.id.group_numbers)
|
||||||
addNodeButtonView = findViewById(R.id.add_node_button)
|
addNodeButtonView = findViewById(R.id.add_node_button)
|
||||||
toolbar = findViewById(R.id.toolbar)
|
toolbar = findViewById(R.id.toolbar)
|
||||||
databaseNameContainer = findViewById(R.id.database_name_container)
|
|
||||||
databaseModifiedView = findViewById(R.id.database_modified)
|
databaseModifiedView = findViewById(R.id.database_modified)
|
||||||
databaseColorView = findViewById(R.id.database_color)
|
databaseColorView = findViewById(R.id.database_color)
|
||||||
databaseNameView = findViewById(R.id.database_name)
|
databaseNameView = findViewById(R.id.database_name)
|
||||||
searchFiltersView = findViewById(R.id.search_filters)
|
searchFiltersView = findViewById(R.id.search_filters)
|
||||||
toolbarBreadcrumb = findViewById(R.id.toolbar_breadcrumb)
|
|
||||||
breadcrumbListView = findViewById(R.id.breadcrumb_list)
|
breadcrumbListView = findViewById(R.id.breadcrumb_list)
|
||||||
toolbarAction = findViewById(R.id.toolbar_action)
|
toolbarAction = findViewById(R.id.toolbar_action)
|
||||||
lockView = findViewById(R.id.lock_button)
|
lockView = findViewById(R.id.lock_button)
|
||||||
loadingView = findViewById(R.id.loading)
|
loadingView = findViewById(R.id.loading)
|
||||||
|
|
||||||
|
// To apply fit window with transparency
|
||||||
|
setTransparentNavigationBar(applyToStatusBar = true) {
|
||||||
|
drawerLayout?.applyWindowInsets(WindowInsetPosition.TOP_BOTTOM_IME)
|
||||||
|
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM_IME)
|
||||||
|
}
|
||||||
|
|
||||||
lockView?.setOnClickListener {
|
lockView?.setOnClickListener {
|
||||||
lockAndExit()
|
lockAndExit()
|
||||||
}
|
}
|
||||||
@@ -297,14 +342,15 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
R.id.menu_save_copy_to -> {
|
R.id.menu_save_copy_to -> {
|
||||||
mExternalFileHelper?.createDocument(
|
mExternalFileHelper?.createDocument(
|
||||||
getString(R.string.database_file_name_default) +
|
getString(R.string.database_file_name_default) +
|
||||||
getString(R.string.database_file_name_copy) +
|
"_" +
|
||||||
mDatabase?.defaultFileExtension)
|
LocalDateTime.now().toString() +
|
||||||
|
mDatabase?.defaultFileExtension)
|
||||||
}
|
}
|
||||||
R.id.menu_lock_all -> {
|
R.id.menu_lock_all -> {
|
||||||
lockAndExit()
|
lockAndExit()
|
||||||
}
|
}
|
||||||
R.id.menu_contribute -> {
|
R.id.menu_contribute -> {
|
||||||
UriUtil.gotoUrl(this@GroupActivity, R.string.contribution_url)
|
this@GroupActivity.openUrl(R.string.contribution_url)
|
||||||
}
|
}
|
||||||
R.id.menu_about -> {
|
R.id.menu_about -> {
|
||||||
startActivity(Intent(this@GroupActivity, AboutActivity::class.java))
|
startActivity(Intent(this@GroupActivity, AboutActivity::class.java))
|
||||||
@@ -316,43 +362,6 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
searchFiltersView?.closeAdvancedFilters()
|
searchFiltersView?.closeAdvancedFilters()
|
||||||
|
|
||||||
mBreadcrumbAdapter = BreadcrumbAdapter(this).apply {
|
|
||||||
// Open group on breadcrumb click
|
|
||||||
onItemClickListener = { node, _ ->
|
|
||||||
// If last item & not a virtual root group
|
|
||||||
val currentGroup = mMainGroup
|
|
||||||
if (currentGroup != null && node == currentGroup
|
|
||||||
&& (currentGroup != mDatabase?.rootGroup
|
|
||||||
|| mDatabase?.rootGroupIsVirtual == false)
|
|
||||||
) {
|
|
||||||
finishNodeAction()
|
|
||||||
launchDialogToShowGroupInfo(currentGroup)
|
|
||||||
} else {
|
|
||||||
if (mGroupFragment?.nodeActionSelectionMode == true) {
|
|
||||||
finishNodeAction()
|
|
||||||
}
|
|
||||||
mDatabase?.let { database ->
|
|
||||||
onNodeClick(database, node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onLongItemClickListener = { node, position ->
|
|
||||||
val currentGroup = mMainGroup
|
|
||||||
if (currentGroup != null && node == currentGroup
|
|
||||||
&& (currentGroup != mDatabase?.rootGroup
|
|
||||||
|| mDatabase?.rootGroupIsVirtual == false)
|
|
||||||
) {
|
|
||||||
finishNodeAction()
|
|
||||||
launchDialogForGroupUpdate(currentGroup)
|
|
||||||
} else {
|
|
||||||
onItemClickListener?.invoke(node, position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
breadcrumbListView?.apply {
|
|
||||||
adapter = mBreadcrumbAdapter
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve group if defined at launch
|
// Retrieve group if defined at launch
|
||||||
manageIntent(intent)
|
manageIntent(intent)
|
||||||
|
|
||||||
@@ -363,7 +372,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
savedInstanceState.remove(REQUEST_STARTUP_SEARCH_KEY)
|
savedInstanceState.remove(REQUEST_STARTUP_SEARCH_KEY)
|
||||||
}
|
}
|
||||||
if (savedInstanceState.containsKey(OLD_GROUP_TO_UPDATE_KEY)) {
|
if (savedInstanceState.containsKey(OLD_GROUP_TO_UPDATE_KEY)) {
|
||||||
mOldGroupToUpdate = savedInstanceState.getParcelable(OLD_GROUP_TO_UPDATE_KEY)
|
mOldGroupToUpdate = savedInstanceState.getParcelableCompat(OLD_GROUP_TO_UPDATE_KEY)
|
||||||
savedInstanceState.remove(OLD_GROUP_TO_UPDATE_KEY)
|
savedInstanceState.remove(OLD_GROUP_TO_UPDATE_KEY)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -371,9 +380,8 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
// Retrieve previous groups
|
// Retrieve previous groups
|
||||||
if (savedInstanceState != null && savedInstanceState.containsKey(PREVIOUS_GROUPS_IDS_KEY)) {
|
if (savedInstanceState != null && savedInstanceState.containsKey(PREVIOUS_GROUPS_IDS_KEY)) {
|
||||||
try {
|
try {
|
||||||
mPreviousGroupsIds =
|
mPreviousGroupsIds = savedInstanceState.getParcelableList(PREVIOUS_GROUPS_IDS_KEY)
|
||||||
(savedInstanceState.getParcelableArray(PREVIOUS_GROUPS_IDS_KEY)
|
?: mutableListOf()
|
||||||
?.map { it as GroupState })?.toMutableList() ?: mutableListOf()
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to retrieve previous groups", e)
|
Log.e(TAG, "Unable to retrieve previous groups", e)
|
||||||
}
|
}
|
||||||
@@ -409,20 +417,14 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
val currentGroup = it.group
|
val currentGroup = it.group
|
||||||
mCurrentGroup = currentGroup
|
mCurrentGroup = currentGroup
|
||||||
if (currentGroup.isVirtual) {
|
if (currentGroup.isVirtual) {
|
||||||
val searchParameters = it.searchParameters
|
mSearchState = SearchState(
|
||||||
mSearchState = SearchState(searchParameters, it.showFromPosition)
|
it.searchParameters,
|
||||||
|
it.showFromPosition
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// Main and search groups in activity are managed with another variables
|
// Main and search groups in activity are managed with another variables
|
||||||
// to keep values during orientation
|
// to keep values during orientation
|
||||||
|
|
||||||
// Expand the search view if defined in settings
|
|
||||||
if (mRequestStartupSearch
|
|
||||||
&& PreferencesUtil.automaticallyFocusSearch(this@GroupActivity)) {
|
|
||||||
// To request search only one time
|
|
||||||
mRequestStartupSearch = false
|
|
||||||
mSearchMenuItem?.expandActionView()
|
|
||||||
}
|
|
||||||
|
|
||||||
loadingView?.hideByFading()
|
loadingView?.hideByFading()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,18 +440,20 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
mGroupEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
|
mGroupEditViewModel.requestDateTimeSelection.observe(this) { dateInstant ->
|
||||||
if (dateInstant.type == DateInstant.Type.TIME) {
|
if (dateInstant.type == DateInstant.Type.TIME) {
|
||||||
// Launch the time picker
|
// Launch the time picker
|
||||||
val dateTime = DateTime(dateInstant.date)
|
MaterialTimePicker.Builder().build().apply {
|
||||||
TimePickerFragment.getInstance(dateTime.hourOfDay, dateTime.minuteOfHour)
|
addOnPositiveButtonClickListener {
|
||||||
.show(supportFragmentManager, "TimePickerFragment")
|
mGroupEditViewModel.selectTime(DataTime(this.hour, this.minute))
|
||||||
|
}
|
||||||
|
show(supportFragmentManager, "TimePickerFragment")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Launch the date picker
|
// Launch the date picker
|
||||||
val dateTime = DateTime(dateInstant.date)
|
MaterialDatePicker.Builder.datePicker().build().apply {
|
||||||
DatePickerFragment.getInstance(
|
addOnPositiveButtonClickListener {
|
||||||
dateTime.year,
|
mGroupEditViewModel.selectDate(datePickerToDataDate(it))
|
||||||
dateTime.monthOfYear - 1,
|
}
|
||||||
dateTime.dayOfMonth
|
show(supportFragmentManager, "DatePickerFragment")
|
||||||
)
|
}
|
||||||
.show(supportFragmentManager, "DatePickerFragment")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,14 +487,12 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
EntrySelectionHelper.doSpecialAction(intent,
|
EntrySelectionHelper.doSpecialAction(intent,
|
||||||
{
|
{
|
||||||
mMainGroup?.nodeId?.let { currentParentGroupId ->
|
mMainGroup?.nodeId?.let { currentParentGroupId ->
|
||||||
mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
|
EntryEditActivity.launchToCreate(
|
||||||
EntryEditActivity.launchToCreate(
|
this@GroupActivity,
|
||||||
this@GroupActivity,
|
database,
|
||||||
database,
|
currentParentGroupId,
|
||||||
currentParentGroupId,
|
mEntryActivityResultLauncher
|
||||||
resultLauncher
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -578,9 +580,46 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
|
mBreadcrumbAdapter = BreadcrumbAdapter(this, database).apply {
|
||||||
|
// Open group on breadcrumb click
|
||||||
|
onItemClickListener = { node, _ ->
|
||||||
|
// If last item & not a virtual root group
|
||||||
|
val currentGroup = mMainGroup
|
||||||
|
if (currentGroup != null && node == currentGroup
|
||||||
|
&& (currentGroup != mDatabase?.rootGroup
|
||||||
|
|| mDatabase?.rootGroupIsVirtual == false)
|
||||||
|
) {
|
||||||
|
finishNodeAction()
|
||||||
|
launchDialogToShowGroupInfo(currentGroup)
|
||||||
|
} else {
|
||||||
|
if (mGroupFragment?.nodeActionSelectionMode == true) {
|
||||||
|
finishNodeAction()
|
||||||
|
}
|
||||||
|
mDatabase?.let { database ->
|
||||||
|
onNodeClick(database, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onLongItemClickListener = { node, position ->
|
||||||
|
val currentGroup = mMainGroup
|
||||||
|
if (currentGroup != null && node == currentGroup
|
||||||
|
&& (currentGroup != mDatabase?.rootGroup
|
||||||
|
|| mDatabase?.rootGroupIsVirtual == false)
|
||||||
|
) {
|
||||||
|
finishNodeAction()
|
||||||
|
launchDialogForGroupUpdate(currentGroup)
|
||||||
|
} else {
|
||||||
|
onItemClickListener?.invoke(node, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
breadcrumbListView?.apply {
|
||||||
|
adapter = mBreadcrumbAdapter
|
||||||
|
}
|
||||||
|
|
||||||
mGroupEditViewModel.setGroupNamesNotAllowed(database?.groupNamesNotAllowed)
|
mGroupEditViewModel.setGroupNamesNotAllowed(database?.groupNamesNotAllowed)
|
||||||
|
|
||||||
mRecyclingBinEnabled = !mDatabaseReadOnly
|
mRecyclingBinEnabled = !mDatabaseReadOnly
|
||||||
@@ -622,15 +661,19 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result
|
result: ActionRunnable.Result
|
||||||
) {
|
) {
|
||||||
super.onDatabaseActionFinished(database, actionTask, result)
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
|
|
||||||
var newNodes: List<Node> = ArrayList()
|
var entry: Entry? = null
|
||||||
result.data?.getBundle(NEW_NODES_KEY)?.let { newNodesBundle ->
|
try {
|
||||||
newNodes = getListNodesFromBundle(database, newNodesBundle)
|
result.data?.getBundle(NEW_NODES_KEY)?.let { newNodesBundle ->
|
||||||
|
entry = getListNodesFromBundle(database, newNodesBundle)[0] as Entry
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to retrieve entry action for selection", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
@@ -647,27 +690,15 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
// Save not used
|
// Save not used
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
try {
|
// Keyboard selection
|
||||||
val entry = newNodes[0] as Entry
|
entry?.let {
|
||||||
entrySelectedForKeyboardSelection(database, entry)
|
entrySelectedForKeyboardSelection(database, it)
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(
|
|
||||||
TAG,
|
|
||||||
"Unable to perform action for keyboard selection after entry update",
|
|
||||||
e
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ _, _ ->
|
{ _, _ ->
|
||||||
try {
|
// Autofill selection
|
||||||
val entry = newNodes[0] as Entry
|
entry?.let {
|
||||||
entrySelectedForAutofillSelection(database, entry)
|
entrySelectedForAutofillSelection(database, it)
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(
|
|
||||||
TAG,
|
|
||||||
"Unable to perform action for autofill selection after entry update",
|
|
||||||
e
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -676,26 +707,12 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ACTION_DATABASE_UPDATE_GROUP_TASK -> {
|
|
||||||
if (result.isSuccess) {
|
|
||||||
try {
|
|
||||||
if (mMainGroup == newNodes[0] as Group)
|
|
||||||
reloadCurrentGroup()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(
|
|
||||||
TAG,
|
|
||||||
"Unable to perform action after group update",
|
|
||||||
e
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
coordinatorLayout?.showActionErrorIfNeeded(result)
|
coordinatorError?.showActionErrorIfNeeded(result)
|
||||||
if (!result.isSuccess) {
|
|
||||||
reloadCurrentGroup()
|
// Reload the group
|
||||||
}
|
loadGroup()
|
||||||
finishNodeAction()
|
finishNodeAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -716,21 +733,27 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
private fun manageIntent(intent: Intent?) {
|
private fun manageIntent(intent: Intent?) {
|
||||||
intent?.let {
|
intent?.let {
|
||||||
if (intent.extras?.containsKey(GROUP_STATE_KEY) == true) {
|
if (intent.extras?.containsKey(GROUP_STATE_KEY) == true) {
|
||||||
mMainGroupState = intent.getParcelableExtra(GROUP_STATE_KEY)
|
mMainGroupState = intent.getParcelableExtraCompat(GROUP_STATE_KEY)
|
||||||
intent.removeExtra(GROUP_STATE_KEY)
|
intent.removeExtra(GROUP_STATE_KEY)
|
||||||
}
|
}
|
||||||
// To transform KEY_SEARCH_INFO in ACTION_SEARCH
|
// To transform KEY_SEARCH_INFO in ACTION_SEARCH
|
||||||
transformSearchInfoIntent(intent)
|
transformSearchInfoIntent(intent)
|
||||||
// Get search query
|
// Get search query
|
||||||
if (intent.action == Intent.ACTION_SEARCH) {
|
if (intent.action == Intent.ACTION_SEARCH) {
|
||||||
|
mAutoSearch = true
|
||||||
val stringQuery = intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
|
val stringQuery = intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: ""
|
||||||
intent.action = Intent.ACTION_DEFAULT
|
intent.action = Intent.ACTION_DEFAULT
|
||||||
intent.removeExtra(SearchManager.QUERY)
|
intent.removeExtra(SearchManager.QUERY)
|
||||||
mSearchState = SearchState(PreferencesUtil.getDefaultSearchParameters(this).apply {
|
mSearchState = SearchState(PreferencesUtil.getDefaultSearchParameters(this).apply {
|
||||||
searchQuery = stringQuery
|
searchQuery = stringQuery
|
||||||
}, mSearchState?.firstVisibleItem ?: 0)
|
}, mSearchState?.firstVisibleItem ?: 0)
|
||||||
|
} else if (mRequestStartupSearch
|
||||||
|
&& PreferencesUtil.automaticallyFocusSearch(this@GroupActivity)) {
|
||||||
|
// Expand the search view if defined in settings
|
||||||
|
// To request search only one time
|
||||||
|
mRequestStartupSearch = false
|
||||||
|
addSearch()
|
||||||
}
|
}
|
||||||
loadGroup()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -742,7 +765,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
outState.putParcelableArray(PREVIOUS_GROUPS_IDS_KEY, mPreviousGroupsIds.toTypedArray())
|
outState.putParcelableList(PREVIOUS_GROUPS_IDS_KEY, mPreviousGroupsIds)
|
||||||
mOldGroupToUpdate?.let {
|
mOldGroupToUpdate?.let {
|
||||||
outState.putParcelable(OLD_GROUP_TO_UPDATE_KEY, it)
|
outState.putParcelable(OLD_GROUP_TO_UPDATE_KEY, it)
|
||||||
}
|
}
|
||||||
@@ -803,7 +826,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onNodeClick(
|
override fun onNodeClick(
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
node: Node
|
node: Node
|
||||||
) {
|
) {
|
||||||
when (node.type) {
|
when (node.type) {
|
||||||
@@ -817,7 +840,6 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
// Open child group
|
// Open child group
|
||||||
loadMainGroup(GroupState(group.nodeId, 0))
|
loadMainGroup(GroupState(group.nodeId, 0))
|
||||||
|
|
||||||
} catch (e: ClassCastException) {
|
} catch (e: ClassCastException) {
|
||||||
Log.e(TAG, "Node can't be cast in Group")
|
Log.e(TAG, "Node can't be cast in Group")
|
||||||
}
|
}
|
||||||
@@ -826,22 +848,22 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
val entryVersioned = node as Entry
|
val entryVersioned = node as Entry
|
||||||
EntrySelectionHelper.doSpecialAction(intent,
|
EntrySelectionHelper.doSpecialAction(intent,
|
||||||
{
|
{
|
||||||
mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
|
EntryActivity.launch(
|
||||||
EntryActivity.launch(
|
this@GroupActivity,
|
||||||
this@GroupActivity,
|
database,
|
||||||
database,
|
entryVersioned.nodeId,
|
||||||
entryVersioned.nodeId,
|
mEntryActivityResultLauncher
|
||||||
resultLauncher
|
)
|
||||||
)
|
// Do not reload group here
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Nothing here, a search is simply performed
|
// Nothing here, a search is simply performed
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
{ searchInfo ->
|
||||||
if (!database.isReadOnly)
|
if (!database.isReadOnly) {
|
||||||
entrySelectedForSave(database, entryVersioned, searchInfo)
|
entrySelectedForSave(database, entryVersioned, searchInfo)
|
||||||
else
|
loadGroup()
|
||||||
|
} else
|
||||||
finish()
|
finish()
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
{ searchInfo ->
|
||||||
@@ -852,6 +874,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
updateEntryWithSearchInfo(database, entryVersioned, searchInfo)
|
updateEntryWithSearchInfo(database, entryVersioned, searchInfo)
|
||||||
}
|
}
|
||||||
entrySelectedForKeyboardSelection(database, entryVersioned)
|
entrySelectedForKeyboardSelection(database, entryVersioned)
|
||||||
|
loadGroup()
|
||||||
},
|
},
|
||||||
{ searchInfo, _ ->
|
{ searchInfo, _ ->
|
||||||
if (!database.isReadOnly
|
if (!database.isReadOnly
|
||||||
@@ -861,23 +884,23 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
updateEntryWithSearchInfo(database, entryVersioned, searchInfo)
|
updateEntryWithSearchInfo(database, entryVersioned, searchInfo)
|
||||||
}
|
}
|
||||||
entrySelectedForAutofillSelection(database, entryVersioned)
|
entrySelectedForAutofillSelection(database, entryVersioned)
|
||||||
|
loadGroup()
|
||||||
},
|
},
|
||||||
{ registerInfo ->
|
{ registerInfo ->
|
||||||
if (!database.isReadOnly)
|
if (!database.isReadOnly) {
|
||||||
entrySelectedForRegistration(database, entryVersioned, registerInfo)
|
entrySelectedForRegistration(database, entryVersioned, registerInfo)
|
||||||
else
|
loadGroup()
|
||||||
|
} else
|
||||||
finish()
|
finish()
|
||||||
})
|
})
|
||||||
} catch (e: ClassCastException) {
|
} catch (e: ClassCastException) {
|
||||||
Log.e(TAG, "Node can't be cast in Entry")
|
Log.e(TAG, "Node can't be cast in Entry")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadGroupIfSearch()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun entrySelectedForSave(database: Database, entry: Entry, searchInfo: SearchInfo) {
|
private fun entrySelectedForSave(database: ContextualDatabase, entry: Entry, searchInfo: SearchInfo) {
|
||||||
reloadCurrentGroup()
|
removeSearch()
|
||||||
// Save to update the entry
|
// Save to update the entry
|
||||||
EntryEditActivity.launchToUpdateForSave(
|
EntryEditActivity.launchToUpdateForSave(
|
||||||
this@GroupActivity,
|
this@GroupActivity,
|
||||||
@@ -888,8 +911,8 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun entrySelectedForKeyboardSelection(database: Database, entry: Entry) {
|
private fun entrySelectedForKeyboardSelection(database: ContextualDatabase, entry: Entry) {
|
||||||
reloadCurrentGroup()
|
removeSearch()
|
||||||
// Populate Magikeyboard with entry
|
// Populate Magikeyboard with entry
|
||||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||||
this,
|
this,
|
||||||
@@ -898,7 +921,8 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
onValidateSpecialMode()
|
onValidateSpecialMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun entrySelectedForAutofillSelection(database: Database, entry: Entry) {
|
private fun entrySelectedForAutofillSelection(database: ContextualDatabase, entry: Entry) {
|
||||||
|
removeSearch()
|
||||||
// Build response with the entry selected
|
// Build response with the entry selected
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
AutofillHelper.buildResponseAndSetResult(
|
AutofillHelper.buildResponseAndSetResult(
|
||||||
@@ -911,11 +935,11 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun entrySelectedForRegistration(
|
private fun entrySelectedForRegistration(
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
entry: Entry,
|
entry: Entry,
|
||||||
registerInfo: RegisterInfo?
|
registerInfo: RegisterInfo?
|
||||||
) {
|
) {
|
||||||
reloadCurrentGroup()
|
removeSearch()
|
||||||
// Registration to update the entry
|
// Registration to update the entry
|
||||||
EntryEditActivity.launchToUpdateForRegistration(
|
EntryEditActivity.launchToUpdateForRegistration(
|
||||||
this@GroupActivity,
|
this@GroupActivity,
|
||||||
@@ -927,7 +951,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateEntryWithSearchInfo(
|
private fun updateEntryWithSearchInfo(
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
entry: Entry,
|
entry: Entry,
|
||||||
searchInfo: SearchInfo
|
searchInfo: SearchInfo
|
||||||
) {
|
) {
|
||||||
@@ -944,30 +968,12 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) {
|
|
||||||
// To fix android 4.4 issue
|
|
||||||
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
|
|
||||||
if (datePicker?.isShown == true) {
|
|
||||||
mGroupEditViewModel.selectDate(year, month, day)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTimeSet(view: TimePicker?, hours: Int, minutes: Int) {
|
|
||||||
mGroupEditViewModel.selectTime(hours, minutes)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun finishNodeAction() {
|
private fun finishNodeAction() {
|
||||||
actionNodeMode?.finish()
|
actionNodeMode?.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun reloadGroupIfSearch() {
|
|
||||||
if (Intent.ACTION_SEARCH == intent.action) {
|
|
||||||
reloadCurrentGroup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNodeSelected(
|
override fun onNodeSelected(
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
nodes: List<Node>
|
nodes: List<Node>
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (nodes.isNotEmpty()) {
|
if (nodes.isNotEmpty()) {
|
||||||
@@ -993,7 +999,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenuClick(
|
override fun onOpenMenuClick(
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
node: Node
|
node: Node
|
||||||
): Boolean {
|
): Boolean {
|
||||||
finishNodeAction()
|
finishNodeAction()
|
||||||
@@ -1002,7 +1008,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onEditMenuClick(
|
override fun onEditMenuClick(
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
node: Node
|
node: Node
|
||||||
): Boolean {
|
): Boolean {
|
||||||
finishNodeAction()
|
finishNodeAction()
|
||||||
@@ -1011,17 +1017,14 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
launchDialogForGroupUpdate(node as Group)
|
launchDialogForGroupUpdate(node as Group)
|
||||||
}
|
}
|
||||||
Type.ENTRY -> {
|
Type.ENTRY -> {
|
||||||
mGroupFragment?.mEntryActivityResultLauncher?.let { resultLauncher ->
|
EntryEditActivity.launchToUpdate(
|
||||||
EntryEditActivity.launchToUpdate(
|
this@GroupActivity,
|
||||||
this@GroupActivity,
|
database,
|
||||||
database,
|
(node as Entry).nodeId,
|
||||||
(node as Entry).nodeId,
|
mEntryActivityResultLauncher
|
||||||
resultLauncher
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reloadGroupIfSearch()
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1050,27 +1053,27 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCopyMenuClick(
|
override fun onCopyMenuClick(
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
nodes: List<Node>
|
nodes: List<Node>
|
||||||
): Boolean {
|
): Boolean {
|
||||||
actionNodeMode?.invalidate()
|
actionNodeMode?.invalidate()
|
||||||
|
removeSearch()
|
||||||
// Nothing here fragment calls onPasteMenuClick internally
|
loadGroup()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMoveMenuClick(
|
override fun onMoveMenuClick(
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
nodes: List<Node>
|
nodes: List<Node>
|
||||||
): Boolean {
|
): Boolean {
|
||||||
actionNodeMode?.invalidate()
|
actionNodeMode?.invalidate()
|
||||||
|
removeSearch()
|
||||||
// Nothing here fragment calls onPasteMenuClick internally
|
loadGroup()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPasteMenuClick(
|
override fun onPasteMenuClick(
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
pasteMode: GroupFragment.PasteMode?,
|
pasteMode: GroupFragment.PasteMode?,
|
||||||
nodes: List<Node>
|
nodes: List<Node>
|
||||||
): Boolean {
|
): Boolean {
|
||||||
@@ -1095,24 +1098,27 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDeleteMenuClick(
|
override fun onDeleteMenuClick(
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
nodes: List<Node>
|
nodes: List<Node>
|
||||||
): Boolean {
|
): Boolean {
|
||||||
deleteNodes(nodes)
|
deleteNodes(nodes)
|
||||||
finishNodeAction()
|
finishNodeAction()
|
||||||
reloadGroupIfSearch()
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAskMainCredentialDialogPositiveClick(databaseUri: Uri?,
|
override fun onAskMainCredentialDialogPositiveClick(
|
||||||
mainCredential: MainCredential) {
|
databaseUri: Uri?,
|
||||||
|
mainCredential: MainCredential
|
||||||
|
) {
|
||||||
databaseUri?.let {
|
databaseUri?.let {
|
||||||
mergeDatabaseFrom(it, mainCredential)
|
mergeDatabaseFrom(it, mainCredential)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAskMainCredentialDialogNegativeClick(databaseUri: Uri?,
|
override fun onAskMainCredentialDialogNegativeClick(
|
||||||
mainCredential: MainCredential) { }
|
databaseUri: Uri?,
|
||||||
|
mainCredential: MainCredential
|
||||||
|
) { }
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
@@ -1124,7 +1130,9 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
View.GONE
|
View.GONE
|
||||||
}
|
}
|
||||||
// Padding if lock button visible
|
// Padding if lock button visible
|
||||||
toolbarAction?.updateLockPaddingLeft()
|
toolbarAction?.updateLockPaddingStart()
|
||||||
|
|
||||||
|
loadGroup()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -1137,8 +1145,9 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
private fun addSearchQueryInSearchView(searchQuery: String) {
|
private fun addSearchQueryInSearchView(searchQuery: String) {
|
||||||
searchView?.setOnQueryTextListener(null)
|
searchView?.setOnQueryTextListener(null)
|
||||||
|
if (mAutoSearch)
|
||||||
|
searchView?.clearFocus()
|
||||||
searchView?.setQuery(searchQuery, false)
|
searchView?.setQuery(searchQuery, false)
|
||||||
searchView?.clearFocus()
|
|
||||||
searchView?.setOnQueryTextListener(mOnSearchQueryTextListener)
|
searchView?.setOnQueryTextListener(mOnSearchQueryTextListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1181,10 +1190,10 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
// Get the SearchView and set the searchable configuration
|
// Get the SearchView and set the searchable configuration
|
||||||
menu.findItem(R.id.menu_search)?.let {
|
menu.findItem(R.id.menu_search)?.let {
|
||||||
mLockSearchListeners = true
|
mLockSearchListeners = true
|
||||||
mSearchMenuItem = it
|
|
||||||
it.setOnActionExpandListener(mOnSearchActionExpandListener)
|
it.setOnActionExpandListener(mOnSearchActionExpandListener)
|
||||||
searchView = it.actionView as SearchView?
|
searchView = it.actionView as SearchView?
|
||||||
searchView?.apply {
|
searchView?.apply {
|
||||||
|
setOnQueryTextFocusChangeListener(mOnSearchTextFocusChangeListener)
|
||||||
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager?
|
val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager?
|
||||||
(searchManager?.getSearchableInfo(
|
(searchManager?.getSearchableInfo(
|
||||||
ComponentName(this@GroupActivity, GroupActivity::class.java)
|
ComponentName(this@GroupActivity, GroupActivity::class.java)
|
||||||
@@ -1200,13 +1209,14 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (it.isActionViewExpanded) {
|
if (it.isActionViewExpanded) {
|
||||||
toolbarBreadcrumb?.visibility = View.GONE
|
breadcrumbListView?.visibility = View.GONE
|
||||||
searchFiltersView?.visibility = View.VISIBLE
|
searchFiltersView?.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
searchFiltersView?.visibility = View.GONE
|
searchFiltersView?.visibility = View.GONE
|
||||||
toolbarBreadcrumb?.visibility = View.VISIBLE
|
breadcrumbListView?.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
mLockSearchListeners = false
|
mLockSearchListeners = false
|
||||||
|
mAutoSearch = false
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
@@ -1325,6 +1335,12 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
mGroupFragment?.onSortSelected(sortNodeEnum, sortNodeParameters)
|
mGroupFragment?.onSortSelected(sortNodeEnum, sortNodeParameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCancelSpecialMode() {
|
||||||
|
super.onCancelSpecialMode()
|
||||||
|
removeSearch()
|
||||||
|
loadGroup()
|
||||||
|
}
|
||||||
|
|
||||||
override fun startActivity(intent: Intent) {
|
override fun startActivity(intent: Intent) {
|
||||||
// Get the intent, verify the action and get the query
|
// Get the intent, verify the action and get the query
|
||||||
if (Intent.ACTION_SEARCH == intent.action) {
|
if (Intent.ACTION_SEARCH == intent.action) {
|
||||||
@@ -1341,12 +1357,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun reloadCurrentGroup() {
|
override fun onDatabaseBackPressed() {
|
||||||
removeSearch()
|
|
||||||
loadGroup()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
|
||||||
if (mGroupFragment?.nodeActionSelectionMode == true) {
|
if (mGroupFragment?.nodeActionSelectionMode == true) {
|
||||||
finishNodeAction()
|
finishNodeAction()
|
||||||
} else {
|
} else {
|
||||||
@@ -1354,8 +1365,8 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
if (mRootGroup != null && mRootGroup != mCurrentGroup) {
|
if (mRootGroup != null && mRootGroup != mCurrentGroup) {
|
||||||
when {
|
when {
|
||||||
Intent.ACTION_SEARCH == intent.action -> {
|
Intent.ACTION_SEARCH == intent.action -> {
|
||||||
// Remove the search
|
removeSearch()
|
||||||
reloadCurrentGroup()
|
loadGroup()
|
||||||
}
|
}
|
||||||
mPreviousGroupsIds.isEmpty() -> {
|
mPreviousGroupsIds.isEmpty() -> {
|
||||||
super.onRegularBackPressed()
|
super.onRegularBackPressed()
|
||||||
@@ -1373,7 +1384,6 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||||
if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) {
|
if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) {
|
||||||
lockAndExit()
|
lockAndExit()
|
||||||
super.onRegularBackPressed()
|
|
||||||
} else {
|
} else {
|
||||||
backToTheAppCaller()
|
backToTheAppCaller()
|
||||||
}
|
}
|
||||||
@@ -1387,8 +1397,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
private constructor(parcel: Parcel) : this(
|
private constructor(parcel: Parcel) : this(
|
||||||
parcel.readParcelable<SearchParameters>
|
parcel.readParcelableCompat<SearchParameters>() ?: SearchParameters(),
|
||||||
(SearchParameters::class.java.classLoader) ?: SearchParameters(),
|
|
||||||
parcel.readInt()
|
parcel.readInt()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1416,7 +1425,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
private constructor(parcel: Parcel) : this(
|
private constructor(parcel: Parcel) : this(
|
||||||
parcel.readParcelable<NodeId<*>>(NodeId::class.java.classLoader),
|
parcel.readParcelableCompat<NodeId<*>>(),
|
||||||
parcel.readInt()
|
parcel.readInt()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1481,7 +1490,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launch(context: Context,
|
fun launch(context: Context,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
autoSearch: Boolean = false) {
|
autoSearch: Boolean = false) {
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
checkTimeAndBuildIntent(context, null) { intent ->
|
checkTimeAndBuildIntent(context, null) { intent ->
|
||||||
@@ -1497,7 +1506,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launchForSearchResult(context: Context,
|
fun launchForSearchResult(context: Context,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
searchInfo: SearchInfo,
|
searchInfo: SearchInfo,
|
||||||
autoSearch: Boolean = false) {
|
autoSearch: Boolean = false) {
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
@@ -1518,7 +1527,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launchForSaveResult(context: Context,
|
fun launchForSaveResult(context: Context,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
searchInfo: SearchInfo,
|
searchInfo: SearchInfo,
|
||||||
autoSearch: Boolean = false) {
|
autoSearch: Boolean = false) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
@@ -1539,7 +1548,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launchForKeyboardSelectionResult(context: Context,
|
fun launchForKeyboardSelectionResult(context: Context,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
searchInfo: SearchInfo? = null,
|
searchInfo: SearchInfo? = null,
|
||||||
autoSearch: Boolean = false) {
|
autoSearch: Boolean = false) {
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
@@ -1561,7 +1570,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
*/
|
*/
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
activityResultLaunch: ActivityResultLauncher<Intent>?,
|
activityResultLaunch: ActivityResultLauncher<Intent>?,
|
||||||
autofillComponent: AutofillComponent,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo? = null,
|
searchInfo: SearchInfo? = null,
|
||||||
@@ -1586,7 +1595,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launchForRegistration(context: Context,
|
fun launchForRegistration(context: Context,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
registerInfo: RegisterInfo? = null) {
|
registerInfo: RegisterInfo? = null) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
checkTimeAndBuildIntent(context, null) { intent ->
|
checkTimeAndBuildIntent(context, null) { intent ->
|
||||||
@@ -1606,7 +1615,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launch(activity: AppCompatActivity,
|
fun launch(activity: AppCompatActivity,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
onValidateSpecialMode: () -> Unit,
|
onValidateSpecialMode: () -> Unit,
|
||||||
onCancelSpecialMode: () -> Unit,
|
onCancelSpecialMode: () -> Unit,
|
||||||
onLaunchActivitySpecialMode: () -> Unit,
|
onLaunchActivitySpecialMode: () -> Unit,
|
||||||
|
|||||||
@@ -41,16 +41,24 @@ import com.kunzisoft.keepass.activities.fragments.IconPickerFragment
|
|||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
import com.kunzisoft.keepass.view.updateLockPaddingStart
|
||||||
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
|
||||||
class IconPickerActivity : DatabaseLockActivity() {
|
class IconPickerActivity : DatabaseLockActivity() {
|
||||||
@@ -96,7 +104,7 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
lockAndExit()
|
lockAndExit()
|
||||||
}
|
}
|
||||||
|
|
||||||
intent?.getParcelableExtra<IconImage>(EXTRA_ICON)?.let {
|
intent?.getParcelableExtraCompat<IconImage>(EXTRA_ICON)?.let {
|
||||||
mIconImage = it
|
mIconImage = it
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +120,7 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
), ICON_PICKER_FRAGMENT_TAG)
|
), ICON_PICKER_FRAGMENT_TAG)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mIconImage = savedInstanceState.getParcelable(EXTRA_ICON) ?: mIconImage
|
mIconImage = savedInstanceState.getParcelableCompat(EXTRA_ICON) ?: mIconImage
|
||||||
}
|
}
|
||||||
|
|
||||||
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
|
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
|
||||||
@@ -166,7 +174,7 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
if (database?.allowCustomIcons == true) {
|
if (database?.allowCustomIcons == true) {
|
||||||
@@ -204,7 +212,7 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Padding if lock button visible
|
// Padding if lock button visible
|
||||||
toolbar.updateLockPaddingLeft()
|
toolbar.updateLockPaddingStart()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
@@ -231,7 +239,7 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
if (mCustomIconsSelectionMode) {
|
if (mCustomIconsSelectionMode) {
|
||||||
iconPickerViewModel.deselectAllCustomIcons()
|
iconPickerViewModel.deselectAllCustomIcons()
|
||||||
} else {
|
} else {
|
||||||
onBackPressed()
|
onDatabaseBackPressed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
R.id.menu_edit -> {
|
R.id.menu_edit -> {
|
||||||
@@ -243,7 +251,7 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
R.id.menu_external_icon -> {
|
R.id.menu_external_icon -> {
|
||||||
UriUtil.gotoUrl(this, R.string.external_icon_url)
|
this.openUrl(R.string.external_icon_url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,7 +265,7 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
// on Progress with thread
|
// on Progress with thread
|
||||||
val asyncResult: Deferred<IconPickerViewModel.IconCustomState?> = async {
|
val asyncResult: Deferred<IconPickerViewModel.IconCustomState?> = async {
|
||||||
val iconCustomState = IconPickerViewModel.IconCustomState(null, true, R.string.error_upload_file)
|
val iconCustomState = IconPickerViewModel.IconCustomState(null, true, R.string.error_upload_file)
|
||||||
UriUtil.getFileData(this@IconPickerActivity, iconToUploadUri)?.also { documentFile ->
|
iconToUploadUri?.getDocumentFile(this@IconPickerActivity)?.also { documentFile ->
|
||||||
if (documentFile.length() > MAX_ICON_SIZE) {
|
if (documentFile.length() > MAX_ICON_SIZE) {
|
||||||
iconCustomState.errorStringId = R.string.error_file_to_big
|
iconCustomState.errorStringId = R.string.error_file_to_big
|
||||||
} else {
|
} else {
|
||||||
@@ -321,9 +329,9 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onDatabaseBackPressed() {
|
||||||
setResult()
|
setResult()
|
||||||
super.onBackPressed()
|
super.onDatabaseBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -336,7 +344,7 @@ class IconPickerActivity : DatabaseLockActivity() {
|
|||||||
listener: (icon: IconImage) -> Unit): ActivityResultLauncher<Intent> {
|
listener: (icon: IconImage) -> Unit): ActivityResultLauncher<Intent> {
|
||||||
return context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
return context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
listener.invoke(result.data?.getParcelableExtra(EXTRA_ICON) ?: IconImage())
|
listener.invoke(result.data?.getParcelableExtraCompat(EXTRA_ICON) ?: IconImage())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,9 +33,10 @@ import androidx.appcompat.widget.Toolbar
|
|||||||
import com.igreenwood.loupe.Loupe
|
import com.igreenwood.loupe.Loupe
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
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.tasks.BinaryDatabaseManager
|
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class ImageViewerActivity : DatabaseLockActivity() {
|
class ImageViewerActivity : DatabaseLockActivity() {
|
||||||
@@ -100,12 +101,12 @@ class ImageViewerActivity : DatabaseLockActivity() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
progressView.visibility = View.VISIBLE
|
progressView.visibility = View.VISIBLE
|
||||||
intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
|
intent.getParcelableExtraCompat<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
|
||||||
|
|
||||||
supportActionBar?.title = attachment.name
|
supportActionBar?.title = attachment.name
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import com.kunzisoft.keepass.R
|
|||||||
import com.kunzisoft.keepass.activities.fragments.KeyGeneratorFragment
|
import com.kunzisoft.keepass.activities.fragments.KeyGeneratorFragment
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
import com.kunzisoft.keepass.view.updateLockPaddingStart
|
||||||
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
||||||
|
|
||||||
class KeyGeneratorActivity : DatabaseLockActivity() {
|
class KeyGeneratorActivity : DatabaseLockActivity() {
|
||||||
@@ -84,7 +84,7 @@ class KeyGeneratorActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Padding if lock button visible
|
// Padding if lock button visible
|
||||||
toolbar.updateLockPaddingLeft()
|
toolbar.updateLockPaddingStart()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
@@ -96,7 +96,7 @@ class KeyGeneratorActivity : DatabaseLockActivity() {
|
|||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
android.R.id.home -> {
|
android.R.id.home -> {
|
||||||
onBackPressed()
|
onDatabaseBackPressed()
|
||||||
}
|
}
|
||||||
R.id.menu_generate -> {
|
R.id.menu_generate -> {
|
||||||
keyGeneratorViewModel.requireKeyGeneration()
|
keyGeneratorViewModel.requireKeyGeneration()
|
||||||
@@ -106,9 +106,9 @@ class KeyGeneratorActivity : DatabaseLockActivity() {
|
|||||||
return super.onOptionsItemSelected(item)
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onDatabaseBackPressed() {
|
||||||
setResult(Activity.RESULT_CANCELED, Intent())
|
setResult(Activity.RESULT_CANCELED, Intent())
|
||||||
super.onBackPressed()
|
super.onDatabaseBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePassDX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
@@ -32,7 +32,6 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.CompoundButton
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
@@ -43,55 +42,72 @@ import androidx.appcompat.widget.Toolbar
|
|||||||
import androidx.biometric.BiometricManager
|
import androidx.biometric.BiometricManager
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity.Companion.UI_VISIBLE_DURING_LOCK
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
|
import com.kunzisoft.keepass.biometric.DeviceUnlockFragment
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.biometric.deviceUnlockError
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||||
import com.kunzisoft.keepass.model.*
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
|
import com.kunzisoft.keepass.model.CipherDecryptDatabase
|
||||||
|
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||||
|
import com.kunzisoft.keepass.model.CredentialStorage
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
||||||
|
import com.kunzisoft.keepass.settings.AdvancedUnlockSettingsActivity
|
||||||
|
import com.kunzisoft.keepass.settings.AppearanceSettingsActivity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity
|
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil.getUri
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
import com.kunzisoft.keepass.view.MainCredentialView
|
import com.kunzisoft.keepass.view.MainCredentialView
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DeviceUnlockState
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DeviceUnlockViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
|
|
||||||
class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener {
|
class MainCredentialActivity : DatabaseModeActivity() {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
private var filenameView: TextView? = null
|
private var filenameView: TextView? = null
|
||||||
|
private var logotypeButton: View? = null
|
||||||
private var advancedUnlockButton: View? = null
|
private var advancedUnlockButton: View? = null
|
||||||
private var mainCredentialView: MainCredentialView? = null
|
private var mainCredentialView: MainCredentialView? = null
|
||||||
private var confirmButtonView: Button? = null
|
private var confirmButtonView: Button? = null
|
||||||
private var infoContainerView: ViewGroup? = null
|
private var infoContainerView: ViewGroup? = null
|
||||||
private lateinit var coordinatorLayout: CoordinatorLayout
|
private lateinit var coordinatorLayout: CoordinatorLayout
|
||||||
private var advancedUnlockFragment: AdvancedUnlockFragment? = null
|
private var deviceUnlockFragment: DeviceUnlockFragment? = null
|
||||||
|
|
||||||
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
|
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||||
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by viewModels()
|
private val mDeviceUnlockViewModel: DeviceUnlockViewModel by viewModels()
|
||||||
|
|
||||||
private val mPasswordActivityEducation = PasswordActivityEducation(this)
|
private val mPasswordActivityEducation = PasswordActivityEducation(this)
|
||||||
|
|
||||||
@@ -101,6 +117,8 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
private var mRememberKeyFile: Boolean = false
|
private var mRememberKeyFile: Boolean = false
|
||||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
|
private var mRememberHardwareKey: Boolean = false
|
||||||
|
|
||||||
private var mReadOnly: Boolean = false
|
private var mReadOnly: Boolean = false
|
||||||
private var mForceReadOnly: Boolean = false
|
private var mForceReadOnly: Boolean = false
|
||||||
|
|
||||||
@@ -121,7 +139,8 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||||
|
|
||||||
filenameView = findViewById(R.id.filename)
|
filenameView = findViewById(R.id.filename)
|
||||||
advancedUnlockButton = findViewById(R.id.activity_password_advanced_unlock_button)
|
logotypeButton = findViewById(R.id.activity_password_logotype)
|
||||||
|
advancedUnlockButton = findViewById(R.id.fragment_advanced_unlock_container_view)
|
||||||
mainCredentialView = findViewById(R.id.activity_password_credentials)
|
mainCredentialView = findViewById(R.id.activity_password_credentials)
|
||||||
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
||||||
infoContainerView = findViewById(R.id.activity_password_info_container)
|
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||||
@@ -133,11 +152,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
PreferencesUtil.enableReadOnlyDatabase(this)
|
PreferencesUtil.enableReadOnlyDatabase(this)
|
||||||
}
|
}
|
||||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||||
|
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this)
|
||||||
|
|
||||||
mExternalFileHelper = ExternalFileHelper(this@MainCredentialActivity)
|
// Build elements to manage keyfile selection
|
||||||
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
mainCredentialView?.populateKeyFileTextView(uri)
|
mainCredentialView?.populateKeyFileView(uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
|
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
|
||||||
@@ -148,29 +169,20 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
// If is a view intent
|
// If is a view intent
|
||||||
getUriFromIntent(intent)
|
getUriFromIntent(intent)
|
||||||
|
|
||||||
// Init Biometric elements
|
// Show appearance
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
logotypeButton?.setOnClickListener {
|
||||||
advancedUnlockButton?.setOnClickListener {
|
startActivity(Intent(this, AppearanceSettingsActivity::class.java))
|
||||||
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
|
// Listen password checkbox to init advanced unlock and confirmation button
|
||||||
mainCredentialView?.onPasswordChecked =
|
mainCredentialView?.onConditionToStoreCredentialChanged = { _, verified ->
|
||||||
CompoundButton.OnCheckedChangeListener { _, _ ->
|
mDeviceUnlockViewModel.checkConditionToStoreCredential(
|
||||||
mAdvancedUnlockViewModel.checkUnlockAvailability()
|
condition = verified,
|
||||||
enableConfirmationButton()
|
databaseFileUri = mDatabaseFileUri
|
||||||
}
|
)
|
||||||
|
// TODO Async by ViewModel
|
||||||
|
enableConfirmationButton()
|
||||||
|
}
|
||||||
|
|
||||||
// Observe if default database
|
// Observe if default database
|
||||||
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
||||||
@@ -204,28 +216,83 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
databaseKeyFileUri
|
databaseKeyFileUri
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val databaseHardwareKey = mainCredentialView?.getMainCredential()?.hardwareKey
|
||||||
|
val hardwareKey =
|
||||||
|
if (mRememberHardwareKey
|
||||||
|
&& databaseHardwareKey == null) {
|
||||||
|
databaseFile?.hardwareKey
|
||||||
|
} else {
|
||||||
|
databaseHardwareKey
|
||||||
|
}
|
||||||
|
|
||||||
// Define title
|
// Define title
|
||||||
filenameView?.text = databaseFile?.databaseAlias ?: ""
|
filenameView?.text = databaseFile?.databaseAlias ?: ""
|
||||||
|
|
||||||
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
|
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri, hardwareKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
mDeviceUnlockViewModel.uiState.collect { uiState ->
|
||||||
|
// New value received
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
uiState.credentialRequiredCipher?.let { cipher ->
|
||||||
|
mDeviceUnlockViewModel.encryptCredential(
|
||||||
|
credential = getCredentialForEncryption(),
|
||||||
|
cipher = cipher
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uiState.cipherEncryptDatabase?.let { cipherEncryptDatabase ->
|
||||||
|
onCredentialEncrypted(cipherEncryptDatabase)
|
||||||
|
mDeviceUnlockViewModel.consumeCredentialEncrypted()
|
||||||
|
}
|
||||||
|
uiState.cipherDecryptDatabase?.let { cipherDecryptDatabase ->
|
||||||
|
onCredentialDecrypted(cipherDecryptDatabase)
|
||||||
|
mDeviceUnlockViewModel.consumeCredentialDecrypted()
|
||||||
|
}
|
||||||
|
uiState.exception?.let { error ->
|
||||||
|
Snackbar.make(
|
||||||
|
coordinatorLayout,
|
||||||
|
deviceUnlockError(error, this@MainCredentialActivity),
|
||||||
|
Snackbar.LENGTH_LONG
|
||||||
|
).asError().show()
|
||||||
|
mDeviceUnlockViewModel.exceptionShown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
|
// Init Biometric elements only if allowed
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||||
|
&& PreferencesUtil.isAdvancedUnlockEnable(this)) {
|
||||||
|
deviceUnlockFragment = supportFragmentManager
|
||||||
|
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? DeviceUnlockFragment?
|
||||||
|
if (deviceUnlockFragment == null) {
|
||||||
|
deviceUnlockFragment = DeviceUnlockFragment().also {
|
||||||
|
supportFragmentManager.commit {
|
||||||
|
replace(
|
||||||
|
R.id.fragment_advanced_unlock_container_view,
|
||||||
|
it,
|
||||||
|
UNLOCK_FRAGMENT_TAG
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity)
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity)
|
||||||
|
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this@MainCredentialActivity)
|
||||||
|
|
||||||
// Back to previous keyboard is setting activated
|
// Back to previous keyboard is setting activated
|
||||||
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@MainCredentialActivity)) {
|
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@MainCredentialActivity)) {
|
||||||
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't allow auto open prompt if lock become when UI visible
|
|
||||||
if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true) {
|
|
||||||
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
|
|
||||||
}
|
|
||||||
|
|
||||||
mDatabaseFileUri?.let { databaseFileUri ->
|
mDatabaseFileUri?.let { databaseFileUri ->
|
||||||
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
mDatabaseFileViewModel.loadDatabaseFile(databaseFileUri)
|
||||||
}
|
}
|
||||||
@@ -235,7 +302,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
if (database != null) {
|
if (database != null) {
|
||||||
// Trying to load another database
|
// Trying to load another database
|
||||||
@@ -252,113 +319,105 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result
|
result: ActionRunnable.Result
|
||||||
) {
|
) {
|
||||||
super.onDatabaseActionFinished(database, actionTask, result)
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
// Recheck advanced unlock if error
|
|
||||||
mAdvancedUnlockViewModel.initAdvancedUnlockMode()
|
|
||||||
|
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
launchGroupActivityIfLoaded(database)
|
launchGroupActivityIfLoaded(database)
|
||||||
} else {
|
} else {
|
||||||
mainCredentialView?.requestPasswordFocus()
|
mainCredentialView?.requestPasswordFocus()
|
||||||
|
// Manage special exceptions
|
||||||
|
when (result.exception) {
|
||||||
|
is DuplicateUuidDatabaseException -> {
|
||||||
|
// Relaunch loading if we need to fix UUID
|
||||||
|
showLoadDatabaseDuplicateUuidMessage {
|
||||||
|
|
||||||
var resultError = ""
|
var databaseUri: Uri? = null
|
||||||
val resultException = result.exception
|
var mainCredential = MainCredential()
|
||||||
val resultMessage = result.message
|
var readOnly = true
|
||||||
|
var cipherEncryptDatabase: CipherEncryptDatabase? = null
|
||||||
|
|
||||||
if (resultException != null) {
|
result.data?.let { resultData ->
|
||||||
resultError = resultException.getLocalizedMessage(resources)
|
databaseUri = resultData.getParcelableCompat(DATABASE_URI_KEY)
|
||||||
|
mainCredential =
|
||||||
when (resultException) {
|
resultData.getParcelableCompat(MAIN_CREDENTIAL_KEY)
|
||||||
is DuplicateUuidDatabaseException -> {
|
?: mainCredential
|
||||||
// Relaunch loading if we need to fix UUID
|
readOnly = resultData.getBoolean(READ_ONLY_KEY)
|
||||||
showLoadDatabaseDuplicateUuidMessage {
|
cipherEncryptDatabase =
|
||||||
|
resultData.getParcelableCompat(CIPHER_DATABASE_KEY)
|
||||||
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 -> {
|
databaseUri?.let { databaseFileUri ->
|
||||||
// Remove this default database inaccessible
|
showProgressDialogAndLoadDatabase(
|
||||||
if (mDefaultDatabase) {
|
databaseFileUri,
|
||||||
mDatabaseFileViewModel.removeDefaultDatabase()
|
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
coordinatorLayout.showActionErrorIfNeeded(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getUriFromIntent(intent: Intent?) {
|
private fun getUriFromIntent(intent: Intent?) {
|
||||||
// If is a view intent
|
// If is a view intent
|
||||||
val action = intent?.action
|
val action = intent?.action
|
||||||
if (action != null
|
if (action == VIEW_INTENT) {
|
||||||
&& action == VIEW_INTENT) {
|
fillCredentials(
|
||||||
mDatabaseFileUri = intent.data
|
intent.data,
|
||||||
mainCredentialView?.populateKeyFileTextView(UriUtil.getUriFromIntent(intent, KEY_KEYFILE))
|
intent.getUri(KEY_KEYFILE),
|
||||||
|
HardwareKey.getHardwareKeyFromString(intent.getStringExtra(KEY_HARDWARE_KEY))
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
|
fillCredentials(
|
||||||
intent?.getParcelableExtra<Uri?>(KEY_KEYFILE)?.let {
|
intent?.getParcelableExtraCompat(KEY_FILENAME),
|
||||||
mainCredentialView?.populateKeyFileTextView(it)
|
intent?.getParcelableExtraCompat(KEY_KEYFILE),
|
||||||
}
|
HardwareKey.getHardwareKeyFromString(intent?.getStringExtra(KEY_HARDWARE_KEY))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
intent?.removeExtra(KEY_KEYFILE)
|
intent?.removeExtra(KEY_KEYFILE)
|
||||||
} catch (e: Exception) {}
|
intent?.removeExtra(KEY_HARDWARE_KEY)
|
||||||
|
} catch (_: Exception) {}
|
||||||
mDatabaseFileUri?.let {
|
mDatabaseFileUri?.let {
|
||||||
mDatabaseFileViewModel.checkIfIsDefaultDatabase(it)
|
mDatabaseFileViewModel.checkIfIsDefaultDatabase(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun fillCredentials(databaseUri: Uri?,
|
||||||
|
keyFileUri: Uri?,
|
||||||
|
hardwareKey: HardwareKey?) {
|
||||||
|
mDatabaseFileUri = databaseUri
|
||||||
|
mainCredentialView?.populateKeyFileView(keyFileUri)
|
||||||
|
mainCredentialView?.populateHardwareKeyView(hardwareKey)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
getUriFromIntent(intent)
|
getUriFromIntent(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchGroupActivityIfLoaded(database: Database) {
|
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
||||||
// Check if database really loaded
|
// Check if database really loaded
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
clearCredentialsViews(true)
|
mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt = true
|
||||||
|
clearCredentialsViews(clearKeyFile = true, clearHardwareKey = true)
|
||||||
GroupActivity.launch(this,
|
GroupActivity.launch(this,
|
||||||
database,
|
database,
|
||||||
{ onValidateSpecialMode() },
|
{ onValidateSpecialMode() },
|
||||||
@@ -369,33 +428,6 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
private val credentialStorageListener = object: MainCredentialView.CredentialStorageListener {
|
||||||
override fun passwordToStore(password: String?): ByteArray? {
|
override fun passwordToStore(password: String?): ByteArray? {
|
||||||
return password?.toByteArray()
|
return password?.toByteArray()
|
||||||
@@ -412,13 +444,26 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
|
private fun getCredentialForEncryption(): ByteArray {
|
||||||
|
return mainCredentialView?.retrieveCredentialForStorage(credentialStorageListener)
|
||||||
|
?: byteArrayOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) {
|
||||||
|
// Load the database if password is registered with biometric
|
||||||
|
loadDatabase(mDatabaseFileUri,
|
||||||
|
mainCredentialView?.getMainCredential(),
|
||||||
|
cipherEncryptDatabase
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
|
||||||
// Load the database if password is retrieve from biometric
|
// Load the database if password is retrieve from biometric
|
||||||
// Retrieve from biometric
|
// Retrieve from biometric
|
||||||
val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
|
val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
|
||||||
when (cipherDecryptDatabase.credentialStorage) {
|
when (cipherDecryptDatabase.credentialStorage) {
|
||||||
CredentialStorage.PASSWORD -> {
|
CredentialStorage.PASSWORD -> {
|
||||||
mainCredential.masterPassword = String(cipherDecryptDatabase.decryptedValue)
|
mainCredential.password = String(cipherDecryptDatabase.decryptedValue)
|
||||||
}
|
}
|
||||||
CredentialStorage.KEY_FILE -> {
|
CredentialStorage.KEY_FILE -> {
|
||||||
// TODO advanced unlock key file
|
// TODO advanced unlock key file
|
||||||
@@ -433,14 +478,23 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) {
|
private fun onDatabaseFileLoaded(databaseFileUri: Uri?,
|
||||||
|
keyFileUri: Uri?,
|
||||||
|
hardwareKey: HardwareKey?) {
|
||||||
// Define Key File text
|
// Define Key File text
|
||||||
if (mRememberKeyFile) {
|
if (mRememberKeyFile) {
|
||||||
mainCredentialView?.populateKeyFileTextView(keyFileUri)
|
mainCredentialView?.populateKeyFileView(keyFileUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define hardware key
|
||||||
|
if (mRememberHardwareKey) {
|
||||||
|
mainCredentialView?.populateHardwareKeyView(hardwareKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define listener for validate button
|
// Define listener for validate button
|
||||||
confirmButtonView?.setOnClickListener { loadDatabase() }
|
confirmButtonView?.setOnClickListener {
|
||||||
|
mainCredentialView?.validateCredential()
|
||||||
|
}
|
||||||
|
|
||||||
// If Activity is launch with a password and want to open directly
|
// If Activity is launch with a password and want to open directly
|
||||||
val intent = intent
|
val intent = intent
|
||||||
@@ -455,7 +509,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
loadDatabase()
|
loadDatabase()
|
||||||
} else {
|
} else {
|
||||||
// Init Biometric elements
|
// Init Biometric elements
|
||||||
mAdvancedUnlockViewModel.databaseFileLoaded(databaseFileUri)
|
mDeviceUnlockViewModel.databaseFileLoaded(databaseFileUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
enableConfirmationButton()
|
enableConfirmationButton()
|
||||||
@@ -472,17 +526,20 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
|
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile,
|
||||||
|
clearHardwareKey: Boolean = !mRememberHardwareKey) {
|
||||||
mainCredentialView?.populatePasswordTextView(null)
|
mainCredentialView?.populatePasswordTextView(null)
|
||||||
if (clearKeyFile) {
|
if (clearKeyFile) {
|
||||||
mainCredentialView?.populateKeyFileTextView(null)
|
mainCredentialView?.populateKeyFileView(null)
|
||||||
|
}
|
||||||
|
if (clearHardwareKey) {
|
||||||
|
mainCredentialView?.populateHardwareKeyView(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
// Reinit locking activity UI variable
|
// Reinit locking activity UI variable
|
||||||
DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null
|
UI_VISIBLE_DURING_LOCK = false
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,7 +668,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
try {
|
try {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||||
&& !readOnlyEducationPerformed) {
|
&& !readOnlyEducationPerformed) {
|
||||||
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(this)
|
val biometricCanAuthenticate = DeviceUnlockManager.canAuthenticate(this)
|
||||||
if ((biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
if ((biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
||||||
&& advancedUnlockButton != null) {
|
&& advancedUnlockButton != null) {
|
||||||
@@ -621,7 +678,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
startActivity(
|
startActivity(
|
||||||
Intent(
|
Intent(
|
||||||
this,
|
this,
|
||||||
SettingsAdvancedUnlockActivity::class.java
|
AdvancedUnlockSettingsActivity::class.java
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -666,18 +723,24 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
|
|
||||||
private const val KEY_FILENAME = "fileName"
|
private const val KEY_FILENAME = "fileName"
|
||||||
private const val KEY_KEYFILE = "keyFile"
|
private const val KEY_KEYFILE = "keyFile"
|
||||||
|
private const val KEY_HARDWARE_KEY = "hardwareKey"
|
||||||
private const val VIEW_INTENT = "android.intent.action.VIEW"
|
private const val VIEW_INTENT = "android.intent.action.VIEW"
|
||||||
|
|
||||||
private const val KEY_READ_ONLY = "KEY_READ_ONLY"
|
private const val KEY_READ_ONLY = "KEY_READ_ONLY"
|
||||||
private const val KEY_PASSWORD = "password"
|
private const val KEY_PASSWORD = "password"
|
||||||
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
|
||||||
|
|
||||||
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?,
|
private fun buildAndLaunchIntent(activity: Activity,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
hardwareKey: HardwareKey?,
|
||||||
intentBuildLauncher: (Intent) -> Unit) {
|
intentBuildLauncher: (Intent) -> Unit) {
|
||||||
val intent = Intent(activity, MainCredentialActivity::class.java)
|
val intent = Intent(activity, MainCredentialActivity::class.java)
|
||||||
intent.putExtra(KEY_FILENAME, databaseFile)
|
intent.putExtra(KEY_FILENAME, databaseFile)
|
||||||
if (keyFile != null)
|
if (keyFile != null)
|
||||||
intent.putExtra(KEY_KEYFILE, keyFile)
|
intent.putExtra(KEY_KEYFILE, keyFile)
|
||||||
|
if (hardwareKey != null)
|
||||||
|
intent.putExtra(KEY_HARDWARE_KEY, hardwareKey.toString())
|
||||||
intentBuildLauncher.invoke(intent)
|
intentBuildLauncher.invoke(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,8 +753,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
fun launch(activity: Activity,
|
fun launch(activity: Activity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?) {
|
keyFile: Uri?,
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
hardwareKey: HardwareKey?) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
activity.startActivity(intent)
|
activity.startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -706,8 +770,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
fun launchForSearchResult(activity: Activity,
|
fun launchForSearchResult(activity: Activity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
|
hardwareKey: HardwareKey?,
|
||||||
searchInfo: SearchInfo) {
|
searchInfo: SearchInfo) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
EntrySelectionHelper.startActivityForSearchModeResult(
|
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
@@ -725,8 +790,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
fun launchForSaveResult(activity: Activity,
|
fun launchForSaveResult(activity: Activity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
|
hardwareKey: HardwareKey?,
|
||||||
searchInfo: SearchInfo) {
|
searchInfo: SearchInfo) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
EntrySelectionHelper.startActivityForSaveModeResult(
|
EntrySelectionHelper.startActivityForSaveModeResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
@@ -744,8 +810,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
fun launchForKeyboardResult(activity: Activity,
|
fun launchForKeyboardResult(activity: Activity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
|
hardwareKey: HardwareKey?,
|
||||||
searchInfo: SearchInfo?) {
|
searchInfo: SearchInfo?) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
|
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
@@ -764,10 +831,11 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
|
hardwareKey: HardwareKey?,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
autofillComponent: AutofillComponent,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo?) {
|
searchInfo: SearchInfo?) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
AutofillHelper.startActivityForAutofillResult(
|
AutofillHelper.startActivityForAutofillResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
@@ -785,8 +853,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
fun launchForRegistration(activity: Activity,
|
fun launchForRegistration(activity: Activity,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
|
hardwareKey: HardwareKey?,
|
||||||
registerInfo: RegisterInfo?) {
|
registerInfo: RegisterInfo?) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
@@ -802,6 +871,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
fun launch(activity: AppCompatActivity,
|
fun launch(activity: AppCompatActivity,
|
||||||
databaseUri: Uri,
|
databaseUri: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
|
hardwareKey: HardwareKey?,
|
||||||
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
||||||
onCancelSpecialMode: () -> Unit,
|
onCancelSpecialMode: () -> Unit,
|
||||||
onLaunchActivitySpecialMode: () -> Unit,
|
onLaunchActivitySpecialMode: () -> Unit,
|
||||||
@@ -810,43 +880,67 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
try {
|
try {
|
||||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||||
{
|
{
|
||||||
MainCredentialActivity.launch(activity,
|
launch(
|
||||||
databaseUri, keyFile)
|
activity,
|
||||||
|
databaseUri,
|
||||||
|
keyFile,
|
||||||
|
hardwareKey
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{ searchInfo -> // Search Action
|
{ searchInfo -> // Search Action
|
||||||
MainCredentialActivity.launchForSearchResult(activity,
|
launchForSearchResult(
|
||||||
databaseUri, keyFile,
|
activity,
|
||||||
searchInfo)
|
databaseUri,
|
||||||
|
keyFile,
|
||||||
|
hardwareKey,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{ searchInfo -> // Save Action
|
{ searchInfo -> // Save Action
|
||||||
MainCredentialActivity.launchForSaveResult(activity,
|
launchForSaveResult(
|
||||||
databaseUri, keyFile,
|
activity,
|
||||||
searchInfo)
|
databaseUri,
|
||||||
|
keyFile,
|
||||||
|
hardwareKey,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{ searchInfo -> // Keyboard Selection Action
|
{ searchInfo -> // Keyboard Selection Action
|
||||||
MainCredentialActivity.launchForKeyboardResult(activity,
|
launchForKeyboardResult(
|
||||||
databaseUri, keyFile,
|
activity,
|
||||||
searchInfo)
|
databaseUri,
|
||||||
|
keyFile,
|
||||||
|
hardwareKey,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{ searchInfo, autofillComponent -> // Autofill Selection Action
|
{ searchInfo, autofillComponent -> // Autofill Selection Action
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
MainCredentialActivity.launchForAutofillResult(activity,
|
launchForAutofillResult(
|
||||||
databaseUri, keyFile,
|
activity,
|
||||||
autofillActivityResultLauncher,
|
databaseUri,
|
||||||
autofillComponent,
|
keyFile,
|
||||||
searchInfo)
|
hardwareKey,
|
||||||
|
autofillActivityResultLauncher,
|
||||||
|
autofillComponent,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
} else {
|
} else {
|
||||||
onCancelSpecialMode()
|
onCancelSpecialMode()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ registerInfo -> // Registration Action
|
{ registerInfo -> // Registration Action
|
||||||
MainCredentialActivity.launchForRegistration(activity,
|
launchForRegistration(
|
||||||
databaseUri, keyFile,
|
activity,
|
||||||
registerInfo)
|
databaseUri,
|
||||||
|
keyFile,
|
||||||
|
hardwareKey,
|
||||||
|
registerInfo
|
||||||
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import android.text.SpannableStringBuilder
|
|||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
|
||||||
|
|
||||||
class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
||||||
@@ -40,8 +41,9 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
|||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
|
|
||||||
val oldSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelable(OLD_FILE_DATABASE_INFO)
|
val oldSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelableCompat(OLD_FILE_DATABASE_INFO)
|
||||||
val newSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelable(NEW_FILE_DATABASE_INFO)
|
val newSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelableCompat(NEW_FILE_DATABASE_INFO)
|
||||||
|
val readOnlyDatabase: Boolean = arguments?.getBoolean(READ_ONLY_DATABASE) ?: true
|
||||||
|
|
||||||
if (oldSnapFileDatabaseInfo != null && newSnapFileDatabaseInfo != null) {
|
if (oldSnapFileDatabaseInfo != null && newSnapFileDatabaseInfo != null) {
|
||||||
// Use the Builder class for convenient dialog construction
|
// Use the Builder class for convenient dialog construction
|
||||||
@@ -53,7 +55,13 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
|||||||
stringBuilder.append("\n\n" +oldSnapFileDatabaseInfo.toString(activity)
|
stringBuilder.append("\n\n" +oldSnapFileDatabaseInfo.toString(activity)
|
||||||
+ "\n→\n" +
|
+ "\n→\n" +
|
||||||
newSnapFileDatabaseInfo.toString(activity) + "\n\n")
|
newSnapFileDatabaseInfo.toString(activity) + "\n\n")
|
||||||
stringBuilder.append(getString(R.string.warning_database_info_changed_options))
|
stringBuilder.append(getString(
|
||||||
|
if (readOnlyDatabase) {
|
||||||
|
R.string.warning_database_info_changed_options_read_only
|
||||||
|
} else {
|
||||||
|
R.string.warning_database_info_changed_options
|
||||||
|
}
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
stringBuilder.append(getString(R.string.warning_database_revoked))
|
stringBuilder.append(getString(R.string.warning_database_revoked))
|
||||||
}
|
}
|
||||||
@@ -76,14 +84,18 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
|||||||
const val DATABASE_CHANGED_DIALOG_TAG = "databaseChangedDialogFragment"
|
const val DATABASE_CHANGED_DIALOG_TAG = "databaseChangedDialogFragment"
|
||||||
private const val OLD_FILE_DATABASE_INFO = "OLD_FILE_DATABASE_INFO"
|
private const val OLD_FILE_DATABASE_INFO = "OLD_FILE_DATABASE_INFO"
|
||||||
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
|
private const val NEW_FILE_DATABASE_INFO = "NEW_FILE_DATABASE_INFO"
|
||||||
|
private const val READ_ONLY_DATABASE = "READ_ONLY_DATABASE"
|
||||||
|
|
||||||
fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
fun getInstance(oldSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
newSnapFileDatabaseInfo: SnapFileDatabaseInfo)
|
newSnapFileDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
readOnly: Boolean
|
||||||
|
)
|
||||||
: DatabaseChangedDialogFragment {
|
: DatabaseChangedDialogFragment {
|
||||||
val fragment = DatabaseChangedDialogFragment()
|
val fragment = DatabaseChangedDialogFragment()
|
||||||
fragment.arguments = Bundle().apply {
|
fragment.arguments = Bundle().apply {
|
||||||
putParcelable(OLD_FILE_DATABASE_INFO, oldSnapFileDatabaseInfo)
|
putParcelable(OLD_FILE_DATABASE_INFO, oldSnapFileDatabaseInfo)
|
||||||
putParcelable(NEW_FILE_DATABASE_INFO, newSnapFileDatabaseInfo)
|
putParcelable(NEW_FILE_DATABASE_INFO, newSnapFileDatabaseInfo)
|
||||||
|
putBoolean(READ_ONLY_DATABASE, readOnly)
|
||||||
}
|
}
|
||||||
return fragment
|
return fragment
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package com.kunzisoft.keepass.activities.dialogs
|
package com.kunzisoft.keepass.activities.dialogs
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager.LayoutParams.FLAG_SECURE
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||||
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
@@ -13,7 +16,7 @@ import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
|||||||
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
||||||
|
|
||||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||||
private var mDatabase: Database? = null
|
private var mDatabase: ContextualDatabase? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -29,6 +32,18 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
// Screenshot mode or hide views
|
||||||
|
context?.let {
|
||||||
|
if (PreferencesUtil.isScreenshotModeEnabled(it)) {
|
||||||
|
dialog?.window?.clearFlags(FLAG_SECURE)
|
||||||
|
} else {
|
||||||
|
dialog?.window?.setFlags(FLAG_SECURE, FLAG_SECURE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
@@ -36,12 +51,12 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
|||||||
resetAppTimeoutOnTouchOrFocus()
|
resetAppTimeoutOnTouchOrFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
// Can be overridden by a subclass
|
// Can be overridden by a subclass
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result
|
result: ActionRunnable.Result
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.activities.dialogs
|
|
||||||
|
|
||||||
import android.app.DatePickerDialog
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
|
|
||||||
// Not as DatabaseDialogFragment because crash on KitKat
|
|
||||||
class DatePickerFragment : DialogFragment() {
|
|
||||||
|
|
||||||
private var mDefaultYear: Int = 2000
|
|
||||||
private var mDefaultMonth: Int = 1
|
|
||||||
private var mDefaultDay: Int = 1
|
|
||||||
|
|
||||||
private var mListener: DatePickerDialog.OnDateSetListener? = null
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
try {
|
|
||||||
mListener = context as DatePickerDialog.OnDateSetListener
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
throw ClassCastException(context.toString()
|
|
||||||
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetach() {
|
|
||||||
mListener = null
|
|
||||||
super.onDetach()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
// Create a new instance of DatePickerDialog and return it
|
|
||||||
return context?.let {
|
|
||||||
arguments?.apply {
|
|
||||||
if (containsKey(DEFAULT_YEAR_BUNDLE_KEY))
|
|
||||||
mDefaultYear = getInt(DEFAULT_YEAR_BUNDLE_KEY)
|
|
||||||
if (containsKey(DEFAULT_MONTH_BUNDLE_KEY))
|
|
||||||
mDefaultMonth = getInt(DEFAULT_MONTH_BUNDLE_KEY)
|
|
||||||
if (containsKey(DEFAULT_DAY_BUNDLE_KEY))
|
|
||||||
mDefaultDay = getInt(DEFAULT_DAY_BUNDLE_KEY)
|
|
||||||
}
|
|
||||||
|
|
||||||
DatePickerDialog(it, mListener, mDefaultYear, mDefaultMonth, mDefaultDay)
|
|
||||||
} ?: super.onCreateDialog(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val DEFAULT_YEAR_BUNDLE_KEY = "DEFAULT_YEAR_BUNDLE_KEY"
|
|
||||||
private const val DEFAULT_MONTH_BUNDLE_KEY = "DEFAULT_MONTH_BUNDLE_KEY"
|
|
||||||
private const val DEFAULT_DAY_BUNDLE_KEY = "DEFAULT_DAY_BUNDLE_KEY"
|
|
||||||
|
|
||||||
fun getInstance(defaultYear: Int,
|
|
||||||
defaultMonth: Int,
|
|
||||||
defaultDay: Int): DatePickerFragment {
|
|
||||||
return DatePickerFragment().apply {
|
|
||||||
arguments = Bundle().apply {
|
|
||||||
putInt(DEFAULT_YEAR_BUNDLE_KEY, defaultYear)
|
|
||||||
putInt(DEFAULT_MONTH_BUNDLE_KEY, defaultMonth)
|
|
||||||
putInt(DEFAULT_DAY_BUNDLE_KEY, defaultDay)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -35,6 +35,7 @@ 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.Field
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
|
||||||
|
|
||||||
class EntryCustomFieldDialogFragment: DatabaseDialogFragment() {
|
class EntryCustomFieldDialogFragment: DatabaseDialogFragment() {
|
||||||
@@ -72,7 +73,7 @@ class EntryCustomFieldDialogFragment: DatabaseDialogFragment() {
|
|||||||
customFieldDeleteButton = root?.findViewById(R.id.entry_custom_field_delete)
|
customFieldDeleteButton = root?.findViewById(R.id.entry_custom_field_delete)
|
||||||
customFieldProtectionButton = root?.findViewById(R.id.entry_custom_field_protection)
|
customFieldProtectionButton = root?.findViewById(R.id.entry_custom_field_protection)
|
||||||
|
|
||||||
oldField = arguments?.getParcelable(KEY_FIELD)
|
oldField = arguments?.getParcelableCompat(KEY_FIELD)
|
||||||
oldField?.let { oldCustomField ->
|
oldField?.let { oldCustomField ->
|
||||||
customFieldLabel?.text = oldCustomField.name
|
customFieldLabel?.text = oldCustomField.name
|
||||||
customFieldProtectionButton?.isChecked = oldCustomField.protectedValue.isProtected
|
customFieldProtectionButton?.isChecked = oldCustomField.protectedValue.isProtected
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
|
|
||||||
class FileManagerDialogFragment : DialogFragment() {
|
class FileManagerDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ class FileManagerDialogFragment : DialogFragment() {
|
|||||||
textDescription.text = getString(R.string.file_manager_install_description)
|
textDescription.text = getString(R.string.file_manager_install_description)
|
||||||
|
|
||||||
root.findViewById<Button>(R.id.file_manager_button).setOnClickListener {
|
root.findViewById<Button>(R.id.file_manager_button).setOnClickListener {
|
||||||
UriUtil.gotoUrl(requireContext(), R.string.file_manager_explanation_url)
|
context?.openUrl(R.string.file_manager_explanation_url)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import android.text.SpannableStringBuilder
|
|||||||
import androidx.appcompat.app.AlertDialog
|
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.utils.getParcelableCompat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Dialog to confirm big file to upload
|
* Custom Dialog to confirm big file to upload
|
||||||
@@ -62,7 +63,7 @@ class FileTooBigDialogFragment : DialogFragment() {
|
|||||||
})
|
})
|
||||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
mActionChooseListener?.onValidateUploadFileTooBig(
|
mActionChooseListener?.onValidateUploadFileTooBig(
|
||||||
arguments?.getParcelable(KEY_FILE_URI),
|
arguments?.getParcelableCompat(KEY_FILE_URI),
|
||||||
arguments?.getString(KEY_FILE_NAME))
|
arguments?.getString(KEY_FILE_NAME))
|
||||||
}
|
}
|
||||||
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
|||||||
@@ -31,11 +31,13 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.adapters.TagsAdapter
|
import com.kunzisoft.keepass.adapters.TagsAdapter
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.model.GroupInfo
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
||||||
import com.kunzisoft.keepass.utils.UuidUtil
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
import com.kunzisoft.keepass.view.DateTimeFieldView
|
import com.kunzisoft.keepass.view.DateTimeFieldView
|
||||||
|
|
||||||
class GroupDialogFragment : DatabaseDialogFragment() {
|
class GroupDialogFragment : DatabaseDialogFragment() {
|
||||||
@@ -60,7 +62,7 @@ class GroupDialogFragment : DatabaseDialogFragment() {
|
|||||||
private lateinit var uuidContainerView: ViewGroup
|
private lateinit var uuidContainerView: ViewGroup
|
||||||
private lateinit var uuidReferenceView: TextView
|
private lateinit var uuidReferenceView: TextView
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
mPopulateIconMethod = { imageView, icon ->
|
mPopulateIconMethod = { imageView, icon ->
|
||||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||||
@@ -106,17 +108,17 @@ class GroupDialogFragment : DatabaseDialogFragment() {
|
|||||||
uuidReferenceView = root.findViewById(R.id.group_UUID_reference)
|
uuidReferenceView = root.findViewById(R.id.group_UUID_reference)
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
// Retrieve the textColor to tint the icon
|
||||||
val ta = activity.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
val ta = activity.theme.obtainStyledAttributes(intArrayOf(R.attr.colorSecondary))
|
||||||
mIconColor = ta.getColor(0, Color.WHITE)
|
mIconColor = ta.getColor(0, Color.WHITE)
|
||||||
ta.recycle()
|
ta.recycle()
|
||||||
|
|
||||||
if (savedInstanceState != null
|
if (savedInstanceState != null
|
||||||
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
|
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
|
||||||
mGroupInfo = savedInstanceState.getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
mGroupInfo = savedInstanceState.getParcelableCompat(KEY_GROUP_INFO) ?: mGroupInfo
|
||||||
} else {
|
} else {
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
if (containsKey(KEY_GROUP_INFO)) {
|
if (containsKey(KEY_GROUP_INFO)) {
|
||||||
mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
mGroupInfo = getParcelableCompat(KEY_GROUP_INFO) ?: mGroupInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,23 +24,27 @@ import android.graphics.Color
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.*
|
import android.widget.Button
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.*
|
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.NONE
|
||||||
|
import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE
|
||||||
import com.kunzisoft.keepass.adapters.TagsProposalAdapter
|
import com.kunzisoft.keepass.adapters.TagsProposalAdapter
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.model.GroupInfo
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
import com.kunzisoft.keepass.view.DateTimeEditFieldView
|
import com.kunzisoft.keepass.view.DateTimeEditFieldView
|
||||||
import com.kunzisoft.keepass.view.InheritedCompletionView
|
import com.kunzisoft.keepass.view.InheritedCompletionView
|
||||||
import com.kunzisoft.keepass.view.TagsCompletionView
|
import com.kunzisoft.keepass.view.TagsCompletionView
|
||||||
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
|
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
|
||||||
import com.tokenautocomplete.FilteredArrayAdapter
|
import com.tokenautocomplete.FilteredArrayAdapter
|
||||||
import org.joda.time.DateTime
|
|
||||||
|
|
||||||
class GroupEditDialogFragment : DatabaseDialogFragment() {
|
class GroupEditDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
@@ -85,29 +89,21 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
mGroupEditViewModel.onDateSelected.observe(this) { viewModelDate ->
|
mGroupEditViewModel.onDateSelected.observe(this) { date ->
|
||||||
// Save the date
|
// Save the date
|
||||||
mGroupInfo.expiryTime = DateInstant(
|
mGroupInfo.expiryTime.setDate(date.year, date.month, date.day)
|
||||||
DateTime(mGroupInfo.expiryTime.date)
|
|
||||||
.withYear(viewModelDate.year)
|
|
||||||
.withMonthOfYear(viewModelDate.month + 1)
|
|
||||||
.withDayOfMonth(viewModelDate.day)
|
|
||||||
.toDate())
|
|
||||||
expirationView.dateTime = mGroupInfo.expiryTime
|
expirationView.dateTime = mGroupInfo.expiryTime
|
||||||
if (expirationView.dateTime.type == DateInstant.Type.DATE_TIME) {
|
if (expirationView.dateTime.type == DateInstant.Type.DATE_TIME) {
|
||||||
val instantTime = DateInstant(mGroupInfo.expiryTime.date, DateInstant.Type.TIME)
|
|
||||||
// Trick to recall selection with time
|
// Trick to recall selection with time
|
||||||
mGroupEditViewModel.requestDateTimeSelection(instantTime)
|
mGroupEditViewModel.requestDateTimeSelection(
|
||||||
|
DateInstant(mGroupInfo.expiryTime.instant, DateInstant.Type.TIME)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mGroupEditViewModel.onTimeSelected.observe(this) { viewModelTime ->
|
mGroupEditViewModel.onTimeSelected.observe(this) { viewModelTime ->
|
||||||
// Save the time
|
// Save the time
|
||||||
mGroupInfo.expiryTime = DateInstant(
|
mGroupInfo.expiryTime.setTime(viewModelTime.hour, viewModelTime.minute)
|
||||||
DateTime(mGroupInfo.expiryTime.date)
|
|
||||||
.withHourOfDay(viewModelTime.hours)
|
|
||||||
.withMinuteOfHour(viewModelTime.minutes)
|
|
||||||
.toDate(), mGroupInfo.expiryTime.type)
|
|
||||||
expirationView.dateTime = mGroupInfo.expiryTime
|
expirationView.dateTime = mGroupInfo.expiryTime
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +112,7 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
mPopulateIconMethod = { imageView, icon ->
|
mPopulateIconMethod = { imageView, icon ->
|
||||||
@@ -170,13 +166,13 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
&& savedInstanceState.containsKey(KEY_ACTION_ID)
|
&& savedInstanceState.containsKey(KEY_ACTION_ID)
|
||||||
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
|
&& savedInstanceState.containsKey(KEY_GROUP_INFO)) {
|
||||||
mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID))
|
mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(savedInstanceState.getInt(KEY_ACTION_ID))
|
||||||
mGroupInfo = savedInstanceState.getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
mGroupInfo = savedInstanceState.getParcelableCompat(KEY_GROUP_INFO) ?: mGroupInfo
|
||||||
} else {
|
} else {
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
if (containsKey(KEY_ACTION_ID))
|
if (containsKey(KEY_ACTION_ID))
|
||||||
mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID))
|
mEditGroupDialogAction = EditGroupDialogAction.getActionFromOrdinal(getInt(KEY_ACTION_ID))
|
||||||
if (containsKey(KEY_GROUP_INFO)) {
|
if (containsKey(KEY_GROUP_INFO)) {
|
||||||
mGroupInfo = getParcelable(KEY_GROUP_INFO) ?: mGroupInfo
|
mGroupInfo = getParcelableCompat(KEY_GROUP_INFO) ?: mGroupInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,11 +248,7 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
|
|
||||||
private fun retrieveGroupInfoFromViews() {
|
private fun retrieveGroupInfoFromViews() {
|
||||||
mGroupInfo.title = nameTextView.text.toString()
|
mGroupInfo.title = nameTextView.text.toString()
|
||||||
// Only if there
|
mGroupInfo.notes = notesTextView.text?.toString()
|
||||||
val newNotes = notesTextView.text.toString()
|
|
||||||
if (newNotes.isNotEmpty()) {
|
|
||||||
mGroupInfo.notes = newNotes
|
|
||||||
}
|
|
||||||
mGroupInfo.expires = expirationView.activation
|
mGroupInfo.expires = expirationView.activation
|
||||||
mGroupInfo.expiryTime = expirationView.dateTime
|
mGroupInfo.expiryTime = expirationView.dateTime
|
||||||
mGroupInfo.searchable = searchableView.getValue()
|
mGroupInfo.searchable = searchableView.getValue()
|
||||||
|
|||||||
@@ -27,10 +27,11 @@ import androidx.appcompat.app.AlertDialog
|
|||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||||
|
|
||||||
class IconEditDialogFragment : DatabaseDialogFragment() {
|
class IconEditDialogFragment : DatabaseDialogFragment() {
|
||||||
@@ -44,7 +45,7 @@ class IconEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
|
|
||||||
private var mCustomIcon: IconImageCustom? = null
|
private var mCustomIcon: IconImageCustom? = null
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
mPopulateIconMethod = { imageView, icon ->
|
mPopulateIconMethod = { imageView, icon ->
|
||||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon)
|
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon)
|
||||||
@@ -63,11 +64,11 @@ class IconEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
|
|
||||||
if (savedInstanceState != null
|
if (savedInstanceState != null
|
||||||
&& savedInstanceState.containsKey(KEY_CUSTOM_ICON_ID)) {
|
&& savedInstanceState.containsKey(KEY_CUSTOM_ICON_ID)) {
|
||||||
mCustomIcon = savedInstanceState.getParcelable(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
|
mCustomIcon = savedInstanceState.getParcelableCompat(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
|
||||||
} else {
|
} else {
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
if (containsKey(KEY_CUSTOM_ICON_ID)) {
|
if (containsKey(KEY_CUSTOM_ICON_ID)) {
|
||||||
mCustomIcon = getParcelable(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
|
mCustomIcon = getParcelableCompat(KEY_CUSTOM_ICON_ID) ?: mCustomIcon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ import android.widget.TextView
|
|||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
import com.kunzisoft.keepass.view.MainCredentialView
|
import com.kunzisoft.keepass.view.MainCredentialView
|
||||||
|
|
||||||
class MainCredentialDialogFragment : DatabaseDialogFragment() {
|
class MainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||||
@@ -65,7 +66,7 @@ class MainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
var databaseUri: Uri? = null
|
var databaseUri: Uri? = null
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
if (containsKey(KEY_ASK_CREDENTIAL_URI))
|
if (containsKey(KEY_ASK_CREDENTIAL_URI))
|
||||||
databaseUri = getParcelable(KEY_ASK_CREDENTIAL_URI)
|
databaseUri = getParcelableCompat(KEY_ASK_CREDENTIAL_URI)
|
||||||
}
|
}
|
||||||
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
@@ -74,7 +75,7 @@ class MainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
mainCredentialView = root.findViewById(R.id.main_credential_view)
|
mainCredentialView = root.findViewById(R.id.main_credential_view)
|
||||||
databaseUri?.let {
|
databaseUri?.let {
|
||||||
root.findViewById<TextView>(R.id.title_database)?.text =
|
root.findViewById<TextView>(R.id.title_database)?.text =
|
||||||
UriUtil.getFileData(requireContext(), it)?.name
|
it.getDocumentFile(requireContext())?.name
|
||||||
}
|
}
|
||||||
builder.setView(root)
|
builder.setView(root)
|
||||||
// Add action buttons
|
// Add action buttons
|
||||||
@@ -95,7 +96,7 @@ class MainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
mExternalFileHelper = ExternalFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
mainCredentialView?.populateKeyFileTextView(uri)
|
mainCredentialView?.populateKeyFileView(uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
|
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ import android.os.Bundle
|
|||||||
import androidx.appcompat.app.AlertDialog
|
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
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
|
||||||
class PasswordEncodingDialogFragment : DialogFragment() {
|
class PasswordEncodingDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
@@ -49,8 +50,8 @@ 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?.getParcelableCompat(DATABASE_URI_KEY)
|
||||||
val mainCredential: MainCredential = savedInstanceState?.getParcelable(MAIN_CREDENTIAL) ?: MainCredential()
|
val mainCredential: MainCredential = savedInstanceState?.getParcelableCompat(MAIN_CREDENTIAL) ?: MainCredential()
|
||||||
|
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
@@ -78,8 +79,10 @@ class PasswordEncodingDialogFragment : DialogFragment() {
|
|||||||
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
|
private const val DATABASE_URI_KEY = "DATABASE_URI_KEY"
|
||||||
private const val MAIN_CREDENTIAL = "MAIN_CREDENTIAL"
|
private const val MAIN_CREDENTIAL = "MAIN_CREDENTIAL"
|
||||||
|
|
||||||
fun getInstance(databaseUri: Uri,
|
fun getInstance(
|
||||||
mainCredential: MainCredential): SortDialogFragment {
|
databaseUri: Uri,
|
||||||
|
mainCredential: MainCredential
|
||||||
|
): 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)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
|
|||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.BuildConfig
|
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.openUrl
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Dialog that asks the user to download the pro version or make a donation.
|
* Custom Dialog that asks the user to download the pro version or make a donation.
|
||||||
@@ -45,13 +45,16 @@ class ProFeatureDialogFragment : DialogFragment() {
|
|||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_ad_free), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_ad_free), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_buy_pro), FROM_HTML_MODE_LEGACY))
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_buy_pro), FROM_HTML_MODE_LEGACY))
|
||||||
builder.setPositiveButton(R.string.download) { _, _ ->
|
builder.setPositiveButton(R.string.download) { _, _ ->
|
||||||
UriUtil.gotoUrl(requireContext(), R.string.app_pro_url)
|
activity.openUrl(
|
||||||
|
activity.getString(R.string.play_store_url,
|
||||||
|
activity.getString(R.string.keepro_app_id))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_feature_generosity), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_feature_generosity), FROM_HTML_MODE_LEGACY)).append("\n\n")
|
||||||
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_donation), FROM_HTML_MODE_LEGACY))
|
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_donation), FROM_HTML_MODE_LEGACY))
|
||||||
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||||
UriUtil.gotoUrl(requireContext(), R.string.contribution_url)
|
activity.openUrl(R.string.contribution_url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
builder.setMessage(stringBuilder)
|
builder.setMessage(stringBuilder)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import android.text.SpannableStringBuilder
|
|||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Dialog to confirm big file to upload
|
* Custom Dialog to confirm big file to upload
|
||||||
@@ -62,8 +63,8 @@ class ReplaceFileDialogFragment : DatabaseDialogFragment() {
|
|||||||
})
|
})
|
||||||
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
builder.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
mActionChooseListener?.onValidateReplaceFile(
|
mActionChooseListener?.onValidateReplaceFile(
|
||||||
arguments?.getParcelable(KEY_FILE_URI),
|
arguments?.getParcelableCompat(KEY_FILE_URI),
|
||||||
arguments?.getParcelable(KEY_ENTRY_ATTACHMENT))
|
arguments?.getParcelableCompat(KEY_ENTRY_ATTACHMENT))
|
||||||
}
|
}
|
||||||
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
builder.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
dismiss()
|
dismiss()
|
||||||
|
|||||||
@@ -35,28 +35,41 @@ import com.google.android.material.textfield.TextInputLayout
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
|
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
|
||||||
import com.kunzisoft.keepass.password.PasswordEntropy
|
import com.kunzisoft.keepass.password.PasswordEntropy
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
|
import com.kunzisoft.keepass.view.HardwareKeySelectionView
|
||||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
import com.kunzisoft.keepass.view.PassKeyView
|
import com.kunzisoft.keepass.view.PasswordEditView
|
||||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.security.SecureRandom
|
||||||
|
|
||||||
|
|
||||||
class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
private var mMasterPassword: String? = null
|
private var mMasterPassword: String? = null
|
||||||
private var mKeyFile: Uri? = null
|
private var mKeyFileUri: Uri? = null
|
||||||
|
private var mHardwareKey: HardwareKey? = null
|
||||||
|
|
||||||
private var rootView: View? = null
|
private lateinit var rootView: View
|
||||||
|
|
||||||
private var passwordCheckBox: CompoundButton? = null
|
private lateinit var passwordCheckBox: CompoundButton
|
||||||
|
private lateinit var passwordEditView: PasswordEditView
|
||||||
|
private lateinit var passwordRepeatTextInputLayout: TextInputLayout
|
||||||
|
private lateinit var passwordRepeatView: TextView
|
||||||
|
|
||||||
private var passKeyView: PassKeyView? = null
|
private lateinit var keyFileCheckBox: CompoundButton
|
||||||
private var passwordRepeatTextInputLayout: TextInputLayout? = null
|
private lateinit var keyFileGenerateButton: View
|
||||||
private var passwordRepeatView: TextView? = null
|
private lateinit var keyFileSelectionView: KeyFileSelectionView
|
||||||
|
|
||||||
private var keyFileCheckBox: CompoundButton? = null
|
private lateinit var hardwareKeyCheckBox: CompoundButton
|
||||||
private var keyFileSelectionView: KeyFileSelectionView? = null
|
private lateinit var hardwareKeySelectionView: HardwareKeySelectionView
|
||||||
|
|
||||||
private var mListener: AssignMainCredentialDialogListener? = null
|
private var mListener: AssignMainCredentialDialogListener? = null
|
||||||
|
|
||||||
@@ -67,13 +80,15 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
||||||
private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null
|
private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null
|
||||||
|
|
||||||
|
private var mAllowNoMasterKey: Boolean = false
|
||||||
|
|
||||||
private val passwordTextWatcher = object : TextWatcher {
|
private val passwordTextWatcher = object : TextWatcher {
|
||||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||||
|
|
||||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||||
|
|
||||||
override fun afterTextChanged(editable: Editable) {
|
override fun afterTextChanged(editable: Editable) {
|
||||||
passwordCheckBox?.isChecked = true
|
passwordCheckBox.isChecked = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,10 +128,9 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
|
|
||||||
var allowNoMasterKey = false
|
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
if (containsKey(ALLOW_NO_MASTER_KEY_ARG))
|
if (containsKey(ALLOW_NO_MASTER_KEY_ARG))
|
||||||
allowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false)
|
mAllowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
@@ -128,63 +142,73 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
|
|
||||||
rootView?.findViewById<View>(R.id.credentials_information)?.setOnClickListener {
|
rootView.findViewById<View>(R.id.credentials_information)?.setOnClickListener {
|
||||||
UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
|
activity.openUrl(R.string.credentials_explanation_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
passwordCheckBox = rootView.findViewById(R.id.password_checkbox)
|
||||||
passKeyView = rootView?.findViewById(R.id.password_view)
|
passwordEditView = rootView.findViewById(R.id.password_view)
|
||||||
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
|
passwordRepeatTextInputLayout = rootView.findViewById(R.id.password_repeat_input_layout)
|
||||||
passwordRepeatView = rootView?.findViewById(R.id.password_confirmation)
|
passwordRepeatView = rootView.findViewById(R.id.password_confirmation)
|
||||||
passwordRepeatView?.applyFontVisibility()
|
passwordRepeatView.applyFontVisibility()
|
||||||
|
|
||||||
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
keyFileCheckBox = rootView.findViewById(R.id.keyfile_checkbox)
|
||||||
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
keyFileGenerateButton = rootView.findViewById(R.id.keyfile_generate)
|
||||||
|
keyFileSelectionView = rootView.findViewById(R.id.keyfile_selection)
|
||||||
|
|
||||||
|
hardwareKeyCheckBox = rootView.findViewById(R.id.hardware_key_checkbox)
|
||||||
|
hardwareKeySelectionView = rootView.findViewById(R.id.hardware_key_selection)
|
||||||
|
|
||||||
mExternalFileHelper = ExternalFileHelper(this)
|
mExternalFileHelper = ExternalFileHelper(this)
|
||||||
|
mExternalFileHelper?.buildCreateDocument { createdFileUri ->
|
||||||
|
createdFileUri?.let { uri ->
|
||||||
|
createKeyFile(uri)
|
||||||
|
keyFileSelectionView.error = null
|
||||||
|
keyFileCheckBox.isChecked = true
|
||||||
|
keyFileSelectionView.uri = uri
|
||||||
|
}
|
||||||
|
}
|
||||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||||
uri?.let { pathUri ->
|
uri?.let { pathUri ->
|
||||||
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
pathUri.getDocumentFile(requireContext())?.length()?.let { lengthFile ->
|
||||||
keyFileSelectionView?.error = null
|
keyFileSelectionView.error = null
|
||||||
keyFileCheckBox?.isChecked = true
|
keyFileCheckBox.isChecked = true
|
||||||
keyFileSelectionView?.uri = pathUri
|
keyFileSelectionView.uri = pathUri
|
||||||
if (lengthFile <= 0L) {
|
showLengthKeyFileConfirmationDialog(lengthFile)
|
||||||
showEmptyKeyFileConfirmationDialog()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
keyFileGenerateButton.setOnClickListener {
|
||||||
|
mExternalFileHelper?.createDocument(DEFAULT_KEYFILE_NAME)
|
||||||
|
}
|
||||||
|
keyFileSelectionView.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
|
|
||||||
|
hardwareKeySelectionView.selectionListener = { hardwareKey ->
|
||||||
|
hardwareKeyCheckBox.isChecked = true
|
||||||
|
hardwareKeySelectionView.error =
|
||||||
|
if (!HardwareKeyActivity.isHardwareKeyAvailable(requireActivity(), hardwareKey)) {
|
||||||
|
// show hardware driver dialog if required
|
||||||
|
getString(R.string.error_driver_required, hardwareKey.toString())
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val dialog = builder.create()
|
val dialog = builder.create()
|
||||||
|
dialog.setOnShowListener { dialog1 ->
|
||||||
|
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
|
||||||
|
positiveButton.setOnClickListener {
|
||||||
|
|
||||||
if (passwordCheckBox != null && keyFileCheckBox!= null) {
|
mMasterPassword = ""
|
||||||
dialog.setOnShowListener { dialog1 ->
|
mKeyFileUri = null
|
||||||
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
|
mHardwareKey = null
|
||||||
positiveButton.setOnClickListener {
|
|
||||||
|
|
||||||
mMasterPassword = ""
|
approveMainCredential()
|
||||||
mKeyFile = null
|
}
|
||||||
|
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
|
||||||
var error = verifyPassword() || verifyKeyFile()
|
negativeButton.setOnClickListener {
|
||||||
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
|
mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
|
||||||
error = true
|
dismiss()
|
||||||
if (allowNoMasterKey)
|
|
||||||
showNoKeyConfirmationDialog()
|
|
||||||
else {
|
|
||||||
passwordRepeatTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!error) {
|
|
||||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
|
|
||||||
negativeButton.setOnClickListener {
|
|
||||||
mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,67 +218,123 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
return super.onCreateDialog(savedInstanceState)
|
return super.onCreateDialog(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createKeyFile(uri: Uri) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
activity?.contentResolver?.openOutputStream(uri)?.use { outputStream ->
|
||||||
|
val randomBytes = ByteArray(DEFAULT_KEYFILE_SIZE)
|
||||||
|
SecureRandom().nextBytes(randomBytes)
|
||||||
|
outputStream.write(randomBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun approveMainCredential() {
|
||||||
|
val errorPassword = verifyPassword()
|
||||||
|
val errorKeyFile = verifyKeyFile()
|
||||||
|
val errorHardwareKey = verifyHardwareKey()
|
||||||
|
// Check all to fill error
|
||||||
|
var error = errorPassword || errorKeyFile || errorHardwareKey
|
||||||
|
val hardwareKey = hardwareKeySelectionView.hardwareKey
|
||||||
|
if (!error
|
||||||
|
&& (!passwordCheckBox.isChecked)
|
||||||
|
&& (!keyFileCheckBox.isChecked)
|
||||||
|
&& (!hardwareKeyCheckBox.isChecked)
|
||||||
|
) {
|
||||||
|
error = true
|
||||||
|
if (mAllowNoMasterKey) {
|
||||||
|
// show no key dialog if required
|
||||||
|
showNoKeyConfirmationDialog()
|
||||||
|
} else {
|
||||||
|
passwordRepeatTextInputLayout.error =
|
||||||
|
getString(R.string.error_disallow_no_credentials)
|
||||||
|
}
|
||||||
|
} else if (!error
|
||||||
|
&& mMasterPassword.isNullOrEmpty()
|
||||||
|
&& !keyFileCheckBox.isChecked
|
||||||
|
&& !hardwareKeyCheckBox.isChecked
|
||||||
|
) {
|
||||||
|
// show empty password dialog if required
|
||||||
|
error = true
|
||||||
|
showEmptyPasswordConfirmationDialog()
|
||||||
|
} else if (!error
|
||||||
|
&& hardwareKey != null
|
||||||
|
&& !HardwareKeyActivity.isHardwareKeyAvailable(
|
||||||
|
requireActivity(), hardwareKey, false)
|
||||||
|
) {
|
||||||
|
// show hardware driver dialog if required
|
||||||
|
error = true
|
||||||
|
hardwareKeySelectionView.error =
|
||||||
|
getString(R.string.error_driver_required, hardwareKey.toString())
|
||||||
|
}
|
||||||
|
if (!error) {
|
||||||
|
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyPassword(): Boolean {
|
||||||
|
var error = false
|
||||||
|
passwordRepeatTextInputLayout.error = null
|
||||||
|
if (passwordCheckBox.isChecked) {
|
||||||
|
mMasterPassword = passwordEditView.passwordString
|
||||||
|
val confPassword = passwordRepeatView.text.toString()
|
||||||
|
|
||||||
|
// Verify that passwords match
|
||||||
|
if (mMasterPassword != confPassword) {
|
||||||
|
error = true
|
||||||
|
// Passwords do not match
|
||||||
|
passwordRepeatTextInputLayout.error = getString(R.string.error_pass_match)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyKeyFile(): Boolean {
|
||||||
|
var error = false
|
||||||
|
keyFileSelectionView.error = null
|
||||||
|
if (keyFileCheckBox.isChecked) {
|
||||||
|
keyFileSelectionView.uri?.let { uri ->
|
||||||
|
mKeyFileUri = uri
|
||||||
|
} ?: run {
|
||||||
|
error = true
|
||||||
|
keyFileSelectionView.error = getString(R.string.error_nokeyfile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyHardwareKey(): Boolean {
|
||||||
|
var error = false
|
||||||
|
hardwareKeySelectionView.error = null
|
||||||
|
if (hardwareKeyCheckBox.isChecked) {
|
||||||
|
hardwareKeySelectionView.hardwareKey?.let { hardwareKey ->
|
||||||
|
mHardwareKey = hardwareKey
|
||||||
|
} ?: run {
|
||||||
|
error = true
|
||||||
|
hardwareKeySelectionView.error = getString(R.string.error_no_hardware_key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
private fun retrieveMainCredential(): MainCredential {
|
private fun retrieveMainCredential(): MainCredential {
|
||||||
val masterPassword = if (passwordCheckBox!!.isChecked) mMasterPassword else null
|
val masterPassword = if (passwordCheckBox.isChecked) mMasterPassword else null
|
||||||
val keyFile = if (keyFileCheckBox!!.isChecked) mKeyFile else null
|
val keyFileUri = if (keyFileCheckBox.isChecked) mKeyFileUri else null
|
||||||
return MainCredential(masterPassword, keyFile)
|
val hardwareKey = if (hardwareKeyCheckBox.isChecked) mHardwareKey else null
|
||||||
|
return MainCredential(masterPassword, keyFileUri, hardwareKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
passKeyView?.addTextChangedListener(passwordTextWatcher)
|
passwordEditView.addTextChangedListener(passwordTextWatcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
passKeyView?.removeTextChangedListener(passwordTextWatcher)
|
passwordEditView.removeTextChangedListener(passwordTextWatcher)
|
||||||
}
|
|
||||||
|
|
||||||
private fun verifyPassword(): Boolean {
|
|
||||||
var error = false
|
|
||||||
if (passwordCheckBox != null
|
|
||||||
&& passwordCheckBox!!.isChecked
|
|
||||||
&& passKeyView != null
|
|
||||||
&& passwordRepeatView != null) {
|
|
||||||
mMasterPassword = passKeyView!!.passwordString
|
|
||||||
val confPassword = passwordRepeatView!!.text.toString()
|
|
||||||
|
|
||||||
// Verify that passwords match
|
|
||||||
if (mMasterPassword != confPassword) {
|
|
||||||
error = true
|
|
||||||
// Passwords do not match
|
|
||||||
passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((mMasterPassword == null
|
|
||||||
|| mMasterPassword!!.isEmpty())
|
|
||||||
&& (keyFileCheckBox == null
|
|
||||||
|| !keyFileCheckBox!!.isChecked
|
|
||||||
|| keyFileSelectionView?.uri == null)) {
|
|
||||||
error = true
|
|
||||||
showEmptyPasswordConfirmationDialog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return error
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun verifyKeyFile(): Boolean {
|
|
||||||
var error = false
|
|
||||||
if (keyFileCheckBox != null
|
|
||||||
&& keyFileCheckBox!!.isChecked) {
|
|
||||||
|
|
||||||
keyFileSelectionView?.uri?.let { uri ->
|
|
||||||
mKeyFile = uri
|
|
||||||
} ?: run {
|
|
||||||
error = true
|
|
||||||
keyFileSelectionView?.error = getString(R.string.error_nokeyfile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showEmptyPasswordConfirmationDialog() {
|
private fun showEmptyPasswordConfirmationDialog() {
|
||||||
@@ -262,10 +342,8 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
val builder = AlertDialog.Builder(it)
|
val builder = AlertDialog.Builder(it)
|
||||||
builder.setMessage(R.string.warning_empty_password)
|
builder.setMessage(R.string.warning_empty_password)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
if (!verifyKeyFile()) {
|
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
this@SetMainCredentialDialogFragment.dismiss()
|
||||||
this@SetMainCredentialDialogFragment.dismiss()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||||
mEmptyPasswordConfirmationDialog = builder.create()
|
mEmptyPasswordConfirmationDialog = builder.create()
|
||||||
@@ -287,21 +365,31 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showEmptyKeyFileConfirmationDialog() {
|
private fun showLengthKeyFileConfirmationDialog(length: Long) {
|
||||||
activity?.let {
|
activity?.let {
|
||||||
val builder = AlertDialog.Builder(it)
|
val builder = AlertDialog.Builder(it)
|
||||||
builder.setMessage(SpannableStringBuilder().apply {
|
builder.setMessage(SpannableStringBuilder().apply {
|
||||||
append(getString(R.string.warning_empty_keyfile))
|
|
||||||
append("\n\n")
|
|
||||||
append(getString(R.string.warning_empty_keyfile_explanation))
|
append(getString(R.string.warning_empty_keyfile_explanation))
|
||||||
append("\n\n")
|
var warning = false
|
||||||
append(getString(R.string.warning_sure_add_file))
|
if (length <= 0L) {
|
||||||
})
|
warning = true
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
append("\n\n")
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
append(getString(R.string.warning_empty_keyfile))
|
||||||
keyFileCheckBox?.isChecked = false
|
} else if (length > 10485760L) {
|
||||||
keyFileSelectionView?.uri = null
|
warning = true
|
||||||
}
|
append("\n\n")
|
||||||
|
append(getString(R.string.warning_large_keyfile))
|
||||||
|
}
|
||||||
|
if (warning) {
|
||||||
|
append("\n\n")
|
||||||
|
append(getString(R.string.warning_sure_add_file))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
keyFileCheckBox.isChecked = false
|
||||||
|
keyFileSelectionView.uri = null
|
||||||
|
}
|
||||||
mEmptyKeyFileConfirmationDialog = builder.create()
|
mEmptyKeyFileConfirmationDialog = builder.create()
|
||||||
mEmptyKeyFileConfirmationDialog?.show()
|
mEmptyKeyFileConfirmationDialog?.show()
|
||||||
}
|
}
|
||||||
@@ -310,6 +398,8 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
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"
|
||||||
|
private const val DEFAULT_KEYFILE_NAME = "keyfile.bin"
|
||||||
|
private const val DEFAULT_KEYFILE_SIZE = 128
|
||||||
|
|
||||||
fun getInstance(allowNoMasterKey: Boolean): SetMainCredentialDialogFragment {
|
fun getInstance(allowNoMasterKey: Boolean): SetMainCredentialDialogFragment {
|
||||||
val fragment = SetMainCredentialDialogFragment()
|
val fragment = SetMainCredentialDialogFragment()
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import android.view.inputmethod.EditorInfo
|
|||||||
import android.widget.*
|
import android.widget.*
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.BuildConfig
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.model.OtpModel
|
import com.kunzisoft.keepass.model.OtpModel
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
@@ -42,10 +41,13 @@ import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_TOTP_PERIOD
|
|||||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_HOTP_COUNTER
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_HOTP_COUNTER
|
||||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_DIGITS
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_DIGITS
|
||||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
|
||||||
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_SECRET
|
||||||
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 com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
||||||
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class SetOTPDialogFragment : DatabaseDialogFragment() {
|
class SetOTPDialogFragment : DatabaseDialogFragment() {
|
||||||
@@ -126,14 +128,14 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
|||||||
// Retrieve OTP model from instance state
|
// Retrieve OTP model from instance state
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
if (savedInstanceState.containsKey(KEY_OTP)) {
|
if (savedInstanceState.containsKey(KEY_OTP)) {
|
||||||
savedInstanceState.getParcelable<OtpModel>(KEY_OTP)?.let { otpModel ->
|
savedInstanceState.getParcelableCompat<OtpModel>(KEY_OTP)?.let { otpModel ->
|
||||||
mOtpElement = OtpElement(otpModel)
|
mOtpElement = OtpElement(otpModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
arguments?.apply {
|
arguments?.apply {
|
||||||
if (containsKey(KEY_OTP)) {
|
if (containsKey(KEY_OTP)) {
|
||||||
getParcelable<OtpModel?>(KEY_OTP)?.let { otpModel ->
|
getParcelableCompat<OtpModel>(KEY_OTP)?.let { otpModel ->
|
||||||
mOtpElement = OtpElement(otpModel)
|
mOtpElement = OtpElement(otpModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,7 +208,7 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
// Proprietary only on full version
|
// Proprietary only on full version
|
||||||
mTotpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
|
mTotpTokenTypeArray = OtpTokenType.getTotpTokenTypeValues(
|
||||||
UriUtil.contributingUser(activity)
|
activity.isContributingUser()
|
||||||
)
|
)
|
||||||
totpTokenTypeAdapter = ArrayAdapter(activity,
|
totpTokenTypeAdapter = ArrayAdapter(activity,
|
||||||
android.R.layout.simple_spinner_item, mTotpTokenTypeArray!!).apply {
|
android.R.layout.simple_spinner_item, mTotpTokenTypeArray!!).apply {
|
||||||
@@ -223,6 +225,9 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
otpAlgorithmSpinner?.adapter = otpAlgorithmAdapter
|
otpAlgorithmSpinner?.adapter = otpAlgorithmAdapter
|
||||||
|
|
||||||
|
// Ensure that the UX does not prevent user from hiding/unhiding text
|
||||||
|
otpSecretContainer?.errorIconDrawable = null
|
||||||
|
|
||||||
// Set the default value of OTP element
|
// Set the default value of OTP element
|
||||||
upgradeType()
|
upgradeType()
|
||||||
upgradeTokenType()
|
upgradeTokenType()
|
||||||
@@ -242,7 +247,7 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
root?.findViewById<View>(R.id.otp_information)?.setOnClickListener {
|
root?.findViewById<View>(R.id.otp_information)?.setOnClickListener {
|
||||||
UriUtil.gotoUrl(activity, R.string.otp_explanation_url)
|
activity.openUrl(R.string.otp_explanation_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.create()
|
return builder.create()
|
||||||
@@ -309,11 +314,16 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
|||||||
otpSecretTextView?.addTextChangedListener(object: TextWatcher {
|
otpSecretTextView?.addTextChangedListener(object: TextWatcher {
|
||||||
override fun afterTextChanged(s: Editable?) {
|
override fun afterTextChanged(s: Editable?) {
|
||||||
s?.toString()?.let { userString ->
|
s?.toString()?.let { userString ->
|
||||||
try {
|
if (userString.length >= MIN_OTP_SECRET) {
|
||||||
mOtpElement.setBase32Secret(userString.uppercase(Locale.ENGLISH))
|
try {
|
||||||
otpSecretContainer?.error = null
|
mOtpElement.setBase32Secret(userString.uppercase(Locale.ENGLISH))
|
||||||
} catch (exception: Exception) {
|
otpSecretContainer?.error = null
|
||||||
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
|
} catch (exception: Exception) {
|
||||||
|
otpSecretContainer?.error = getString(R.string.error_otp_secret_key)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
otpSecretContainer?.error = getString(R.string.error_otp_secret_length,
|
||||||
|
MIN_OTP_SECRET)
|
||||||
}
|
}
|
||||||
mSecretWellFormed = otpSecretContainer?.error == null
|
mSecretWellFormed = otpSecretContainer?.error == null
|
||||||
}
|
}
|
||||||
@@ -470,4 +480,4 @@ class SetOTPDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.activities.dialogs
|
|
||||||
|
|
||||||
import android.app.DatePickerDialog
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.app.TimePickerDialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.format.DateFormat
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
|
|
||||||
// Not as DatabaseDialogFragment because crash on KitKat
|
|
||||||
class TimePickerFragment : DialogFragment() {
|
|
||||||
|
|
||||||
private var defaultHour: Int = 0
|
|
||||||
private var defaultMinute: Int = 0
|
|
||||||
|
|
||||||
private var mListener: TimePickerDialog.OnTimeSetListener? = null
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
try {
|
|
||||||
mListener = context as TimePickerDialog.OnTimeSetListener
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
throw ClassCastException(context.toString()
|
|
||||||
+ " must implement " + DatePickerDialog.OnDateSetListener::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetach() {
|
|
||||||
mListener = null
|
|
||||||
super.onDetach()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
// Create a new instance of DatePickerDialog and return it
|
|
||||||
return context?.let {
|
|
||||||
arguments?.apply {
|
|
||||||
if (containsKey(DEFAULT_HOUR_BUNDLE_KEY))
|
|
||||||
defaultHour = getInt(DEFAULT_HOUR_BUNDLE_KEY)
|
|
||||||
if (containsKey(DEFAULT_MINUTE_BUNDLE_KEY))
|
|
||||||
defaultMinute = getInt(DEFAULT_MINUTE_BUNDLE_KEY)
|
|
||||||
}
|
|
||||||
|
|
||||||
TimePickerDialog(it, mListener, defaultHour, defaultMinute, DateFormat.is24HourFormat(activity))
|
|
||||||
} ?: super.onCreateDialog(savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val DEFAULT_HOUR_BUNDLE_KEY = "DEFAULT_HOUR_BUNDLE_KEY"
|
|
||||||
private const val DEFAULT_MINUTE_BUNDLE_KEY = "DEFAULT_MINUTE_BUNDLE_KEY"
|
|
||||||
|
|
||||||
fun getInstance(defaultHour: Int,
|
|
||||||
defaultMinute: Int): TimePickerFragment {
|
|
||||||
return TimePickerFragment().apply {
|
|
||||||
arguments = Bundle().apply {
|
|
||||||
putInt(DEFAULT_HOUR_BUNDLE_KEY, defaultHour)
|
|
||||||
putInt(DEFAULT_MINUTE_BUNDLE_KEY, defaultMinute)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,12 +22,12 @@ package com.kunzisoft.keepass.activities.dialogs
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
class UnavailableFeatureDialogFragment : DialogFragment() {
|
class UnavailableFeatureDialogFragment : DialogFragment() {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ 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.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Dialog that asks the user to download the pro version or make a donation.
|
* Custom Dialog that asks the user to download the pro version or make a donation.
|
||||||
@@ -39,21 +39,22 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
|
|||||||
val builder = AlertDialog.Builder(activity)
|
val builder = AlertDialog.Builder(activity)
|
||||||
|
|
||||||
val stringBuilder = SpannableStringBuilder()
|
val stringBuilder = SpannableStringBuilder()
|
||||||
if (UriUtil.contributingUser(activity)) {
|
/*
|
||||||
|
if (activity.isContributingUser()) {
|
||||||
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 {
|
} 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(" ")
|
||||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||||
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||||
UriUtil.gotoUrl(requireContext(), R.string.contribution_url)
|
context?.openUrl(R.string.contribution_url)
|
||||||
}
|
}
|
||||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
//}
|
||||||
}
|
|
||||||
builder.setMessage(stringBuilder)
|
builder.setMessage(stringBuilder)
|
||||||
// Create the AlertDialog object and return it
|
// Create the AlertDialog object and return it
|
||||||
return builder.create()
|
return builder.create()
|
||||||
|
|||||||
@@ -2,19 +2,19 @@ package com.kunzisoft.keepass.activities.fragments
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval
|
||||||
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
|
||||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
|
||||||
abstract class DatabaseFragment : StylishFragment(), DatabaseRetrieval {
|
abstract class DatabaseFragment : Fragment(), DatabaseRetrieval {
|
||||||
|
|
||||||
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
private val mDatabaseViewModel: DatabaseViewModel by activityViewModels()
|
||||||
protected var mDatabase: Database? = null
|
protected var mDatabase: ContextualDatabase? = null
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
@@ -38,7 +38,7 @@ abstract class DatabaseFragment : StylishFragment(), DatabaseRetrieval {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result
|
result: ActionRunnable.Result
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -35,14 +35,20 @@ import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
|
|||||||
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
||||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||||
import com.kunzisoft.keepass.adapters.TagsProposalAdapter
|
import com.kunzisoft.keepass.adapters.TagsProposalAdapter
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
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.template.Template
|
import com.kunzisoft.keepass.database.element.template.Template
|
||||||
import com.kunzisoft.keepass.model.AttachmentState
|
import com.kunzisoft.keepass.model.AttachmentState
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.StreamDirection
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
import com.kunzisoft.keepass.view.*
|
import com.kunzisoft.keepass.utils.getParcelableList
|
||||||
|
import com.kunzisoft.keepass.utils.putParcelableList
|
||||||
|
import com.kunzisoft.keepass.view.TagsCompletionView
|
||||||
|
import com.kunzisoft.keepass.view.TemplateEditView
|
||||||
|
import com.kunzisoft.keepass.view.collapse
|
||||||
|
import com.kunzisoft.keepass.view.expand
|
||||||
|
import com.kunzisoft.keepass.view.showByFading
|
||||||
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
|
||||||
import com.tokenautocomplete.FilteredArrayAdapter
|
import com.tokenautocomplete.FilteredArrayAdapter
|
||||||
|
|
||||||
@@ -71,12 +77,11 @@ class EntryEditFragment: DatabaseFragment() {
|
|||||||
super.onCreateView(inflater, container, savedInstanceState)
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
// Retrieve the textColor to tint the icon
|
||||||
val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
val taIconColor = context?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
mIconColor = taIconColor?.getColor(0, Color.BLACK) ?: Color.BLACK
|
mIconColor = taIconColor?.getColor(0, Color.BLACK) ?: Color.BLACK
|
||||||
taIconColor?.recycle()
|
taIconColor?.recycle()
|
||||||
|
|
||||||
return inflater.cloneInContext(contextThemed)
|
return inflater.inflate(R.layout.fragment_entry_edit, container, false)
|
||||||
.inflate(R.layout.fragment_entry_edit, container, false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View,
|
override fun onViewCreated(view: View,
|
||||||
@@ -124,7 +129,7 @@ class EntryEditFragment: DatabaseFragment() {
|
|||||||
|
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
val attachments: List<Attachment> =
|
val attachments: List<Attachment> =
|
||||||
savedInstanceState.getParcelableArrayList(ATTACHMENTS_TAG) ?: listOf()
|
savedInstanceState.getParcelableList(ATTACHMENTS_TAG) ?: listOf()
|
||||||
setAttachments(attachments)
|
setAttachments(attachments)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +273,7 @@ class EntryEditFragment: DatabaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
|
|
||||||
templateView.populateIconMethod = { imageView, icon ->
|
templateView.populateIconMethod = { imageView, icon ->
|
||||||
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor)
|
||||||
@@ -379,7 +384,7 @@ class EntryEditFragment: DatabaseFragment() {
|
|||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
outState.putParcelableArrayList(ATTACHMENTS_TAG, ArrayList(getAttachments()))
|
outState.putParcelableList(ATTACHMENTS_TAG, getAttachments())
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------
|
/* -------------
|
||||||
|
|||||||
@@ -14,20 +14,21 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
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.template.TemplateField
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
|
import com.kunzisoft.keepass.database.helper.getLocalizedName
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.StreamDirection
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
|
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
||||||
import com.kunzisoft.keepass.utils.UuidUtil
|
import com.kunzisoft.keepass.utils.UuidUtil
|
||||||
import com.kunzisoft.keepass.view.TemplateView
|
import com.kunzisoft.keepass.view.TemplateView
|
||||||
import com.kunzisoft.keepass.view.hideByFading
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
import com.kunzisoft.keepass.view.showByFading
|
import com.kunzisoft.keepass.view.showByFading
|
||||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class EntryFragment: DatabaseFragment() {
|
class EntryFragment: DatabaseFragment() {
|
||||||
|
|
||||||
@@ -58,8 +59,7 @@ class EntryFragment: DatabaseFragment() {
|
|||||||
savedInstanceState: Bundle?): View? {
|
savedInstanceState: Bundle?): View? {
|
||||||
super.onCreateView(inflater, container, savedInstanceState)
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
return inflater.cloneInContext(contextThemed)
|
return inflater.inflate(R.layout.fragment_entry, container, false)
|
||||||
.inflate(R.layout.fragment_entry, container, false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View,
|
override fun onViewCreated(view: View,
|
||||||
@@ -133,7 +133,7 @@ class EntryFragment: DatabaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
|
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
|
||||||
attachmentsAdapter?.database = database
|
attachmentsAdapter?.database = database
|
||||||
@@ -158,11 +158,9 @@ class EntryFragment: DatabaseFragment() {
|
|||||||
|
|
||||||
setOnCopyActionClickListener { field ->
|
setOnCopyActionClickListener { field ->
|
||||||
mClipboardHelper?.timeoutCopyToClipboard(
|
mClipboardHelper?.timeoutCopyToClipboard(
|
||||||
|
TemplateField.getLocalizedName(context, field.name),
|
||||||
field.protectedValue.stringValue,
|
field.protectedValue.stringValue,
|
||||||
getString(
|
field.protectedValue.isProtected
|
||||||
R.string.copy_field,
|
|
||||||
TemplateField.getLocalizedName(context, field.name)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,8 +249,7 @@ class EntryFragment: DatabaseFragment() {
|
|||||||
|
|
||||||
fun launchEntryCopyEducationAction() {
|
fun launchEntryCopyEducationAction() {
|
||||||
val appNameString = getString(R.string.app_name)
|
val appNameString = getString(R.string.app_name)
|
||||||
mClipboardHelper?.timeoutCopyToClipboard(appNameString,
|
mClipboardHelper?.timeoutCopyToClipboard(appNameString, appNameString)
|
||||||
getString(R.string.copy_field, appNameString))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
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 com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
|
||||||
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
|
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||||
|
|
||||||
class EntryHistoryFragment: StylishFragment() {
|
class EntryHistoryFragment: Fragment() {
|
||||||
|
|
||||||
private lateinit var historyContainerView: View
|
private lateinit var historyContainerView: View
|
||||||
private lateinit var historyListView: RecyclerView
|
private lateinit var historyListView: RecyclerView
|
||||||
@@ -28,8 +28,7 @@ class EntryHistoryFragment: StylishFragment() {
|
|||||||
): View? {
|
): View? {
|
||||||
super.onCreateView(inflater, container, savedInstanceState)
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
return inflater.cloneInContext(contextThemed)
|
return inflater.inflate(R.layout.fragment_entry_history, container, false)
|
||||||
.inflate(R.layout.fragment_entry_history, container, false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
|||||||
@@ -22,27 +22,33 @@ package com.kunzisoft.keepass.activities.fragments
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.*
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.fragment.app.activityViewModels
|
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 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.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.adapters.NodesAdapter
|
import com.kunzisoft.keepass.adapters.NodesAdapter
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
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.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.utils.KeyboardUtil.hideKeyboard
|
||||||
import com.kunzisoft.keepass.viewmodels.GroupViewModel
|
import com.kunzisoft.keepass.viewmodels.GroupViewModel
|
||||||
import java.util.*
|
import java.util.LinkedList
|
||||||
|
|
||||||
class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListener {
|
class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListener {
|
||||||
|
|
||||||
@@ -73,19 +79,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
private var mRecycleBinEnable: Boolean = false
|
private var mRecycleBinEnable: Boolean = false
|
||||||
private var mRecycleBin: Group? = null
|
private var mRecycleBin: Group? = null
|
||||||
|
|
||||||
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() {
|
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
|
||||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||||
super.onScrollStateChanged(recyclerView, newState)
|
super.onScrollStateChanged(recyclerView, newState)
|
||||||
@@ -99,6 +92,40 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val menuProvider: MenuProvider = object: MenuProvider {
|
||||||
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
|
menuInflater.inflate(R.menu.tree, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||||
|
return when (menuItem.itemId) {
|
||||||
|
R.id.menu_sort -> {
|
||||||
|
context?.let { context ->
|
||||||
|
val sortDialogFragment: SortDialogFragment =
|
||||||
|
if (mRecycleBinEnable) {
|
||||||
|
SortDialogFragment.getInstance(
|
||||||
|
PreferencesUtil.getListSort(context),
|
||||||
|
PreferencesUtil.getAscendingSort(context),
|
||||||
|
PreferencesUtil.getGroupsBeforeSort(context),
|
||||||
|
PreferencesUtil.getRecycleBinBottomSort(context)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SortDialogFragment.getInstance(
|
||||||
|
PreferencesUtil.getListSort(context),
|
||||||
|
PreferencesUtil.getAscendingSort(context),
|
||||||
|
PreferencesUtil.getGroupsBeforeSort(context)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
|
|
||||||
@@ -137,23 +164,16 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
super.onDetach()
|
super.onDetach()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
setHasOptionsMenu(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
|
||||||
mRecycleBinEnable = database?.isRecycleBinEnabled == true
|
mRecycleBinEnable = database?.isRecycleBinEnabled == true
|
||||||
mRecycleBin = database?.recycleBin
|
mRecycleBin = database?.recycleBin
|
||||||
|
|
||||||
contextThemed?.let { context ->
|
context?.let { context ->
|
||||||
database?.let { database ->
|
database?.let { database ->
|
||||||
mAdapter = NodesAdapter(context, database).apply {
|
mAdapter = NodesAdapter(context, database).apply {
|
||||||
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
|
setOnNodeClickListener(object : NodesAdapter.NodeClickCallback {
|
||||||
override fun onNodeClick(database: Database, node: Node) {
|
override fun onNodeClick(database: ContextualDatabase, node: Node) {
|
||||||
if (mCurrentGroup?.isVirtual == false
|
if (nodeActionSelectionMode) {
|
||||||
&& nodeActionSelectionMode) {
|
|
||||||
if (listActionNodes.contains(node)) {
|
if (listActionNodes.contains(node)) {
|
||||||
// Remove selected item if already selected
|
// Remove selected item if already selected
|
||||||
listActionNodes.remove(node)
|
listActionNodes.remove(node)
|
||||||
@@ -169,9 +189,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNodeLongClick(database: Database, node: Node): Boolean {
|
override fun onNodeLongClick(database: ContextualDatabase, node: Node): Boolean {
|
||||||
if (mCurrentGroup?.isVirtual == false
|
if (nodeActionPasteMode == PasteMode.UNDEFINED) {
|
||||||
&& nodeActionPasteMode == PasteMode.UNDEFINED) {
|
|
||||||
// Select the first item after a long click
|
// Select the first item after a long click
|
||||||
if (!listActionNodes.contains(node))
|
if (!listActionNodes.contains(node))
|
||||||
listActionNodes.add(node)
|
listActionNodes.add(node)
|
||||||
@@ -180,6 +199,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
|
|
||||||
setActionNodes(listActionNodes)
|
setActionNodes(listActionNodes)
|
||||||
notifyNodeChanged(node)
|
notifyNodeChanged(node)
|
||||||
|
activity?.hideKeyboard()
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -191,7 +211,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result
|
result: ActionRunnable.Result
|
||||||
) {
|
) {
|
||||||
@@ -207,13 +227,14 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
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
|
||||||
return inflater.cloneInContext(contextThemed)
|
return inflater.inflate(R.layout.fragment_nodes, container, false)
|
||||||
.inflate(R.layout.fragment_nodes, container, false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
|
||||||
|
|
||||||
mNodesRecyclerView = view.findViewById(R.id.nodes_list)
|
mNodesRecyclerView = view.findViewById(R.id.nodes_list)
|
||||||
notFoundView = view.findViewById(R.id.not_found_container)
|
notFoundView = view.findViewById(R.id.not_found_container)
|
||||||
|
|
||||||
@@ -242,8 +263,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
activity?.intent?.let {
|
activity?.intent?.let {
|
||||||
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
|
specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
rebuildList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
@@ -293,43 +312,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
fun actionNodesCallback(database: ContextualDatabase,
|
||||||
inflater.inflate(R.menu.tree, menu)
|
|
||||||
|
|
||||||
super.onCreateOptionsMenu(menu, inflater)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
|
||||||
|
|
||||||
R.id.menu_sort -> {
|
|
||||||
context?.let { context ->
|
|
||||||
val sortDialogFragment: SortDialogFragment =
|
|
||||||
if (mRecycleBinEnable) {
|
|
||||||
SortDialogFragment.getInstance(
|
|
||||||
PreferencesUtil.getListSort(context),
|
|
||||||
PreferencesUtil.getAscendingSort(context),
|
|
||||||
PreferencesUtil.getGroupsBeforeSort(context),
|
|
||||||
PreferencesUtil.getRecycleBinBottomSort(context)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
SortDialogFragment.getInstance(
|
|
||||||
PreferencesUtil.getListSort(context),
|
|
||||||
PreferencesUtil.getAscendingSort(context),
|
|
||||||
PreferencesUtil.getGroupsBeforeSort(context)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun actionNodesCallback(database: Database,
|
|
||||||
nodes: List<Node>,
|
nodes: List<Node>,
|
||||||
menuListener: NodesActionMenuListener?,
|
menuListener: NodesActionMenuListener?,
|
||||||
onDestroyActionMode: (mode: ActionMode?) -> Unit) : ActionMode.Callback {
|
onDestroyActionMode: (mode: ActionMode?) -> Unit) : ActionMode.Callback {
|
||||||
@@ -363,14 +346,12 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Move
|
// Move
|
||||||
if (database.isReadOnly
|
if (database.isReadOnly) {
|
||||||
|| isASearchResult) {
|
|
||||||
menu?.removeItem(R.id.menu_move)
|
menu?.removeItem(R.id.menu_move)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy (not allowed for group)
|
// Copy (not allowed for group)
|
||||||
if (database.isReadOnly
|
if (database.isReadOnly
|
||||||
|| isASearchResult
|
|
||||||
|| nodes.any { it.type == Type.GROUP }) {
|
|| nodes.any { it.type == Type.GROUP }) {
|
||||||
menu?.removeItem(R.id.menu_copy)
|
menu?.removeItem(R.id.menu_copy)
|
||||||
}
|
}
|
||||||
@@ -433,20 +414,20 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
* 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(database: Database, node: Node)
|
fun onNodeClick(database: ContextualDatabase, node: Node)
|
||||||
fun onNodeSelected(database: Database, nodes: List<Node>): Boolean
|
fun onNodeSelected(database: ContextualDatabase, 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(database: Database, node: Node): Boolean
|
fun onOpenMenuClick(database: ContextualDatabase, node: Node): Boolean
|
||||||
fun onEditMenuClick(database: Database, node: Node): Boolean
|
fun onEditMenuClick(database: ContextualDatabase, node: Node): Boolean
|
||||||
fun onCopyMenuClick(database: Database, nodes: List<Node>): Boolean
|
fun onCopyMenuClick(database: ContextualDatabase, nodes: List<Node>): Boolean
|
||||||
fun onMoveMenuClick(database: Database, nodes: List<Node>): Boolean
|
fun onMoveMenuClick(database: ContextualDatabase, nodes: List<Node>): Boolean
|
||||||
fun onDeleteMenuClick(database: Database, nodes: List<Node>): Boolean
|
fun onDeleteMenuClick(database: ContextualDatabase, nodes: List<Node>): Boolean
|
||||||
fun onPasteMenuClick(database: Database, pasteMode: PasteMode?, nodes: List<Node>): Boolean
|
fun onPasteMenuClick(database: ContextualDatabase, pasteMode: PasteMode?, nodes: List<Node>): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class PasteMode {
|
enum class PasteMode {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.activities.fragments
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
|
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
|
|||||||
return R.layout.fragment_icon_grid
|
return R.layout.fragment_icon_grid
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun defineIconList(database: Database?) {
|
override fun defineIconList(database: ContextualDatabase?) {
|
||||||
database?.doForEachCustomIcons { customIcon, _ ->
|
database?.doForEachCustomIcons { customIcon, _ ->
|
||||||
iconPickerAdapter.addIcon(customIcon, false)
|
iconPickerAdapter.addIcon(customIcon, false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import androidx.fragment.app.activityViewModels
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.adapters.IconPickerAdapter
|
import com.kunzisoft.keepass.adapters.IconPickerAdapter
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
|
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
|
||||||
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -47,7 +47,7 @@ abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
|
|||||||
|
|
||||||
abstract fun retrieveMainLayoutId(): Int
|
abstract fun retrieveMainLayoutId(): Int
|
||||||
|
|
||||||
abstract fun defineIconList(database: Database?)
|
abstract fun defineIconList(database: ContextualDatabase?)
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater,
|
override fun onCreateView(inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
@@ -59,7 +59,7 @@ abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
// Retrieve the textColor to tint the icon
|
||||||
val ta = contextThemed?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
val ta = context?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
val tintColor = ta?.getColor(0, Color.BLACK) ?: Color.BLACK
|
val tintColor = ta?.getColor(0, Color.BLACK) ?: Color.BLACK
|
||||||
ta?.recycle()
|
ta?.recycle()
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ abstract class IconFragment<T: IconImageDraw> : DatabaseFragment(),
|
|||||||
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
resetAppTimeoutWhenViewFocusedOrChanged(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory
|
iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import com.google.android.material.tabs.TabLayout
|
|||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.adapters.IconPickerPagerAdapter
|
import com.kunzisoft.keepass.adapters.IconPickerPagerAdapter
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
|
||||||
|
|
||||||
class IconPickerFragment : DatabaseFragment() {
|
class IconPickerFragment : DatabaseFragment() {
|
||||||
@@ -48,7 +48,7 @@ class IconPickerFragment : DatabaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
|
iconPickerPagerAdapter = IconPickerPagerAdapter(this,
|
||||||
if (database?.allowCustomIcons == true) 2 else 1)
|
if (database?.allowCustomIcons == true) 2 else 1)
|
||||||
viewPager.adapter = iconPickerPagerAdapter
|
viewPager.adapter = iconPickerPagerAdapter
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
package com.kunzisoft.keepass.activities.fragments
|
package com.kunzisoft.keepass.activities.fragments
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
||||||
|
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ class IconStandardFragment : IconFragment<IconImageStandard>() {
|
|||||||
return R.layout.fragment_icon_grid
|
return R.layout.fragment_icon_grid
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun defineIconList(database: Database?) {
|
override fun defineIconList(database: ContextualDatabase?) {
|
||||||
database?.doForEachStandardIcons { standardIcon ->
|
database?.doForEachStandardIcons { standardIcon ->
|
||||||
iconPickerAdapter.addIcon(standardIcon, false)
|
iconPickerAdapter.addIcon(standardIcon, false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import com.google.android.material.tabs.TabLayout
|
|||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.adapters.KeyGeneratorPagerAdapter
|
import com.kunzisoft.keepass.adapters.KeyGeneratorPagerAdapter
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
||||||
|
|
||||||
class KeyGeneratorFragment : DatabaseFragment() {
|
class KeyGeneratorFragment : DatabaseFragment() {
|
||||||
@@ -107,7 +107,7 @@ class KeyGeneratorFragment : DatabaseFragment() {
|
|||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
// Nothing here
|
// Nothing here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,16 +30,16 @@ import androidx.core.widget.doOnTextChanged
|
|||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import com.google.android.material.slider.Slider
|
import com.google.android.material.slider.Slider
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.password.PassphraseGenerator
|
import com.kunzisoft.keepass.password.PassphraseGenerator
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
import com.kunzisoft.keepass.view.PassKeyView
|
import com.kunzisoft.keepass.view.PasswordEditView
|
||||||
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
||||||
|
|
||||||
class PassphraseGeneratorFragment : DatabaseFragment() {
|
class PassphraseGeneratorFragment : DatabaseFragment() {
|
||||||
|
|
||||||
private lateinit var passKeyView: PassKeyView
|
private lateinit var passwordEditView: PasswordEditView
|
||||||
|
|
||||||
private lateinit var sliderWordCount: Slider
|
private lateinit var sliderWordCount: Slider
|
||||||
private lateinit var wordCountText: EditText
|
private lateinit var wordCountText: EditText
|
||||||
@@ -62,7 +62,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
passKeyView = view.findViewById(R.id.passphrase_view)
|
passwordEditView = view.findViewById(R.id.passphrase_view)
|
||||||
val passphraseCopyView: ImageView? = view.findViewById(R.id.passphrase_copy_button)
|
val passphraseCopyView: ImageView? = view.findViewById(R.id.passphrase_copy_button)
|
||||||
sliderWordCount = view.findViewById(R.id.slider_word_count)
|
sliderWordCount = view.findViewById(R.id.slider_word_count)
|
||||||
wordCountText = view.findViewById(R.id.word_count)
|
wordCountText = view.findViewById(R.id.word_count)
|
||||||
@@ -73,14 +73,16 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
|
|||||||
minSliderWordCount = resources.getInteger(R.integer.passphrase_generator_word_count_min)
|
minSliderWordCount = resources.getInteger(R.integer.passphrase_generator_word_count_min)
|
||||||
maxSliderWordCount = resources.getInteger(R.integer.passphrase_generator_word_count_max)
|
maxSliderWordCount = resources.getInteger(R.integer.passphrase_generator_word_count_max)
|
||||||
|
|
||||||
contextThemed?.let { context ->
|
context?.let { context ->
|
||||||
passphraseCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(context))
|
passphraseCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(context))
|
||||||
View.VISIBLE else View.GONE
|
View.VISIBLE else View.GONE
|
||||||
val clipboardHelper = ClipboardHelper(context)
|
val clipboardHelper = ClipboardHelper(context)
|
||||||
passphraseCopyView?.setOnClickListener {
|
passphraseCopyView?.setOnClickListener {
|
||||||
clipboardHelper.timeoutCopyToClipboard(passKeyView.passwordString,
|
clipboardHelper.timeoutCopyToClipboard(
|
||||||
getString(R.string.copy_field,
|
getString(R.string.passphrase),
|
||||||
getString(R.string.entry_password)))
|
passwordEditView.passwordString,
|
||||||
|
true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
wordCaseAdapter = ArrayAdapter(context,
|
wordCaseAdapter = ArrayAdapter(context,
|
||||||
@@ -144,7 +146,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
|
|||||||
generatePassphrase()
|
generatePassphrase()
|
||||||
|
|
||||||
mKeyGeneratorViewModel.passphraseGeneratedValidated.observe(viewLifecycleOwner) {
|
mKeyGeneratorViewModel.passphraseGeneratedValidated.observe(viewLifecycleOwner) {
|
||||||
mKeyGeneratorViewModel.setKeyGenerated(passKeyView.passwordString)
|
mKeyGeneratorViewModel.setKeyGenerated(passwordEditView.passwordString)
|
||||||
}
|
}
|
||||||
|
|
||||||
mKeyGeneratorViewModel.requirePassphraseGeneration.observe(viewLifecycleOwner) {
|
mKeyGeneratorViewModel.requirePassphraseGeneration.observe(viewLifecycleOwner) {
|
||||||
@@ -217,7 +219,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to generate a passphrase", e)
|
Log.e(TAG, "Unable to generate a passphrase", e)
|
||||||
}
|
}
|
||||||
passKeyView.passwordString = passphrase
|
passwordEditView.passwordString = passphrase
|
||||||
charactersCountText.text = getString(R.string.character_count, passphrase.length)
|
charactersCountText.text = getString(R.string.character_count, passphrase.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +244,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
// Nothing here
|
// Nothing here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,16 +32,16 @@ import androidx.core.widget.doOnTextChanged
|
|||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import com.google.android.material.slider.Slider
|
import com.google.android.material.slider.Slider
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.password.PasswordGenerator
|
import com.kunzisoft.keepass.password.PasswordGenerator
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
import com.kunzisoft.keepass.view.PassKeyView
|
import com.kunzisoft.keepass.view.PasswordEditView
|
||||||
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
import com.kunzisoft.keepass.viewmodels.KeyGeneratorViewModel
|
||||||
|
|
||||||
class PasswordGeneratorFragment : DatabaseFragment() {
|
class PasswordGeneratorFragment : DatabaseFragment() {
|
||||||
|
|
||||||
private lateinit var passKeyView: PassKeyView
|
private lateinit var passwordEditView: PasswordEditView
|
||||||
|
|
||||||
private lateinit var sliderLength: Slider
|
private lateinit var sliderLength: Slider
|
||||||
private lateinit var lengthEditView: EditText
|
private lateinit var lengthEditView: EditText
|
||||||
@@ -74,7 +74,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
passKeyView = view.findViewById(R.id.password_view)
|
passwordEditView = view.findViewById(R.id.password_view)
|
||||||
val passwordCopyView: ImageView? = view.findViewById(R.id.password_copy_button)
|
val passwordCopyView: ImageView? = view.findViewById(R.id.password_copy_button)
|
||||||
|
|
||||||
sliderLength = view.findViewById(R.id.slider_length)
|
sliderLength = view.findViewById(R.id.slider_length)
|
||||||
@@ -94,14 +94,16 @@ class PasswordGeneratorFragment : DatabaseFragment() {
|
|||||||
atLeastOneCompound = view.findViewById(R.id.atLeastOne_filter)
|
atLeastOneCompound = view.findViewById(R.id.atLeastOne_filter)
|
||||||
excludeAmbiguousCompound = view.findViewById(R.id.excludeAmbiguous_filter)
|
excludeAmbiguousCompound = view.findViewById(R.id.excludeAmbiguous_filter)
|
||||||
|
|
||||||
contextThemed?.let { context ->
|
context?.let { context ->
|
||||||
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(context))
|
passwordCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(context))
|
||||||
View.VISIBLE else View.GONE
|
View.VISIBLE else View.GONE
|
||||||
val clipboardHelper = ClipboardHelper(context)
|
val clipboardHelper = ClipboardHelper(context)
|
||||||
passwordCopyView?.setOnClickListener {
|
passwordCopyView?.setOnClickListener {
|
||||||
clipboardHelper.timeoutCopyToClipboard(passKeyView.passwordString,
|
clipboardHelper.timeoutCopyToClipboard(
|
||||||
getString(R.string.copy_field,
|
getString(R.string.password),
|
||||||
getString(R.string.entry_password)))
|
passwordEditView.passwordString,
|
||||||
|
true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +195,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
|
|||||||
generatePassword()
|
generatePassword()
|
||||||
|
|
||||||
mKeyGeneratorViewModel.passwordGeneratedValidated.observe(viewLifecycleOwner) {
|
mKeyGeneratorViewModel.passwordGeneratedValidated.observe(viewLifecycleOwner) {
|
||||||
mKeyGeneratorViewModel.setKeyGenerated(passKeyView.passwordString)
|
mKeyGeneratorViewModel.setKeyGenerated(passwordEditView.passwordString)
|
||||||
}
|
}
|
||||||
|
|
||||||
mKeyGeneratorViewModel.requirePasswordGeneration.observe(viewLifecycleOwner) {
|
mKeyGeneratorViewModel.requirePasswordGeneration.observe(viewLifecycleOwner) {
|
||||||
@@ -308,7 +310,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to generate a password", e)
|
Log.e(TAG, "Unable to generate a password", e)
|
||||||
}
|
}
|
||||||
passKeyView.passwordString = password
|
passwordEditView.passwordString = password
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
@@ -316,7 +318,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
// Nothing here
|
// Nothing here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ import com.kunzisoft.keepass.autofill.AutofillComponent
|
|||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||||
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 java.io.Serializable
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
import com.kunzisoft.keepass.utils.getEnumExtra
|
||||||
|
import com.kunzisoft.keepass.utils.putEnumExtra
|
||||||
|
|
||||||
object EntrySelectionHelper {
|
object EntrySelectionHelper {
|
||||||
|
|
||||||
@@ -82,7 +84,7 @@ object EntrySelectionHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun retrieveSearchInfoFromIntent(intent: Intent): SearchInfo? {
|
fun retrieveSearchInfoFromIntent(intent: Intent): SearchInfo? {
|
||||||
return intent.getParcelableExtra(KEY_SEARCH_INFO)
|
return intent.getParcelableExtraCompat(KEY_SEARCH_INFO)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
|
private fun addRegisterInfoInIntent(intent: Intent, registerInfo: RegisterInfo?) {
|
||||||
@@ -92,7 +94,7 @@ object EntrySelectionHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun retrieveRegisterInfoFromIntent(intent: Intent): RegisterInfo? {
|
fun retrieveRegisterInfoFromIntent(intent: Intent): RegisterInfo? {
|
||||||
return intent.getParcelableExtra(KEY_REGISTER_INFO)
|
return intent.getParcelableExtraCompat(KEY_REGISTER_INFO)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeInfoFromIntent(intent: Intent) {
|
fun removeInfoFromIntent(intent: Intent) {
|
||||||
@@ -101,7 +103,7 @@ object EntrySelectionHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
|
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
|
||||||
intent.putExtra(KEY_SPECIAL_MODE, specialMode as Serializable)
|
intent.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
|
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
|
||||||
@@ -109,12 +111,11 @@ object EntrySelectionHelper {
|
|||||||
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
|
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
|
||||||
return SpecialMode.SELECTION
|
return SpecialMode.SELECTION
|
||||||
}
|
}
|
||||||
return intent.getSerializableExtra(KEY_SPECIAL_MODE) as SpecialMode?
|
return intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) ?: SpecialMode.DEFAULT
|
||||||
?: SpecialMode.DEFAULT
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
|
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
|
||||||
intent.putExtra(KEY_TYPE_MODE, typeMode as Serializable)
|
intent.putEnumExtra(KEY_TYPE_MODE, typeMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
|
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
|
||||||
@@ -122,7 +123,7 @@ object EntrySelectionHelper {
|
|||||||
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
|
if (AutofillHelper.retrieveAutofillComponent(intent) != null)
|
||||||
return TypeMode.AUTOFILL
|
return TypeMode.AUTOFILL
|
||||||
}
|
}
|
||||||
return intent.getSerializableExtra(KEY_TYPE_MODE) as TypeMode? ?: TypeMode.DEFAULT
|
return intent.getEnumExtra<TypeMode>(KEY_TYPE_MODE) ?: TypeMode.DEFAULT
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeModesFromIntent(intent: Intent) {
|
fun removeModesFromIntent(intent: Intent) {
|
||||||
@@ -175,7 +176,7 @@ object EntrySelectionHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!autofillComponentInit) {
|
if (!autofillComponentInit) {
|
||||||
if (intent.getSerializableExtra(KEY_SPECIAL_MODE) != null) {
|
if (intent.getEnumExtra<SpecialMode>(KEY_SPECIAL_MODE) != null) {
|
||||||
when (retrieveTypeModeFromIntent(intent)) {
|
when (retrieveTypeModeFromIntent(intent)) {
|
||||||
TypeMode.DEFAULT -> {
|
TypeMode.DEFAULT -> {
|
||||||
removeModesFromIntent(intent)
|
removeModesFromIntent(intent)
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.activities.helpers
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -33,7 +32,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil.takeUriPermission
|
||||||
|
|
||||||
class ExternalFileHelper {
|
class ExternalFileHelper {
|
||||||
|
|
||||||
@@ -56,11 +55,9 @@ class ExternalFileHelper {
|
|||||||
|
|
||||||
fun buildOpenDocument(onFileSelected: ((uri: Uri?) -> Unit)?) {
|
fun buildOpenDocument(onFileSelected: ((uri: Uri?) -> Unit)?) {
|
||||||
|
|
||||||
val resultCallback = ActivityResultCallback<Uri> { result ->
|
val resultCallback = ActivityResultCallback<Uri?> { result ->
|
||||||
result?.let { uri ->
|
activity?.contentResolver?.takeUriPermission(result)
|
||||||
UriUtil.takeUriPermission(activity?.contentResolver, uri)
|
onFileSelected?.invoke(result)
|
||||||
onFileSelected?.invoke(uri)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getContentResultLauncher = if (fragment != null) {
|
getContentResultLauncher = if (fragment != null) {
|
||||||
@@ -91,7 +88,7 @@ class ExternalFileHelper {
|
|||||||
fun buildCreateDocument(typeString: String = "application/octet-stream",
|
fun buildCreateDocument(typeString: String = "application/octet-stream",
|
||||||
onFileCreated: (fileCreated: Uri?)->Unit) {
|
onFileCreated: (fileCreated: Uri?)->Unit) {
|
||||||
|
|
||||||
val resultCallback = ActivityResultCallback<Uri> { result ->
|
val resultCallback = ActivityResultCallback<Uri?> { result ->
|
||||||
onFileCreated.invoke(result)
|
onFileCreated.invoke(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +147,7 @@ class ExternalFileHelper {
|
|||||||
|
|
||||||
class OpenDocument : ActivityResultContracts.OpenDocument() {
|
class OpenDocument : ActivityResultContracts.OpenDocument() {
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
override fun createIntent(context: Context, input: Array<out String>): Intent {
|
override fun createIntent(context: Context, input: Array<String>): Intent {
|
||||||
return super.createIntent(context, input).apply {
|
return super.createIntent(context, input).apply {
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
@@ -178,34 +175,17 @@ class ExternalFileHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateDocument(private val typeString: String) : ActivityResultContracts.CreateDocument() {
|
class CreateDocument(typeString: String) : ActivityResultContracts.CreateDocument(typeString) {
|
||||||
override fun createIntent(context: Context, input: String): Intent {
|
override fun createIntent(context: Context, input: String): Intent {
|
||||||
return super.createIntent(context, input).apply {
|
return super.createIntent(context, input).apply {
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
type = typeString
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val TAG = "OpenFileHelper"
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,23 +4,24 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.utils.getBinaryDir
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
|
||||||
abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
||||||
|
|
||||||
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
|
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
|
||||||
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
||||||
protected var mDatabase: Database? = null
|
protected var mDatabase: ContextualDatabase? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
mDatabaseTaskProvider = DatabaseTaskProvider(this)
|
mDatabaseTaskProvider = DatabaseTaskProvider(this, showDatabaseDialog())
|
||||||
|
|
||||||
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
||||||
val databaseWasReloaded = database?.wasReloaded == true
|
val databaseWasReloaded = database?.wasReloaded == true
|
||||||
@@ -36,14 +37,25 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
protected open fun showDatabaseDialog(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
mDatabaseTaskProvider?.destroy()
|
||||||
|
mDatabaseTaskProvider = null
|
||||||
|
mDatabase = null
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
mDatabase = database
|
mDatabase = database
|
||||||
mDatabaseViewModel.defineDatabase(database)
|
mDatabaseViewModel.defineDatabase(database)
|
||||||
// optional method implementation
|
// optional method implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result
|
result: ActionRunnable.Result
|
||||||
) {
|
) {
|
||||||
@@ -51,21 +63,25 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
|||||||
// optional method implementation
|
// optional method implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createDatabase(databaseUri: Uri,
|
fun createDatabase(
|
||||||
mainCredential: MainCredential) {
|
databaseUri: Uri,
|
||||||
|
mainCredential: MainCredential
|
||||||
|
) {
|
||||||
mDatabaseTaskProvider?.startDatabaseCreate(databaseUri, mainCredential)
|
mDatabaseTaskProvider?.startDatabaseCreate(databaseUri, mainCredential)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadDatabase(databaseUri: Uri,
|
fun loadDatabase(
|
||||||
mainCredential: MainCredential,
|
databaseUri: Uri,
|
||||||
readOnly: Boolean,
|
mainCredential: MainCredential,
|
||||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
readOnly: Boolean,
|
||||||
fixDuplicateUuid: Boolean) {
|
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||||
|
fixDuplicateUuid: Boolean
|
||||||
|
) {
|
||||||
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEncryptDatabase, fixDuplicateUuid)
|
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEncryptDatabase, fixDuplicateUuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun closeDatabase() {
|
protected fun closeDatabase() {
|
||||||
mDatabase?.clearAndClose(this)
|
mDatabase?.clearAndClose(this.getBinaryDir())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -35,22 +36,25 @@ import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
|||||||
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
|
||||||
import com.kunzisoft.keepass.model.GroupInfo
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.*
|
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||||
|
import com.kunzisoft.keepass.utils.LockReceiver
|
||||||
|
import com.kunzisoft.keepass.utils.closeDatabase
|
||||||
|
import com.kunzisoft.keepass.utils.registerLockReceiver
|
||||||
|
import com.kunzisoft.keepass.utils.unregisterLockReceiver
|
||||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
import com.kunzisoft.keepass.viewmodels.NodesViewModel
|
import com.kunzisoft.keepass.viewmodels.NodesViewModel
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
||||||
PasswordEncodingDialogFragment.Listener {
|
PasswordEncodingDialogFragment.Listener {
|
||||||
@@ -66,8 +70,6 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
protected var mMergeDataAllowed: Boolean = false
|
protected var mMergeDataAllowed: Boolean = false
|
||||||
private var mAutoSaveEnable: Boolean = true
|
private var mAutoSaveEnable: Boolean = true
|
||||||
|
|
||||||
protected var mIconDrawableFactory: IconDrawableFactory? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -89,8 +91,8 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
mDatabaseTaskProvider?.startDatabaseSave(save)
|
mDatabaseTaskProvider?.startDatabaseSave(save)
|
||||||
}
|
}
|
||||||
|
|
||||||
mDatabaseViewModel.mergeDatabase.observe(this) {
|
mDatabaseViewModel.mergeDatabase.observe(this) { save ->
|
||||||
mDatabaseTaskProvider?.startDatabaseMerge()
|
mDatabaseTaskProvider?.startDatabaseMerge(save)
|
||||||
}
|
}
|
||||||
|
|
||||||
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
|
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
|
||||||
@@ -166,7 +168,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
super.onDatabaseRetrieved(database)
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
// End activity if database not loaded
|
// End activity if database not loaded
|
||||||
@@ -186,8 +188,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
mLockReceiver = LockReceiver {
|
mLockReceiver = LockReceiver {
|
||||||
mDatabase = null
|
mDatabase = null
|
||||||
closeDatabase(database)
|
closeDatabase(database)
|
||||||
if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null)
|
UI_VISIBLE_DURING_LOCK = UI_VISIBLE
|
||||||
LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE
|
|
||||||
mExitLock = true
|
mExitLock = true
|
||||||
closeOptionsMenu()
|
closeOptionsMenu()
|
||||||
finish()
|
finish()
|
||||||
@@ -206,16 +207,24 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
mDatabaseReadOnly = database.isReadOnly
|
mDatabaseReadOnly = database.isReadOnly
|
||||||
mMergeDataAllowed = database.isMergeDataAllowed()
|
mMergeDataAllowed = database.isMergeDataAllowed()
|
||||||
mIconDrawableFactory = database.iconDrawableFactory
|
|
||||||
|
|
||||||
checkRegister()
|
checkRegister()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun finish() {
|
||||||
|
// To fix weird crash
|
||||||
|
try {
|
||||||
|
super.finish()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to finish the activity", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
abstract fun viewToInvalidateTimeout(): View?
|
abstract fun viewToInvalidateTimeout(): View?
|
||||||
|
|
||||||
override fun onDatabaseActionFinished(
|
override fun onDatabaseActionFinished(
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result
|
result: ActionRunnable.Result
|
||||||
) {
|
) {
|
||||||
@@ -226,6 +235,9 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
// Reload the current activity
|
// Reload the current activity
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
reloadActivity()
|
reloadActivity()
|
||||||
|
if (actionTask == DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK) {
|
||||||
|
Toast.makeText(this, R.string.merge_success, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.showActionErrorIfNeeded(result)
|
this.showActionErrorIfNeeded(result)
|
||||||
finish()
|
finish()
|
||||||
@@ -234,15 +246,19 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPasswordEncodingValidateListener(databaseUri: Uri?,
|
override fun onPasswordEncodingValidateListener(
|
||||||
mainCredential: MainCredential) {
|
databaseUri: Uri?,
|
||||||
|
mainCredential: MainCredential
|
||||||
|
) {
|
||||||
assignDatabasePassword(databaseUri, mainCredential)
|
assignDatabasePassword(databaseUri, mainCredential)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assignDatabasePassword(databaseUri: Uri?,
|
private fun assignDatabasePassword(
|
||||||
mainCredential: MainCredential) {
|
databaseUri: Uri?,
|
||||||
|
mainCredential: MainCredential
|
||||||
|
) {
|
||||||
if (databaseUri != null) {
|
if (databaseUri != null) {
|
||||||
mDatabaseTaskProvider?.startDatabaseAssignPassword(databaseUri, mainCredential)
|
mDatabaseTaskProvider?.startDatabaseAssignCredential(databaseUri, mainCredential)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +266,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
database.fileUri?.let { databaseUri ->
|
database.fileUri?.let { databaseUri ->
|
||||||
// Show the progress dialog now or after dialog confirmation
|
// Show the progress dialog now or after dialog confirmation
|
||||||
if (database.validatePasswordEncoding(mainCredential)) {
|
if (database.isValidCredential(mainCredential.toMasterCredential(contentResolver))) {
|
||||||
assignDatabasePassword(databaseUri, mainCredential)
|
assignDatabasePassword(databaseUri, mainCredential)
|
||||||
} else {
|
} else {
|
||||||
PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
|
PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
|
||||||
@@ -269,11 +285,11 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun mergeDatabase() {
|
fun mergeDatabase() {
|
||||||
mDatabaseTaskProvider?.startDatabaseMerge()
|
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
|
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
|
||||||
mDatabaseTaskProvider?.startDatabaseMerge(uri, mainCredential)
|
mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable, uri, mainCredential)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reloadDatabase() {
|
fun reloadDatabase() {
|
||||||
@@ -302,7 +318,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
mDatabaseTaskProvider?.startDatabaseMoveNodes(nodesToMove, newParent, mAutoSaveEnable)
|
mDatabaseTaskProvider?.startDatabaseMoveNodes(nodesToMove, newParent, mAutoSaveEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun eachNodeRecyclable(database: Database, nodes: List<Node>): Boolean {
|
private fun eachNodeRecyclable(database: ContextualDatabase, nodes: List<Node>): Boolean {
|
||||||
return nodes.find { node ->
|
return nodes.find { node ->
|
||||||
var cannotRecycle = true
|
var cannotRecycle = true
|
||||||
if (node is Entry) {
|
if (node is Entry) {
|
||||||
@@ -318,7 +334,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
// If recycle bin enabled, ensure it exists
|
// If recycle bin enabled, ensure it exists
|
||||||
if (database.isRecycleBinEnabled) {
|
if (database.isRecycleBinEnabled) {
|
||||||
database.ensureRecycleBinExists(resources)
|
database.ensureRecycleBinExists(resources.getString(R.string.recycle_bin))
|
||||||
}
|
}
|
||||||
|
|
||||||
// If recycle bin enabled and not in recycle bin, move in recycle bin
|
// If recycle bin enabled and not in recycle bin, move in recycle bin
|
||||||
@@ -401,7 +417,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
|
|
||||||
LOCKING_ACTIVITY_UI_VISIBLE = true
|
UI_VISIBLE = true
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun checkTimeAndLockIfTimeoutOrResetTimeout(action: (() -> Unit)? = null) {
|
protected fun checkTimeAndLockIfTimeoutOrResetTimeout(action: (() -> Unit)? = null) {
|
||||||
@@ -416,7 +432,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
LOCKING_ACTIVITY_UI_VISIBLE = false
|
UI_VISIBLE = false
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
@@ -433,9 +449,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
protected fun lockAndExit() {
|
protected fun lockAndExit() {
|
||||||
// Ask confirmation if modification not saved
|
// Ask confirmation if modification not saved
|
||||||
if (mDatabase?.isReadOnly == false
|
if (mDatabase?.dataModifiedSinceLastLoading == true) {
|
||||||
&& mDatabase?.dataModifiedSinceLastLoading == true
|
|
||||||
&& !PreferencesUtil.isAutoSaveDatabaseEnabled(this)) {
|
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setMessage(R.string.discard_changes)
|
.setMessage(R.string.discard_changes)
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
@@ -452,14 +466,14 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
mDatabase?.loaded ?: false)
|
mDatabase?.loaded ?: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onDatabaseBackPressed() {
|
||||||
if (mTimeoutEnable) {
|
if (mTimeoutEnable) {
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
|
||||||
mDatabase?.loaded == true) {
|
mDatabase?.loaded == true) {
|
||||||
super.onBackPressed()
|
super.onDatabaseBackPressed()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
super.onBackPressed()
|
super.onDatabaseBackPressed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,8 +484,8 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
|
||||||
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
const val TIMEOUT_ENABLE_KEY_DEFAULT = true
|
||||||
|
|
||||||
private var LOCKING_ACTIVITY_UI_VISIBLE = false
|
var UI_VISIBLE: Boolean = false
|
||||||
var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null
|
var UI_VISIBLE_DURING_LOCK: Boolean = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,25 +494,33 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
|
|||||||
*/
|
*/
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
fun View.resetAppTimeoutWhenViewTouchedOrFocused(context: Context, databaseLoaded: Boolean?) {
|
fun View.resetAppTimeoutWhenViewTouchedOrFocused(context: Context, databaseLoaded: Boolean?) {
|
||||||
// Log.d(DatabaseLockActivity.TAG, "View prepared to reset app timeout")
|
try {
|
||||||
setOnTouchListener { _, event ->
|
// Log.d(DatabaseLockActivity.TAG, "View prepared to reset app timeout")
|
||||||
when (event.action) {
|
setOnTouchListener { _, event ->
|
||||||
MotionEvent.ACTION_DOWN -> {
|
when (event.action) {
|
||||||
// Log.d(DatabaseLockActivity.TAG, "View touched, try to reset app timeout")
|
MotionEvent.ACTION_DOWN -> {
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context,
|
// Log.d(DatabaseLockActivity.TAG, "View touched, try to reset app timeout")
|
||||||
databaseLoaded ?: false)
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(
|
||||||
|
context,
|
||||||
|
databaseLoaded ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
setOnFocusChangeListener { _, _ ->
|
||||||
|
// Log.d(DatabaseLockActivity.TAG, "View focused, try to reset app timeout")
|
||||||
|
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(
|
||||||
|
context,
|
||||||
|
databaseLoaded ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (this is ViewGroup) {
|
||||||
|
for (i in 0..childCount) {
|
||||||
|
getChildAt(i)?.resetAppTimeoutWhenViewTouchedOrFocused(context, databaseLoaded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
} catch (e: Exception) {
|
||||||
}
|
Log.e("AppTimeout", "Unable to reset app timeout", e)
|
||||||
setOnFocusChangeListener { _, _ ->
|
|
||||||
// Log.d(DatabaseLockActivity.TAG, "View focused, try to reset app timeout")
|
|
||||||
TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context,
|
|
||||||
databaseLoaded ?: false)
|
|
||||||
}
|
|
||||||
if (this is ViewGroup) {
|
|
||||||
for (i in 0..childCount) {
|
|
||||||
getChildAt(i)?.resetAppTimeoutWhenViewTouchedOrFocused(context, databaseLoaded)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
package com.kunzisoft.keepass.activities.legacy
|
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 androidx.activity.OnBackPressedCallback
|
||||||
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.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.ToolbarSpecial
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,20 +21,22 @@ 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
|
||||||
|
|
||||||
private var mSpecialModeView: SpecialModeView? = null
|
private var mToolbarSpecial: ToolbarSpecial? = null
|
||||||
|
|
||||||
override fun onBackPressed() {
|
open fun onDatabaseBackPressed() {
|
||||||
if (mSpecialMode != SpecialMode.DEFAULT)
|
if (mSpecialMode != SpecialMode.DEFAULT)
|
||||||
onCancelSpecialMode()
|
onCancelSpecialMode()
|
||||||
else
|
else
|
||||||
super.onBackPressed()
|
onRegularBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To call the regular onBackPressed() method in special mode
|
* To call the regular onBackPressed() method in special mode
|
||||||
*/
|
*/
|
||||||
protected fun onRegularBackPressed() {
|
protected fun onRegularBackPressed() {
|
||||||
super.onBackPressed()
|
// Do not call onBackPressedDispatcher.onBackPressed() to avoid loop
|
||||||
|
// Calling onBackPressed() is now deprecated, directly finish the activity
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,7 +75,7 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
open fun onCancelSpecialMode() {
|
open fun onCancelSpecialMode() {
|
||||||
if (isIntentSender()) {
|
if (isIntentSender()) {
|
||||||
// To get the app caller, only for IntentSender
|
// To get the app caller, only for IntentSender
|
||||||
super.onBackPressed()
|
onRegularBackPressed()
|
||||||
} else {
|
} else {
|
||||||
EntrySelectionHelper.removeModesFromIntent(intent)
|
EntrySelectionHelper.removeModesFromIntent(intent)
|
||||||
EntrySelectionHelper.removeInfoFromIntent(intent)
|
EntrySelectionHelper.removeInfoFromIntent(intent)
|
||||||
@@ -88,7 +88,7 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
protected fun backToTheAppCaller() {
|
protected fun backToTheAppCaller() {
|
||||||
if (isIntentSender()) {
|
if (isIntentSender()) {
|
||||||
// To get the app caller, only for IntentSender
|
// To get the app caller, only for IntentSender
|
||||||
super.onBackPressed()
|
onRegularBackPressed()
|
||||||
} else {
|
} else {
|
||||||
backToTheMainAppAndFinish()
|
backToTheMainAppAndFinish()
|
||||||
}
|
}
|
||||||
@@ -96,14 +96,19 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
|
|
||||||
private fun backToTheMainAppAndFinish() {
|
private fun backToTheMainAppAndFinish() {
|
||||||
// To move the app in background and return to the main app
|
// To move the app in background and return to the main app
|
||||||
// Not visible as opened with FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
|
||||||
moveTaskToBack(true)
|
moveTaskToBack(true)
|
||||||
// Not finish() to prevent service kill
|
// Not using FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS or finish() because kills the service
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
onDatabaseBackPressed()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
||||||
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
||||||
}
|
}
|
||||||
@@ -117,8 +122,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
||||||
|
|
||||||
// To show the selection mode
|
// To show the selection mode
|
||||||
mSpecialModeView = findViewById(R.id.special_mode_view)
|
mToolbarSpecial = findViewById(R.id.special_mode_view)
|
||||||
mSpecialModeView?.apply {
|
mToolbarSpecial?.apply {
|
||||||
// Populate title
|
// Populate title
|
||||||
val selectionModeStringId = when (mSpecialMode) {
|
val selectionModeStringId = when (mSpecialMode) {
|
||||||
SpecialMode.DEFAULT, // Not important because hidden
|
SpecialMode.DEFAULT, // Not important because hidden
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.kunzisoft.keepass.activities.legacy
|
package com.kunzisoft.keepass.activities.legacy
|
||||||
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
|
||||||
interface DatabaseRetrieval {
|
interface DatabaseRetrieval {
|
||||||
fun onDatabaseRetrieved(database: Database?)
|
fun onDatabaseRetrieved(database: ContextualDatabase?)
|
||||||
fun onDatabaseActionFinished(database: Database,
|
fun onDatabaseActionFinished(database: ContextualDatabase,
|
||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result)
|
result: ActionRunnable.Result)
|
||||||
}
|
}
|
||||||
@@ -23,6 +23,7 @@ import android.content.Context
|
|||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.StyleRes
|
import androidx.annotation.StyleRes
|
||||||
|
import com.google.android.material.color.DynamicColors
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ object Stylish {
|
|||||||
* @param context Context to retrieve the theme preference
|
* @param context Context to retrieve the theme preference
|
||||||
*/
|
*/
|
||||||
fun load(context: Context) {
|
fun load(context: Context) {
|
||||||
Log.d(Stylish::class.java.name, "Attatching to " + context.packageName)
|
Log.d(Stylish::class.java.name, "Attaching to " + context.packageName)
|
||||||
try {
|
try {
|
||||||
themeString = PreferencesUtil.getStyle(context)
|
themeString = PreferencesUtil.getStyle(context)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -46,7 +47,7 @@ object Stylish {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retrieveEquivalentSystemStyle(context: Context, styleString: String): String {
|
fun retrieveEquivalentSystemStyle(context: Context, styleString: String?): String {
|
||||||
val systemNightMode = when (PreferencesUtil.getStyleBrightness(context)) {
|
val systemNightMode = when (PreferencesUtil.getStyleBrightness(context)) {
|
||||||
context.getString(R.string.list_style_brightness_light) -> false
|
context.getString(R.string.list_style_brightness_light) -> false
|
||||||
context.getString(R.string.list_style_brightness_night) -> true
|
context.getString(R.string.list_style_brightness_night) -> true
|
||||||
@@ -58,14 +59,21 @@ object Stylish {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return if (systemNightMode) {
|
return if (systemNightMode) {
|
||||||
retrieveEquivalentNightStyle(context, styleString)
|
retrieveEquivalentNightStyle(
|
||||||
|
context,
|
||||||
|
styleString ?: context.getString(R.string.list_style_name_night)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
retrieveEquivalentLightStyle(context, styleString)
|
retrieveEquivalentLightStyle(
|
||||||
|
context,
|
||||||
|
styleString ?: context.getString(R.string.list_style_name_light)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retrieveEquivalentLightStyle(context: Context, styleString: String): String {
|
fun retrieveEquivalentLightStyle(context: Context, styleString: String): String {
|
||||||
return when (styleString) {
|
return when (styleString) {
|
||||||
|
context.getString(R.string.list_style_name_dynamic_night) -> context.getString(R.string.list_style_name_dynamic_light)
|
||||||
context.getString(R.string.list_style_name_night) -> context.getString(R.string.list_style_name_light)
|
context.getString(R.string.list_style_name_night) -> context.getString(R.string.list_style_name_light)
|
||||||
context.getString(R.string.list_style_name_black) -> context.getString(R.string.list_style_name_white)
|
context.getString(R.string.list_style_name_black) -> context.getString(R.string.list_style_name_white)
|
||||||
context.getString(R.string.list_style_name_dark) -> context.getString(R.string.list_style_name_clear)
|
context.getString(R.string.list_style_name_dark) -> context.getString(R.string.list_style_name_clear)
|
||||||
@@ -80,6 +88,7 @@ object Stylish {
|
|||||||
|
|
||||||
private fun retrieveEquivalentNightStyle(context: Context, styleString: String): String {
|
private fun retrieveEquivalentNightStyle(context: Context, styleString: String): String {
|
||||||
return when (styleString) {
|
return when (styleString) {
|
||||||
|
context.getString(R.string.list_style_name_dynamic_light) -> context.getString(R.string.list_style_name_dynamic_night)
|
||||||
context.getString(R.string.list_style_name_light) -> context.getString(R.string.list_style_name_night)
|
context.getString(R.string.list_style_name_light) -> context.getString(R.string.list_style_name_night)
|
||||||
context.getString(R.string.list_style_name_white) -> context.getString(R.string.list_style_name_black)
|
context.getString(R.string.list_style_name_white) -> context.getString(R.string.list_style_name_black)
|
||||||
context.getString(R.string.list_style_name_clear) -> context.getString(R.string.list_style_name_dark)
|
context.getString(R.string.list_style_name_clear) -> context.getString(R.string.list_style_name_dark)
|
||||||
@@ -104,6 +113,13 @@ object Stylish {
|
|||||||
PreferencesUtil.setStyle(context, styleString)
|
PreferencesUtil.setStyle(context, styleString)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isDynamic(context: Context): Boolean {
|
||||||
|
return DynamicColors.isDynamicColorAvailable() && (
|
||||||
|
themeString == context.getString(R.string.list_style_name_dynamic_night)
|
||||||
|
|| themeString == context.getString(R.string.list_style_name_dynamic_light)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function that returns the current id of the style selected in the preference
|
* Function that returns the current id of the style selected in the preference
|
||||||
* @param context Context to retrieve the id
|
* @param context Context to retrieve the id
|
||||||
@@ -111,7 +127,7 @@ object Stylish {
|
|||||||
*/
|
*/
|
||||||
@StyleRes
|
@StyleRes
|
||||||
fun getThemeId(context: Context): Int {
|
fun getThemeId(context: Context): Int {
|
||||||
return when (retrieveEquivalentSystemStyle(context, themeString ?: context.getString(R.string.list_style_name_light))) {
|
return when (retrieveEquivalentSystemStyle(context, themeString)) {
|
||||||
context.getString(R.string.list_style_name_night) -> R.style.KeepassDXStyle_Night
|
context.getString(R.string.list_style_name_night) -> R.style.KeepassDXStyle_Night
|
||||||
context.getString(R.string.list_style_name_white) -> R.style.KeepassDXStyle_White
|
context.getString(R.string.list_style_name_white) -> R.style.KeepassDXStyle_White
|
||||||
context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black
|
context.getString(R.string.list_style_name_black) -> R.style.KeepassDXStyle_Black
|
||||||
@@ -127,6 +143,8 @@ object Stylish {
|
|||||||
context.getString(R.string.list_style_name_reply_night) -> R.style.KeepassDXStyle_Reply_Night
|
context.getString(R.string.list_style_name_reply_night) -> R.style.KeepassDXStyle_Reply_Night
|
||||||
context.getString(R.string.list_style_name_purple) -> R.style.KeepassDXStyle_Purple
|
context.getString(R.string.list_style_name_purple) -> R.style.KeepassDXStyle_Purple
|
||||||
context.getString(R.string.list_style_name_purple_dark) -> R.style.KeepassDXStyle_Purple_Dark
|
context.getString(R.string.list_style_name_purple_dark) -> R.style.KeepassDXStyle_Purple_Dark
|
||||||
|
context.getString(R.string.list_style_name_dynamic_light) -> R.style.KeepassDXStyle_Light_Dynamic
|
||||||
|
context.getString(R.string.list_style_name_dynamic_night) -> R.style.KeepassDXStyle_Night_Dynamic
|
||||||
else -> R.style.KeepassDXStyle_Light
|
else -> R.style.KeepassDXStyle_Light
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,23 @@ package com.kunzisoft.keepass.activities.stylish
|
|||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.WindowManager
|
import android.view.View
|
||||||
|
import android.view.View.GONE
|
||||||
|
import android.view.View.VISIBLE
|
||||||
|
import android.view.WindowManager.LayoutParams.FLAG_SECURE
|
||||||
import androidx.annotation.StyleRes
|
import androidx.annotation.StyleRes
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.google.android.material.color.DynamicColors
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_PREFERENCE_CHANGED
|
import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_PREFERENCE_CHANGED
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
|
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
|
||||||
@@ -69,7 +78,18 @@ abstract class StylishActivity : AppCompatActivity() {
|
|||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
finish()
|
finish()
|
||||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
@Suppress("DEPRECATION")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
overrideActivityTransition(
|
||||||
|
OVERRIDE_TRANSITION_OPEN,
|
||||||
|
android.R.anim.fade_in,
|
||||||
|
android.R.anim.fade_out
|
||||||
|
)
|
||||||
|
else
|
||||||
|
overridePendingTransition(
|
||||||
|
android.R.anim.fade_in,
|
||||||
|
android.R.anim.fade_out
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -77,12 +97,33 @@ abstract class StylishActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
customStyle = applyCustomStyle()
|
customStyle = applyCustomStyle()
|
||||||
if (customStyle) {
|
if (customStyle) {
|
||||||
|
// Preconfigured themes
|
||||||
this.themeId = Stylish.getThemeId(this)
|
this.themeId = Stylish.getThemeId(this)
|
||||||
setTheme(themeId)
|
setTheme(themeId)
|
||||||
|
if (Stylish.isDynamic(this)) {
|
||||||
|
// Material You theme
|
||||||
|
DynamicColors.applyToActivityIfAvailable(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
.registerOnSharedPreferenceChangeListener(onScreenshotModePrefListener)
|
||||||
|
}
|
||||||
|
private val onScreenshotModePrefListener = OnSharedPreferenceChangeListener { _, key ->
|
||||||
|
if (key != getString(R.string.enable_screenshot_mode_key)) return@OnSharedPreferenceChangeListener
|
||||||
|
|
||||||
|
setScreenshotMode(PreferencesUtil.isScreenshotModeEnabled(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setScreenshotMode(isEnabled: Boolean) {
|
||||||
|
findViewById<View>(R.id.screenshot_mode_banner)?.visibility = if (isEnabled) VISIBLE else GONE
|
||||||
|
|
||||||
// Several gingerbread devices have problems with FLAG_SECURE
|
// Several gingerbread devices have problems with FLAG_SECURE
|
||||||
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
|
if (isEnabled) {
|
||||||
|
window.clearFlags(FLAG_SECURE)
|
||||||
|
} else {
|
||||||
|
window.setFlags(FLAG_SECURE, FLAG_SECURE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -94,6 +135,7 @@ abstract class StylishActivity : AppCompatActivity() {
|
|||||||
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
|
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
|
||||||
recreateActivity()
|
recreateActivity()
|
||||||
}
|
}
|
||||||
|
setScreenshotMode(PreferencesUtil.isScreenshotModeEnabled(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun recreateActivity() {
|
private fun recreateActivity() {
|
||||||
|
|||||||
@@ -1,99 +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.stylish
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.annotation.StyleRes
|
|
||||||
import androidx.appcompat.view.ContextThemeWrapper
|
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
|
|
||||||
abstract class StylishFragment : Fragment() {
|
|
||||||
|
|
||||||
@StyleRes
|
|
||||||
protected var themeId: Int = 0
|
|
||||||
protected var contextThemed: Context? = null
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
this.themeId = Stylish.getThemeId(context)
|
|
||||||
contextThemed = ContextThemeWrapper(context, themeId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
||||||
// To fix status bar color
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
val window = requireActivity().window
|
|
||||||
val defaultColor = Color.BLACK
|
|
||||||
val windowInset = WindowInsetsControllerCompat(window, window.decorView)
|
|
||||||
try {
|
|
||||||
val taStatusBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.statusBarColor))
|
|
||||||
window.statusBarColor = taStatusBarColor?.getColor(0, defaultColor) ?: defaultColor
|
|
||||||
taStatusBarColor?.recycle()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to retrieve theme : status bar color", e)
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
try {
|
|
||||||
val taWindowStatusLight = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightStatusBar))
|
|
||||||
windowInset.isAppearanceLightStatusBars = taWindowStatusLight
|
|
||||||
?.getBoolean(0, false) == true
|
|
||||||
taWindowStatusLight?.recycle()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to retrieve theme : window light status bar", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
val taNavigationBarColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.navigationBarColor))
|
|
||||||
window.navigationBarColor = taNavigationBarColor?.getColor(0, defaultColor) ?: defaultColor
|
|
||||||
taNavigationBarColor?.recycle()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to retrieve theme : navigation bar color", e)
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
|
||||||
try {
|
|
||||||
val taWindowLightNavigationBar = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.windowLightNavigationBar))
|
|
||||||
windowInset.isAppearanceLightNavigationBars = taWindowLightNavigationBar
|
|
||||||
?.getBoolean(0, false) == true
|
|
||||||
taWindowLightNavigationBar?.recycle()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to retrieve theme : navigation light navigation bar", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.onCreateView(inflater, container, savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetach() {
|
|
||||||
contextThemed = null
|
|
||||||
super.onDetach()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val TAG = StylishFragment::class.java.simpleName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,7 @@ import android.widget.ImageView
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
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
|
||||||
@@ -17,7 +18,7 @@ import com.kunzisoft.keepass.icons.IconDrawableFactory
|
|||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.strikeOut
|
import com.kunzisoft.keepass.view.strikeOut
|
||||||
|
|
||||||
class BreadcrumbAdapter(val context: Context)
|
class BreadcrumbAdapter(val context: Context, val database: Database?)
|
||||||
: RecyclerView.Adapter<BreadcrumbAdapter.BreadcrumbGroupViewHolder>() {
|
: RecyclerView.Adapter<BreadcrumbAdapter.BreadcrumbGroupViewHolder>() {
|
||||||
|
|
||||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
@@ -31,6 +32,8 @@ class BreadcrumbAdapter(val context: Context)
|
|||||||
var onItemClickListener: ((item: Node, position: Int)->Unit)? = null
|
var onItemClickListener: ((item: Node, position: Int)->Unit)? = null
|
||||||
var onLongItemClickListener: ((item: Node, position: Int)->Unit)? = null
|
var onLongItemClickListener: ((item: Node, position: Int)->Unit)? = null
|
||||||
|
|
||||||
|
private var mNodeFilter: NodeFilter = NodeFilter(context, database)
|
||||||
|
|
||||||
private var mShowNumberEntries = false
|
private var mShowNumberEntries = false
|
||||||
private var mShowUUID = false
|
private var mShowUUID = false
|
||||||
private var mIconColor: Int = 0
|
private var mIconColor: Int = 0
|
||||||
@@ -39,10 +42,10 @@ class BreadcrumbAdapter(val context: Context)
|
|||||||
mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
|
mShowNumberEntries = PreferencesUtil.showNumberEntries(context)
|
||||||
mShowUUID = PreferencesUtil.showUUID(context)
|
mShowUUID = PreferencesUtil.showUUID(context)
|
||||||
|
|
||||||
// Retrieve the textColor to tint the icon
|
// Retrieve the color to tint the icon
|
||||||
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.textColorInverse))
|
val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnSurface))
|
||||||
mIconColor = taTextColor.getColor(0, Color.WHITE)
|
mIconColor = taIconColor.getColor(0, Color.WHITE)
|
||||||
taTextColor.recycle()
|
taIconColor.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
@@ -71,7 +74,7 @@ class BreadcrumbAdapter(val context: Context)
|
|||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BreadcrumbGroupViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BreadcrumbGroupViewHolder {
|
||||||
return BreadcrumbGroupViewHolder(inflater.inflate(
|
return BreadcrumbGroupViewHolder(inflater.inflate(
|
||||||
when (viewType) {
|
when (viewType) {
|
||||||
0 -> R.layout.item_group
|
0 -> R.layout.item_breadcrumb_important
|
||||||
else -> R.layout.item_breadcrumb
|
else -> R.layout.item_breadcrumb
|
||||||
}, parent, false)
|
}, parent, false)
|
||||||
)
|
)
|
||||||
@@ -112,8 +115,10 @@ class BreadcrumbAdapter(val context: Context)
|
|||||||
|
|
||||||
holder.groupNumbersView?.apply {
|
holder.groupNumbersView?.apply {
|
||||||
if (mShowNumberEntries) {
|
if (mShowNumberEntries) {
|
||||||
group.refreshNumberOfChildEntries(Group.ChildFilter.getDefaults(context))
|
text = group.getNumberOfChildEntries(
|
||||||
text = group.numberOfChildEntries.toString()
|
mNodeFilter.recursiveNumberOfEntries,
|
||||||
|
mNodeFilter.filter
|
||||||
|
).toString()
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
visibility = View.GONE
|
visibility = View.GONE
|
||||||
|
|||||||
@@ -27,16 +27,18 @@ import android.util.TypedValue
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.ProgressBar
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.android.material.progressindicator.CircularProgressIndicator
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.ImageViewerActivity
|
import com.kunzisoft.keepass.activities.ImageViewerActivity
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
|
import com.kunzisoft.keepass.database.helper.getLocalizedName
|
||||||
import com.kunzisoft.keepass.model.AttachmentState
|
import com.kunzisoft.keepass.model.AttachmentState
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.model.StreamDirection
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
|
import com.kunzisoft.keepass.services.AttachmentFileNotificationService.Companion.FILE_PROGRESSION_MAX
|
||||||
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||||
import com.kunzisoft.keepass.view.expand
|
import com.kunzisoft.keepass.view.expand
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
@@ -45,7 +47,7 @@ import kotlin.math.max
|
|||||||
class EntryAttachmentsItemsAdapter(context: Context)
|
class EntryAttachmentsItemsAdapter(context: Context)
|
||||||
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
|
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
|
||||||
|
|
||||||
var database: Database? = null
|
var database: ContextualDatabase? = null
|
||||||
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
|
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
|
||||||
var onBinaryPreviewLoaded: ((item: EntryAttachmentState) -> Unit)? = null
|
var onBinaryPreviewLoaded: ((item: EntryAttachmentState) -> Unit)? = null
|
||||||
|
|
||||||
@@ -130,13 +132,14 @@ class EntryAttachmentsItemsAdapter(context: Context)
|
|||||||
holder.binaryFileSize.text = Formatter.formatFileSize(context, size)
|
holder.binaryFileSize.text = Formatter.formatFileSize(context, size)
|
||||||
holder.binaryFileCompression.apply {
|
holder.binaryFileCompression.apply {
|
||||||
if (entryAttachmentState.attachment.binaryData.isCompressed) {
|
if (entryAttachmentState.attachment.binaryData.isCompressed) {
|
||||||
text = CompressionAlgorithm.GZip.getName(context.resources)
|
text = CompressionAlgorithm.GZIP.getLocalizedName(context.resources)
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
text = ""
|
text = ""
|
||||||
visibility = View.GONE
|
visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
holder.binaryFileProgress.max = FILE_PROGRESSION_MAX
|
||||||
when (entryAttachmentState.streamDirection) {
|
when (entryAttachmentState.streamDirection) {
|
||||||
StreamDirection.UPLOAD -> {
|
StreamDirection.UPLOAD -> {
|
||||||
holder.binaryFileProgressIcon.isActivated = true
|
holder.binaryFileProgressIcon.isActivated = true
|
||||||
@@ -181,7 +184,7 @@ class EntryAttachmentsItemsAdapter(context: Context)
|
|||||||
AttachmentState.START,
|
AttachmentState.START,
|
||||||
AttachmentState.IN_PROGRESS -> View.VISIBLE
|
AttachmentState.IN_PROGRESS -> View.VISIBLE
|
||||||
}
|
}
|
||||||
progress = entryAttachmentState.downloadProgression
|
setProgressCompat(entryAttachmentState.downloadProgression, true)
|
||||||
}
|
}
|
||||||
holder.binaryFileInfo.setOnClickListener {
|
holder.binaryFileInfo.setOnClickListener {
|
||||||
onItemClickListener?.invoke(entryAttachmentState)
|
onItemClickListener?.invoke(entryAttachmentState)
|
||||||
@@ -200,7 +203,7 @@ class EntryAttachmentsItemsAdapter(context: Context)
|
|||||||
var binaryFileCompression: TextView = itemView.findViewById(R.id.item_attachment_compression)
|
var binaryFileCompression: TextView = itemView.findViewById(R.id.item_attachment_compression)
|
||||||
var binaryFileProgressContainer: View = itemView.findViewById(R.id.item_attachment_progress_container)
|
var binaryFileProgressContainer: View = itemView.findViewById(R.id.item_attachment_progress_container)
|
||||||
var binaryFileProgressIcon: ImageView = itemView.findViewById(R.id.item_attachment_icon)
|
var binaryFileProgressIcon: ImageView = itemView.findViewById(R.id.item_attachment_icon)
|
||||||
var binaryFileProgress: ProgressBar = itemView.findViewById(R.id.item_attachment_progress)
|
var binaryFileProgress: CircularProgressIndicator = itemView.findViewById(R.id.item_attachment_progress)
|
||||||
var binaryFileDeleteButton: View = itemView.findViewById(R.id.item_attachment_delete_button)
|
var binaryFileDeleteButton: View = itemView.findViewById(R.id.item_attachment_delete_button)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import android.widget.TextView
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
||||||
|
|
||||||
class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHistoryAdapter.EntryHistoryViewHolder>() {
|
class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter<EntryHistoryAdapter.EntryHistoryViewHolder>() {
|
||||||
|
|
||||||
|
|||||||
@@ -23,12 +23,14 @@ import android.content.Context
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.PorterDuff
|
import android.graphics.PorterDuff
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.*
|
import android.widget.CompoundButton
|
||||||
import androidx.annotation.ColorInt
|
import android.widget.EditText
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.ViewSwitcher
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SortedList
|
import androidx.recyclerview.widget.SortedList
|
||||||
import androidx.recyclerview.widget.SortedListAdapterCallback
|
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||||
@@ -84,20 +86,6 @@ class FileDatabaseHistoryAdapter(context: Context)
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ColorInt
|
|
||||||
private val defaultColor: Int
|
|
||||||
@ColorInt
|
|
||||||
private val warningColor: Int
|
|
||||||
|
|
||||||
init {
|
|
||||||
val typedValue = TypedValue()
|
|
||||||
val theme = context.theme
|
|
||||||
theme.resolveAttribute(R.attr.colorAccent, typedValue, true)
|
|
||||||
warningColor = typedValue.data
|
|
||||||
theme.resolveAttribute(android.R.attr.textColorHintInverse, typedValue, true)
|
|
||||||
defaultColor = typedValue.data
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileDatabaseHistoryViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileDatabaseHistoryViewHolder {
|
||||||
val view = inflater.inflate(R.layout.item_file_info, parent, false)
|
val view = inflater.inflate(R.layout.item_file_info, parent, false)
|
||||||
return FileDatabaseHistoryViewHolder(view)
|
return FileDatabaseHistoryViewHolder(view)
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
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.settings.PreferencesUtil
|
||||||
|
|
||||||
|
class NodeFilter(
|
||||||
|
context: Context,
|
||||||
|
var database: Database? = null
|
||||||
|
) {
|
||||||
|
var recursiveNumberOfEntries = PreferencesUtil.recursiveNumberEntries(context)
|
||||||
|
private set
|
||||||
|
private var showExpired = PreferencesUtil.showExpiredEntries(context)
|
||||||
|
private var showTemplate = PreferencesUtil.showTemplates(context)
|
||||||
|
|
||||||
|
val filter: (Node) -> Boolean = { node ->
|
||||||
|
when (node) {
|
||||||
|
is Entry -> {
|
||||||
|
node.entryKDB?.isMetaStream() != true
|
||||||
|
}
|
||||||
|
is Group -> {
|
||||||
|
showTemplate || database?.templatesGroup != node
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
} && (showExpired || !node.isCurrentlyExpires)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,21 +20,24 @@
|
|||||||
package com.kunzisoft.keepass.adapters
|
package com.kunzisoft.keepass.adapters
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SortedList
|
import androidx.recyclerview.widget.SortedList
|
||||||
import androidx.recyclerview.widget.SortedListAdapterCallback
|
import androidx.recyclerview.widget.SortedListAdapterCallback
|
||||||
import com.google.android.material.progressindicator.CircularProgressIndicator
|
import com.google.android.material.progressindicator.CircularProgressIndicator
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
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
|
||||||
@@ -42,26 +45,29 @@ import com.kunzisoft.keepass.database.element.node.Node
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
|
import com.kunzisoft.keepass.database.helper.getLocalizedName
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.otp.OtpType
|
import com.kunzisoft.keepass.otp.OtpType
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
import com.kunzisoft.keepass.view.setTextSize
|
import com.kunzisoft.keepass.view.setTextSize
|
||||||
import com.kunzisoft.keepass.view.strikeOut
|
import com.kunzisoft.keepass.view.strikeOut
|
||||||
import java.util.*
|
import java.util.LinkedList
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create node list adapter with contextMenu or not
|
* Create node list adapter with contextMenu or not
|
||||||
* @param context Context to use
|
* @param context Context to use
|
||||||
*/
|
*/
|
||||||
class NodesAdapter (private val context: Context,
|
class NodesAdapter (
|
||||||
private val database: Database)
|
private val context: Context,
|
||||||
: RecyclerView.Adapter<NodesAdapter.NodeViewHolder>() {
|
private val database: ContextualDatabase
|
||||||
|
) : RecyclerView.Adapter<NodesAdapter.NodeViewHolder>() {
|
||||||
|
|
||||||
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
|
private var mNodeComparator: Comparator<NodeVersionedInterface<Group>>? = null
|
||||||
private val mNodeSortedListCallback: NodeSortedListCallback
|
private val mNodeSortedListCallback: NodeSortedListCallback
|
||||||
private val mNodeSortedList: SortedList<Node>
|
private val mNodeSortedList: SortedList<Node>
|
||||||
private val mInflater: LayoutInflater = LayoutInflater.from(context)
|
private val mInflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
|
private val mNodeFilter: NodeFilter = NodeFilter(context, database)
|
||||||
|
|
||||||
private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type
|
private var mCalculateViewTypeTextSize = Array(2) { true } // number of view type
|
||||||
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
private var mTextSizeUnit: Int = TypedValue.COMPLEX_UNIT_PX
|
||||||
@@ -78,7 +84,7 @@ class NodesAdapter (private val context: Context,
|
|||||||
private var mShowNumberEntries: Boolean = true
|
private var mShowNumberEntries: Boolean = true
|
||||||
private var mShowOTP: Boolean = false
|
private var mShowOTP: Boolean = false
|
||||||
private var mShowUUID: Boolean = false
|
private var mShowUUID: Boolean = false
|
||||||
private var mEntryFilters = arrayOf<Group.ChildFilter>()
|
private var mNodeFilters: NodeFilter? = null
|
||||||
private var mOldVirtualGroup = false
|
private var mOldVirtualGroup = false
|
||||||
private var mVirtualGroup = false
|
private var mVirtualGroup = false
|
||||||
|
|
||||||
@@ -86,6 +92,8 @@ class NodesAdapter (private val context: Context,
|
|||||||
private var mNodeClickCallback: NodeClickCallback? = null
|
private var mNodeClickCallback: NodeClickCallback? = null
|
||||||
private var mClipboardHelper = ClipboardHelper(context)
|
private var mClipboardHelper = ClipboardHelper(context)
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
private val mColorSurfaceContainer: Int
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private val mTextColorPrimary: Int
|
private val mTextColorPrimary: Int
|
||||||
@ColorInt
|
@ColorInt
|
||||||
@@ -93,9 +101,9 @@ class NodesAdapter (private val context: Context,
|
|||||||
@ColorInt
|
@ColorInt
|
||||||
private val mTextColorSecondary: Int
|
private val mTextColorSecondary: Int
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private val mColorAccentLight: Int
|
private val mColorSecondary: Int
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private val mColorOnAccentColor: Int
|
private val mColorOnSecondary: Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the adapter contains or not any element
|
* Determine if the adapter contains or not any element
|
||||||
@@ -112,26 +120,29 @@ class NodesAdapter (private val context: Context,
|
|||||||
this.mNodeSortedListCallback = NodeSortedListCallback()
|
this.mNodeSortedListCallback = NodeSortedListCallback()
|
||||||
this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback)
|
this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback)
|
||||||
|
|
||||||
|
val taColorSurfaceContainer = context.obtainStyledAttributes(intArrayOf(R.attr.colorSurfaceContainer))
|
||||||
|
this.mColorSurfaceContainer = taColorSurfaceContainer.getColor(0, Color.BLACK)
|
||||||
|
taColorSurfaceContainer.recycle()
|
||||||
// Retrieve the color to tint the icon
|
// Retrieve the color to tint the icon
|
||||||
val taTextColorPrimary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
val taTextColorPrimary = context.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
||||||
this.mTextColorPrimary = taTextColorPrimary.getColor(0, Color.BLACK)
|
this.mTextColorPrimary = taTextColorPrimary.getColor(0, Color.BLACK)
|
||||||
taTextColorPrimary.recycle()
|
taTextColorPrimary.recycle()
|
||||||
// To get text color
|
// To get text color
|
||||||
val taTextColor = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
val taTextColor = context.obtainStyledAttributes(intArrayOf(android.R.attr.textColor))
|
||||||
this.mTextColor = taTextColor.getColor(0, Color.BLACK)
|
this.mTextColor = taTextColor.getColor(0, Color.BLACK)
|
||||||
taTextColor.recycle()
|
taTextColor.recycle()
|
||||||
// To get text color secondary
|
// To get text color secondary
|
||||||
val taTextColorSecondary = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorSecondary))
|
val taTextColorSecondary = context.obtainStyledAttributes(intArrayOf(android.R.attr.textColorSecondary))
|
||||||
this.mTextColorSecondary = taTextColorSecondary.getColor(0, Color.BLACK)
|
this.mTextColorSecondary = taTextColorSecondary.getColor(0, Color.BLACK)
|
||||||
taTextColorSecondary.recycle()
|
taTextColorSecondary.recycle()
|
||||||
// To get background color for selection
|
// To get background color for selection
|
||||||
val taColorAccentLight = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccentLight))
|
val taColorSecondary = context.obtainStyledAttributes(intArrayOf(R.attr.colorSecondary))
|
||||||
this.mColorAccentLight = taColorAccentLight.getColor(0, Color.GRAY)
|
this.mColorSecondary = taColorSecondary.getColor(0, Color.GRAY)
|
||||||
taColorAccentLight.recycle()
|
taColorSecondary.recycle()
|
||||||
// To get text color for selection
|
// To get text color for selection
|
||||||
val taColorOnAccentColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnAccentColor))
|
val taColorOnSecondary = context.obtainStyledAttributes(intArrayOf(R.attr.colorOnSecondary))
|
||||||
this.mColorOnAccentColor = taColorOnAccentColor.getColor(0, Color.WHITE)
|
this.mColorOnSecondary = taColorOnSecondary.getColor(0, Color.WHITE)
|
||||||
taColorOnAccentColor.recycle()
|
taColorOnSecondary.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assignPreferences() {
|
private fun assignPreferences() {
|
||||||
@@ -152,7 +163,7 @@ class NodesAdapter (private val context: Context,
|
|||||||
this.mShowOTP = PreferencesUtil.showOTPToken(context)
|
this.mShowOTP = PreferencesUtil.showOTPToken(context)
|
||||||
this.mShowUUID = PreferencesUtil.showUUID(context)
|
this.mShowUUID = PreferencesUtil.showUUID(context)
|
||||||
|
|
||||||
this.mEntryFilters = Group.ChildFilter.getDefaults(context)
|
this.mNodeFilters = NodeFilter(context, database)
|
||||||
|
|
||||||
// Reinit textSize for all view type
|
// Reinit textSize for all view type
|
||||||
mCalculateViewTypeTextSize.forEachIndexed { index, _ -> mCalculateViewTypeTextSize[index] = true }
|
mCalculateViewTypeTextSize.forEachIndexed { index, _ -> mCalculateViewTypeTextSize[index] = true }
|
||||||
@@ -165,7 +176,7 @@ class NodesAdapter (private val context: Context,
|
|||||||
mOldVirtualGroup = mVirtualGroup
|
mOldVirtualGroup = mVirtualGroup
|
||||||
mVirtualGroup = group.isVirtual
|
mVirtualGroup = group.isVirtual
|
||||||
assignPreferences()
|
assignPreferences()
|
||||||
mNodeSortedList.replaceAll(group.getFilteredChildren(mEntryFilters))
|
mNodeSortedList.replaceAll(group.getChildren(mNodeFilter.filter))
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class NodeSortedListCallback: SortedListAdapterCallback<Node>(this) {
|
private inner class NodeSortedListCallback: SortedListAdapterCallback<Node>(this) {
|
||||||
@@ -186,6 +197,7 @@ class NodesAdapter (private val context: Context,
|
|||||||
&& oldItem.containsAttachment() == newItem.containsAttachment()
|
&& oldItem.containsAttachment() == newItem.containsAttachment()
|
||||||
} else if (oldItem is Group && newItem is Group) {
|
} else if (oldItem is Group && newItem is Group) {
|
||||||
typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries
|
typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries
|
||||||
|
&& oldItem.recursiveNumberOfChildEntries == newItem.recursiveNumberOfChildEntries
|
||||||
&& oldItem.notes == newItem.notes
|
&& oldItem.notes == newItem.notes
|
||||||
}
|
}
|
||||||
return typeContentTheSame
|
return typeContentTheSame
|
||||||
@@ -193,6 +205,11 @@ class NodesAdapter (private val context: Context,
|
|||||||
&& oldItem.type == newItem.type
|
&& oldItem.type == newItem.type
|
||||||
&& oldItem.title == newItem.title
|
&& oldItem.title == newItem.title
|
||||||
&& oldItem.icon == newItem.icon
|
&& oldItem.icon == newItem.icon
|
||||||
|
&& oldItem.creationTime == newItem.creationTime
|
||||||
|
&& oldItem.lastModificationTime == newItem.lastModificationTime
|
||||||
|
&& oldItem.lastAccessTime == newItem.lastAccessTime
|
||||||
|
&& oldItem.expiryTime == newItem.expiryTime
|
||||||
|
&& oldItem.expires == newItem.expires
|
||||||
&& oldItem.isCurrentlyExpires == newItem.isCurrentlyExpires
|
&& oldItem.isCurrentlyExpires == newItem.isCurrentlyExpires
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,10 +393,10 @@ class NodesAdapter (private val context: Context,
|
|||||||
|
|
||||||
// Assign icon colors
|
// Assign icon colors
|
||||||
var iconColor = if (holder.container.isSelected)
|
var iconColor = if (holder.container.isSelected)
|
||||||
mColorOnAccentColor
|
mColorOnSecondary
|
||||||
else when (subNode.type) {
|
else when (subNode.type) {
|
||||||
Type.GROUP -> mTextColorPrimary
|
Type.GROUP -> mTextColor
|
||||||
Type.ENTRY -> mTextColor
|
Type.ENTRY -> mColorSecondary
|
||||||
}
|
}
|
||||||
|
|
||||||
// Specific elements for entry
|
// Specific elements for entry
|
||||||
@@ -424,16 +441,8 @@ class NodesAdapter (private val context: Context,
|
|||||||
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
// Assign colors
|
// Assign colors
|
||||||
val backgroundColor = if (mShowEntryColors) entry.backgroundColor else null
|
assignBackgroundColor(holder.container, entry)
|
||||||
if (!holder.container.isSelected) {
|
assignBackgroundColor(holder.otpContainer, entry)
|
||||||
if (backgroundColor != null) {
|
|
||||||
holder.container.setBackgroundColor(backgroundColor)
|
|
||||||
} else {
|
|
||||||
holder.container.setBackgroundColor(Color.TRANSPARENT)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
holder.container.setBackgroundColor(mColorAccentLight)
|
|
||||||
}
|
|
||||||
val foregroundColor = if (mShowEntryColors) entry.foregroundColor else null
|
val foregroundColor = if (mShowEntryColors) entry.foregroundColor else null
|
||||||
if (!holder.container.isSelected) {
|
if (!holder.container.isSelected) {
|
||||||
if (foregroundColor != null) {
|
if (foregroundColor != null) {
|
||||||
@@ -453,12 +462,12 @@ class NodesAdapter (private val context: Context,
|
|||||||
holder.meta.setTextColor(mTextColor)
|
holder.meta.setTextColor(mTextColor)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
holder.text.setTextColor(mColorOnAccentColor)
|
holder.text.setTextColor(mColorOnSecondary)
|
||||||
holder.subText?.setTextColor(mColorOnAccentColor)
|
holder.subText?.setTextColor(mColorOnSecondary)
|
||||||
holder.otpToken?.setTextColor(mColorOnAccentColor)
|
holder.otpToken?.setTextColor(mColorOnSecondary)
|
||||||
holder.otpProgress?.setIndicatorColor(mColorOnAccentColor)
|
holder.otpProgress?.setIndicatorColor(mColorOnSecondary)
|
||||||
holder.attachmentIcon?.setColorFilter(mColorOnAccentColor)
|
holder.attachmentIcon?.setColorFilter(mColorOnSecondary)
|
||||||
holder.meta.setTextColor(mColorOnAccentColor)
|
holder.meta.setTextColor(mColorOnSecondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
database.stopManageEntry(entry)
|
database.stopManageEntry(entry)
|
||||||
@@ -469,7 +478,10 @@ class NodesAdapter (private val context: Context,
|
|||||||
if (mShowNumberEntries) {
|
if (mShowNumberEntries) {
|
||||||
holder.numberChildren?.apply {
|
holder.numberChildren?.apply {
|
||||||
text = (subNode as Group)
|
text = (subNode as Group)
|
||||||
.numberOfChildEntries
|
.getNumberOfChildEntries(
|
||||||
|
mNodeFilter.recursiveNumberOfEntries,
|
||||||
|
mNodeFilter.filter
|
||||||
|
)
|
||||||
.toString()
|
.toString()
|
||||||
setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier)
|
setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier)
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
@@ -518,20 +530,41 @@ class NodesAdapter (private val context: Context,
|
|||||||
holder?.otpToken?.apply {
|
holder?.otpToken?.apply {
|
||||||
text = otpElement?.tokenString
|
text = otpElement?.tokenString
|
||||||
setTextSize(mTextSizeUnit, mOtpTokenTextDefaultDimension, mPrefSizeMultiplier)
|
setTextSize(mTextSizeUnit, mOtpTokenTextDefaultDimension, mPrefSizeMultiplier)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
|
textDirection = View.TEXT_DIRECTION_LTR
|
||||||
|
}
|
||||||
}
|
}
|
||||||
holder?.otpContainer?.setOnClickListener {
|
holder?.otpContainer?.setOnClickListener {
|
||||||
otpElement?.token?.let { token ->
|
otpElement?.token?.let { token ->
|
||||||
Toast.makeText(
|
try {
|
||||||
context,
|
mClipboardHelper.copyToClipboard(
|
||||||
context.getString(R.string.copy_field,
|
TemplateField.getLocalizedName(context, TemplateField.LABEL_TOKEN),
|
||||||
TemplateField.getLocalizedName(context, TemplateField.LABEL_TOKEN)),
|
token,
|
||||||
Toast.LENGTH_LONG
|
true
|
||||||
).show()
|
)
|
||||||
mClipboardHelper.copyToClipboard(token)
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to copy the OTP token", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun assignBackgroundColor(view: View?, entry: Entry) {
|
||||||
|
view?.let {
|
||||||
|
ViewCompat.setBackgroundTintList(
|
||||||
|
view,
|
||||||
|
ColorStateList.valueOf(
|
||||||
|
if (!view.isSelected) {
|
||||||
|
(if (mShowEntryColors) entry.backgroundColor else null)
|
||||||
|
?: mColorSurfaceContainer
|
||||||
|
} else {
|
||||||
|
mColorSecondary
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class OtpRunnable(val view: View?): Runnable {
|
class OtpRunnable(val view: View?): Runnable {
|
||||||
|
|
||||||
var action: (() -> Unit)? = null
|
var action: (() -> Unit)? = null
|
||||||
@@ -561,8 +594,8 @@ class NodesAdapter (private val context: Context,
|
|||||||
* Callback listener to redefine to do an action when a node is click
|
* Callback listener to redefine to do an action when a node is click
|
||||||
*/
|
*/
|
||||||
interface NodeClickCallback {
|
interface NodeClickCallback {
|
||||||
fun onNodeClick(database: Database, node: Node)
|
fun onNodeClick(database: ContextualDatabase, node: Node)
|
||||||
fun onNodeLongClick(database: Database, node: Node): Boolean
|
fun onNodeLongClick(database: ContextualDatabase, node: Node): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ import android.widget.TextView
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.template.Template
|
import com.kunzisoft.keepass.database.element.template.Template
|
||||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
|
import com.kunzisoft.keepass.database.helper.getLocalizedName
|
||||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
|
|
||||||
|
|
||||||
class TemplatesSelectorAdapter(
|
class TemplatesSelectorAdapter(
|
||||||
context: Context,
|
private val context: Context,
|
||||||
private var templates: List<Template>): BaseAdapter() {
|
private var templates: List<Template>): BaseAdapter() {
|
||||||
|
|
||||||
var iconDrawableFactory: IconDrawableFactory? = null
|
var iconDrawableFactory: IconDrawableFactory? = null
|
||||||
@@ -35,7 +36,9 @@ class TemplatesSelectorAdapter(
|
|||||||
var templateView = convertView
|
var templateView = convertView
|
||||||
if (templateView == null) {
|
if (templateView == null) {
|
||||||
holder = TemplateSelectorViewHolder()
|
holder = TemplateSelectorViewHolder()
|
||||||
templateView = inflater.inflate(R.layout.item_template, parent, false)
|
templateView = inflater
|
||||||
|
.cloneInContext(context)
|
||||||
|
.inflate(R.layout.item_template, parent, false)
|
||||||
holder.background = templateView?.findViewById(R.id.template_background)
|
holder.background = templateView?.findViewById(R.id.template_background)
|
||||||
holder.icon = templateView?.findViewById(R.id.template_image)
|
holder.icon = templateView?.findViewById(R.id.template_image)
|
||||||
holder.name = templateView?.findViewById(R.id.template_name)
|
holder.name = templateView?.findViewById(R.id.template_name)
|
||||||
@@ -74,4 +77,4 @@ class TemplatesSelectorAdapter(
|
|||||||
var icon: ImageView? = null
|
var icon: ImageView? = null
|
||||||
var name: TextView? = null
|
var name: TextView? = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,15 @@ import androidx.room.Database
|
|||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.room.AutoMigration
|
||||||
|
|
||||||
@Database(version = 1, entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class])
|
@Database(
|
||||||
|
version = 2,
|
||||||
|
entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class],
|
||||||
|
autoMigrations = [
|
||||||
|
AutoMigration (from = 1, to = 2)
|
||||||
|
]
|
||||||
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|
||||||
abstract fun fileDatabaseHistoryDao(): FileDatabaseHistoryDao
|
abstract fun fileDatabaseHistoryDao(): FileDatabaseHistoryDao
|
||||||
|
|||||||
@@ -19,24 +19,28 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.app.database
|
package com.kunzisoft.keepass.app.database
|
||||||
|
|
||||||
import android.content.*
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.ServiceConnection
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryData.Companion.BASE64_FLAG
|
||||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||||
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.IOActionTask
|
||||||
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
||||||
import java.util.*
|
import java.util.LinkedList
|
||||||
|
|
||||||
class CipherDatabaseAction(context: Context) {
|
class CipherDatabaseAction(context: Context) {
|
||||||
|
|
||||||
private val applicationContext = context.applicationContext
|
private val applicationContext = context.applicationContext
|
||||||
private val cipherDatabaseDao =
|
private val cipherDatabaseDao =
|
||||||
AppDatabase
|
AppDatabase.getDatabase(applicationContext).cipherDatabaseDao()
|
||||||
.getDatabase(applicationContext)
|
|
||||||
.cipherDatabaseDao()
|
|
||||||
|
|
||||||
// Temp DAO to easily remove content if object no longer in memory
|
// Temp DAO to easily remove content if object no longer in memory
|
||||||
private var useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
|
private var useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
|
||||||
@@ -66,9 +70,11 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
private fun attachService(performedAction: () -> Unit) {
|
private fun attachService(performedAction: () -> Unit) {
|
||||||
applicationContext.registerReceiver(mAdvancedUnlockBroadcastReceiver, IntentFilter().apply {
|
ContextCompat.registerReceiver(applicationContext, mAdvancedUnlockBroadcastReceiver,
|
||||||
addAction(AdvancedUnlockNotificationService.REMOVE_ADVANCED_UNLOCK_KEY_ACTION)
|
IntentFilter().apply {
|
||||||
})
|
addAction(AdvancedUnlockNotificationService.REMOVE_ADVANCED_UNLOCK_KEY_ACTION)
|
||||||
|
}, ContextCompat.RECEIVER_EXPORTED
|
||||||
|
)
|
||||||
|
|
||||||
mServiceConnection = object : ServiceConnection {
|
mServiceConnection = object : ServiceConnection {
|
||||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||||
@@ -83,7 +89,7 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
try {
|
try {
|
||||||
AdvancedUnlockNotificationService.bindService(applicationContext,
|
AdvancedUnlockNotificationService.bindService(applicationContext,
|
||||||
mServiceConnection!!,
|
mServiceConnection!!,
|
||||||
Context.BIND_AUTO_CREATE)
|
Context.BIND_AUTO_CREATE)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to start cipher action", e)
|
Log.e(TAG, "Unable to start cipher action", e)
|
||||||
performedAction.invoke()
|
performedAction.invoke()
|
||||||
@@ -94,7 +100,7 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
private fun detachService() {
|
private fun detachService() {
|
||||||
try {
|
try {
|
||||||
applicationContext.unregisterReceiver(mAdvancedUnlockBroadcastReceiver)
|
applicationContext.unregisterReceiver(mAdvancedUnlockBroadcastReceiver)
|
||||||
} catch (e: Exception) {}
|
} catch (_: Exception) {}
|
||||||
|
|
||||||
mServiceConnection?.let {
|
mServiceConnection?.let {
|
||||||
AdvancedUnlockNotificationService.unbindService(applicationContext, it)
|
AdvancedUnlockNotificationService.unbindService(applicationContext, it)
|
||||||
@@ -136,11 +142,11 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
|
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
|
||||||
this.encryptedValue = Base64.decode(
|
this.encryptedValue = Base64.decode(
|
||||||
cipherDatabaseEntity.encryptedValue,
|
cipherDatabaseEntity.encryptedValue,
|
||||||
Base64.NO_WRAP
|
BASE64_FLAG
|
||||||
)
|
)
|
||||||
this.specParameters = Base64.decode(
|
this.specParameters = Base64.decode(
|
||||||
cipherDatabaseEntity.specParameters,
|
cipherDatabaseEntity.specParameters,
|
||||||
Base64.NO_WRAP
|
BASE64_FLAG
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,8 +154,9 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())?.let { cipherDatabaseEntity ->
|
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
|
||||||
|
?.let { cipherDatabaseEntity ->
|
||||||
CipherEncryptDatabase().apply {
|
CipherEncryptDatabase().apply {
|
||||||
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
|
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
|
||||||
this.encryptedValue = Base64.decode(
|
this.encryptedValue = Base64.decode(
|
||||||
@@ -162,18 +169,30 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cipherDatabaseResultListener.invoke(it)
|
cipherDatabaseResultListener.invoke(it)
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun containsCipherDatabase(databaseUri: Uri,
|
fun containsCipherDatabase(databaseUri: Uri?,
|
||||||
contains: (Boolean) -> Unit) {
|
contains: (Boolean) -> Unit) {
|
||||||
getCipherDatabase(databaseUri) {
|
if (databaseUri == null) {
|
||||||
contains.invoke(it != null)
|
contains.invoke(false)
|
||||||
|
} else {
|
||||||
|
getCipherDatabase(databaseUri) {
|
||||||
|
contains.invoke(it != null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetCipherParameters(databaseUri: Uri?) {
|
||||||
|
containsCipherDatabase(databaseUri) { contains ->
|
||||||
|
if (contains) {
|
||||||
|
mBinder?.resetTimer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,8 +202,8 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
|
|
||||||
val cipherDatabaseEntity = CipherDatabaseEntity(
|
val cipherDatabaseEntity = CipherDatabaseEntity(
|
||||||
databaseUri.toString(),
|
databaseUri.toString(),
|
||||||
Base64.encodeToString(cipherEncryptDatabase.encryptedValue, Base64.NO_WRAP),
|
Base64.encodeToString(cipherEncryptDatabase.encryptedValue, BASE64_FLAG),
|
||||||
Base64.encodeToString(cipherEncryptDatabase.specParameters, Base64.NO_WRAP),
|
Base64.encodeToString(cipherEncryptDatabase.specParameters, BASE64_FLAG),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (useTempDao) {
|
if (useTempDao) {
|
||||||
@@ -222,12 +241,12 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
|
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cipherDatabaseResultListener?.invoke()
|
cipherDatabaseResultListener?.invoke()
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,9 +259,9 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
}
|
}
|
||||||
// To erase the residues
|
// To erase the residues
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
cipherDatabaseDao.deleteAll()
|
cipherDatabaseDao.deleteAll()
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
// Unbind
|
// Unbind
|
||||||
removeAllDataAndDetach()
|
removeAllDataAndDetach()
|
||||||
@@ -251,4 +270,4 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction) {
|
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction) {
|
||||||
private val TAG = CipherDatabaseAction::class.java.name
|
private val TAG = CipherDatabaseAction::class.java.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
*
|
*
|
||||||
* This file is part of KeePassDX.
|
* This file is part of KeePassDX.
|
||||||
*
|
*
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
@@ -22,204 +22,225 @@ package com.kunzisoft.keepass.app.database
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
import com.kunzisoft.keepass.model.DatabaseFile
|
import com.kunzisoft.keepass.model.DatabaseFile
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.IOActionTask
|
||||||
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.decodeUri
|
||||||
|
import com.kunzisoft.keepass.utils.parseUri
|
||||||
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
|
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
|
||||||
|
|
||||||
class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||||
|
|
||||||
private val databaseFileHistoryDao =
|
private val databaseFileHistoryDao =
|
||||||
AppDatabase
|
AppDatabase.getDatabase(applicationContext).fileDatabaseHistoryDao()
|
||||||
.getDatabase(applicationContext)
|
|
||||||
.fileDatabaseHistoryDao()
|
|
||||||
|
|
||||||
fun getDatabaseFile(databaseUri: Uri,
|
fun getDatabaseFile(databaseUri: Uri,
|
||||||
databaseFileResult: (DatabaseFile?) -> Unit) {
|
databaseFileResult: (DatabaseFile?) -> Unit) {
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
val fileDatabaseHistoryEntity = databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
val fileDatabaseHistoryEntity =
|
||||||
val fileDatabaseInfo = FileDatabaseInfo(applicationContext, databaseUri)
|
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||||
DatabaseFile(
|
val fileDatabaseInfo = FileDatabaseInfo(
|
||||||
databaseUri,
|
applicationContext,
|
||||||
UriUtil.parse(fileDatabaseHistoryEntity?.keyFileUri),
|
databaseUri)
|
||||||
UriUtil.decode(fileDatabaseHistoryEntity?.databaseUri),
|
DatabaseFile(
|
||||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias ?: ""),
|
databaseUri,
|
||||||
fileDatabaseInfo.exists,
|
fileDatabaseHistoryEntity?.keyFileUri?.parseUri(),
|
||||||
fileDatabaseInfo.getLastModificationString(),
|
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity?.hardwareKey),
|
||||||
fileDatabaseInfo.getSizeString()
|
fileDatabaseHistoryEntity?.databaseUri?.decodeUri(),
|
||||||
)
|
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias
|
||||||
},
|
?: ""),
|
||||||
{
|
fileDatabaseInfo.exists,
|
||||||
databaseFileResult.invoke(it)
|
fileDatabaseInfo.getLastModificationString(),
|
||||||
}
|
fileDatabaseInfo.getSizeString()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
databaseFileResult.invoke(it)
|
||||||
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getKeyFileUriByDatabaseUri(databaseUri: Uri,
|
fun getKeyFileUriByDatabaseUri(databaseUri: Uri,
|
||||||
keyFileUriResultListener: (Uri?) -> Unit) {
|
keyFileUriResultListener: (Uri?) -> Unit) {
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
it?.let { fileHistoryEntity ->
|
it?.let { fileHistoryEntity ->
|
||||||
fileHistoryEntity.keyFileUri?.let { keyFileUri ->
|
fileHistoryEntity.keyFileUri?.let { keyFileUri ->
|
||||||
keyFileUriResultListener.invoke(UriUtil.parse(keyFileUri))
|
keyFileUriResultListener.invoke(keyFileUri.parseUri())
|
||||||
}
|
}
|
||||||
} ?: keyFileUriResultListener.invoke(null)
|
} ?: keyFileUriResultListener.invoke(null)
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDatabaseFileList(databaseFileListResult: (List<DatabaseFile>) -> Unit) {
|
fun getDatabaseFileList(databaseFileListResult: (List<DatabaseFile>) -> Unit) {
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
val hideBrokenLocations = PreferencesUtil.hideBrokenLocations(applicationContext)
|
val hideBrokenLocations =
|
||||||
// Show only uri accessible
|
PreferencesUtil.hideBrokenLocations(
|
||||||
val databaseFileListLoaded = ArrayList<DatabaseFile>()
|
applicationContext)
|
||||||
databaseFileHistoryDao.getAll().forEach { fileDatabaseHistoryEntity ->
|
// Show only uri accessible
|
||||||
val fileDatabaseInfo = FileDatabaseInfo(applicationContext, fileDatabaseHistoryEntity.databaseUri)
|
val databaseFileListLoaded = ArrayList<DatabaseFile>()
|
||||||
if (hideBrokenLocations && fileDatabaseInfo.exists
|
databaseFileHistoryDao.getAll().forEach { fileDatabaseHistoryEntity ->
|
||||||
|| !hideBrokenLocations) {
|
val fileDatabaseInfo = FileDatabaseInfo(
|
||||||
databaseFileListLoaded.add(
|
applicationContext,
|
||||||
DatabaseFile(
|
fileDatabaseHistoryEntity.databaseUri)
|
||||||
UriUtil.parse(fileDatabaseHistoryEntity.databaseUri),
|
if (hideBrokenLocations && fileDatabaseInfo.exists
|
||||||
UriUtil.parse(fileDatabaseHistoryEntity.keyFileUri),
|
|| !hideBrokenLocations
|
||||||
UriUtil.decode(fileDatabaseHistoryEntity.databaseUri),
|
) {
|
||||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
|
databaseFileListLoaded.add(
|
||||||
fileDatabaseInfo.exists,
|
DatabaseFile(
|
||||||
fileDatabaseInfo.getLastModificationString(),
|
fileDatabaseHistoryEntity.databaseUri.parseUri(),
|
||||||
fileDatabaseInfo.getSizeString()
|
fileDatabaseHistoryEntity.keyFileUri?.parseUri(),
|
||||||
)
|
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity.hardwareKey),
|
||||||
|
fileDatabaseHistoryEntity.databaseUri.decodeUri(),
|
||||||
|
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
|
||||||
|
fileDatabaseInfo.exists,
|
||||||
|
fileDatabaseInfo.getLastModificationString(),
|
||||||
|
fileDatabaseInfo.getSizeString()
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
}
|
|
||||||
databaseFileListLoaded
|
|
||||||
},
|
|
||||||
{
|
|
||||||
databaseFileList ->
|
|
||||||
databaseFileList?.let {
|
|
||||||
databaseFileListResult.invoke(it)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
databaseFileListLoaded
|
||||||
|
},
|
||||||
|
{ databaseFileList ->
|
||||||
|
databaseFileList?.let {
|
||||||
|
databaseFileListResult.invoke(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null,
|
fun addOrUpdateDatabaseUri(databaseUri: Uri,
|
||||||
|
keyFileUri: Uri? = null,
|
||||||
|
hardwareKey: HardwareKey? = null,
|
||||||
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
|
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
|
||||||
addOrUpdateDatabaseFile(DatabaseFile(
|
addOrUpdateDatabaseFile(DatabaseFile(
|
||||||
databaseUri,
|
databaseUri,
|
||||||
keyFileUri
|
keyFileUri,
|
||||||
|
hardwareKey
|
||||||
), databaseFileAddedOrUpdatedResult)
|
), databaseFileAddedOrUpdatedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addOrUpdateDatabaseFile(databaseFileToAddOrUpdate: DatabaseFile,
|
fun addOrUpdateDatabaseFile(databaseFileToAddOrUpdate: DatabaseFile,
|
||||||
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
|
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileToAddOrUpdate.databaseUri?.let { databaseUri ->
|
databaseFileToAddOrUpdate.databaseUri?.let { databaseUri ->
|
||||||
// Try to get info in database first
|
// Try to get info in database first
|
||||||
val fileDatabaseHistoryRetrieve = databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
val fileDatabaseHistoryRetrieve =
|
||||||
|
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||||
|
|
||||||
// Complete alias if not exists
|
// Complete alias if not exists
|
||||||
val fileDatabaseHistory = FileDatabaseHistoryEntity(
|
val fileDatabaseHistory =
|
||||||
databaseUri.toString(),
|
FileDatabaseHistoryEntity(
|
||||||
databaseFileToAddOrUpdate.databaseAlias
|
databaseUri.toString(),
|
||||||
?: fileDatabaseHistoryRetrieve?.databaseAlias
|
databaseFileToAddOrUpdate.databaseAlias
|
||||||
?: "",
|
?: fileDatabaseHistoryRetrieve?.databaseAlias
|
||||||
databaseFileToAddOrUpdate.keyFileUri?.toString(),
|
?: "",
|
||||||
System.currentTimeMillis()
|
databaseFileToAddOrUpdate.keyFileUri?.toString(),
|
||||||
|
databaseFileToAddOrUpdate.hardwareKey?.value,
|
||||||
|
System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
|
|
||||||
// Update values if history element not yet in the database
|
// Update values if history element not yet in the database
|
||||||
try {
|
try {
|
||||||
if (fileDatabaseHistoryRetrieve == null) {
|
if (fileDatabaseHistoryRetrieve == null) {
|
||||||
databaseFileHistoryDao.add(fileDatabaseHistory)
|
databaseFileHistoryDao.add(fileDatabaseHistory)
|
||||||
} else {
|
} else {
|
||||||
databaseFileHistoryDao.update(fileDatabaseHistory)
|
databaseFileHistoryDao.update(fileDatabaseHistory)
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to add or update database history", e)
|
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
val fileDatabaseInfo = FileDatabaseInfo(applicationContext,
|
Log.e(TAG, "Unable to add or update database history", e)
|
||||||
fileDatabaseHistory.databaseUri)
|
|
||||||
DatabaseFile(
|
|
||||||
UriUtil.parse(fileDatabaseHistory.databaseUri),
|
|
||||||
UriUtil.parse(fileDatabaseHistory.keyFileUri),
|
|
||||||
UriUtil.decode(fileDatabaseHistory.databaseUri),
|
|
||||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
|
|
||||||
fileDatabaseInfo.exists,
|
|
||||||
fileDatabaseInfo.getLastModificationString(),
|
|
||||||
fileDatabaseInfo.getSizeString()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
val fileDatabaseInfo =
|
||||||
databaseFileAddedOrUpdatedResult?.invoke(it)
|
FileDatabaseInfo(applicationContext,
|
||||||
|
fileDatabaseHistory.databaseUri)
|
||||||
|
DatabaseFile(
|
||||||
|
fileDatabaseHistory.databaseUri.parseUri(),
|
||||||
|
fileDatabaseHistory.keyFileUri?.parseUri(),
|
||||||
|
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
|
||||||
|
fileDatabaseHistory.databaseUri.decodeUri(),
|
||||||
|
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
|
||||||
|
fileDatabaseInfo.exists,
|
||||||
|
fileDatabaseInfo.getLastModificationString(),
|
||||||
|
fileDatabaseInfo.getSizeString()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
databaseFileAddedOrUpdatedResult?.invoke(it)
|
||||||
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteDatabaseFile(databaseFileToDelete: DatabaseFile,
|
fun deleteDatabaseFile(databaseFileToDelete: DatabaseFile,
|
||||||
databaseFileDeletedResult: (DatabaseFile?) -> Unit) {
|
databaseFileDeletedResult: (DatabaseFile?) -> Unit) {
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileToDelete.databaseUri?.let { databaseUri ->
|
databaseFileToDelete.databaseUri?.let { databaseUri ->
|
||||||
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())?.let { fileDatabaseHistory ->
|
databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
|
||||||
|
?.let { fileDatabaseHistory ->
|
||||||
val returnValue = databaseFileHistoryDao.delete(fileDatabaseHistory)
|
val returnValue = databaseFileHistoryDao.delete(fileDatabaseHistory)
|
||||||
if (returnValue > 0) {
|
if (returnValue > 0) {
|
||||||
DatabaseFile(
|
DatabaseFile(
|
||||||
UriUtil.parse(fileDatabaseHistory.databaseUri),
|
fileDatabaseHistory.databaseUri.parseUri(),
|
||||||
UriUtil.parse(fileDatabaseHistory.keyFileUri),
|
fileDatabaseHistory.keyFileUri?.parseUri(),
|
||||||
UriUtil.decode(fileDatabaseHistory.databaseUri),
|
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
|
||||||
databaseFileToDelete.databaseAlias
|
fileDatabaseHistory.databaseUri.decodeUri(),
|
||||||
|
databaseFileToDelete.databaseAlias
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
databaseFileDeletedResult.invoke(it)
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
databaseFileDeletedResult.invoke(it)
|
||||||
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteKeyFileByDatabaseUri(databaseUri: Uri,
|
fun deleteKeyFileByDatabaseUri(databaseUri: Uri,
|
||||||
result: (() ->Unit)? = null) {
|
result: (() ->Unit)? = null) {
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
|
databaseFileHistoryDao.deleteKeyFileByDatabaseUri(databaseUri.toString())
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
result?.invoke()
|
result?.invoke()
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAllKeyFiles(result: (() ->Unit)? = null) {
|
fun deleteAllKeyFiles(result: (() ->Unit)? = null) {
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.deleteAllKeyFiles()
|
databaseFileHistoryDao.deleteAllKeyFiles()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
result?.invoke()
|
result?.invoke()
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAll(result: (() ->Unit)? = null) {
|
fun deleteAll(result: (() ->Unit)? = null) {
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
databaseFileHistoryDao.deleteAll()
|
databaseFileHistoryDao.deleteAll()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
result?.invoke()
|
result?.invoke()
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ data class FileDatabaseHistoryEntity(
|
|||||||
@ColumnInfo(name = "keyfile_uri")
|
@ColumnInfo(name = "keyfile_uri")
|
||||||
var keyFileUri: String?,
|
var keyFileUri: String?,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "hardware_key")
|
||||||
|
var hardwareKey: String?,
|
||||||
|
|
||||||
@ColumnInfo(name = "updated")
|
@ColumnInfo(name = "updated")
|
||||||
val updated: Long
|
val updated: Long
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.autofill
|
||||||
|
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
import android.view.inputmethod.InlineSuggestionsRequest
|
|
||||||
|
|
||||||
data class AutofillComponent(val assistStructure: AssistStructure,
|
data class AutofillComponent(val assistStructure: AssistStructure,
|
||||||
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?)
|
val compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?)
|
||||||
@@ -29,9 +29,12 @@ import android.graphics.BlendMode
|
|||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.service.autofill.Dataset
|
import android.service.autofill.Dataset
|
||||||
|
import android.service.autofill.Field
|
||||||
import android.service.autofill.FillResponse
|
import android.service.autofill.FillResponse
|
||||||
import android.service.autofill.InlinePresentation
|
import android.service.autofill.InlinePresentation
|
||||||
|
import android.service.autofill.Presentations
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.autofill.AutofillId
|
||||||
import android.view.autofill.AutofillManager
|
import android.view.autofill.AutofillManager
|
||||||
import android.view.autofill.AutofillValue
|
import android.view.autofill.AutofillValue
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
@@ -48,7 +51,7 @@ import com.kunzisoft.keepass.R
|
|||||||
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
||||||
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.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
@@ -56,6 +59,8 @@ import com.kunzisoft.keepass.model.SearchInfo
|
|||||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
@@ -65,10 +70,10 @@ object AutofillHelper {
|
|||||||
private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
|
private const val EXTRA_INLINE_SUGGESTIONS_REQUEST = "com.kunzisoft.keepass.autofill.INLINE_SUGGESTIONS_REQUEST"
|
||||||
|
|
||||||
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
|
fun retrieveAutofillComponent(intent: Intent?): AutofillComponent? {
|
||||||
intent?.getParcelableExtra<AssistStructure?>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
|
intent?.getParcelableExtraCompat<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { assistStructure ->
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
AutofillComponent(assistStructure,
|
AutofillComponent(assistStructure,
|
||||||
intent.getParcelableExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST))
|
intent.getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST))
|
||||||
} else {
|
} else {
|
||||||
AutofillComponent(assistStructure, null)
|
AutofillComponent(assistStructure, null)
|
||||||
}
|
}
|
||||||
@@ -89,44 +94,90 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun newRemoteViews(context: Context,
|
private fun newRemoteViews(context: Context,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
remoteViewsText: String,
|
remoteViewsText: String,
|
||||||
remoteViewsIcon: IconImage? = null): RemoteViews {
|
remoteViewsIcon: IconImage? = null): RemoteViews {
|
||||||
val presentation = RemoteViews(context.packageName, R.layout.item_autofill_entry)
|
val remoteViews = RemoteViews(context.packageName, R.layout.item_autofill_entry)
|
||||||
presentation.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
|
remoteViews.setTextViewText(R.id.autofill_entry_text, remoteViewsText)
|
||||||
if (remoteViewsIcon != null) {
|
if (remoteViewsIcon != null) {
|
||||||
try {
|
try {
|
||||||
database.iconDrawableFactory.getBitmapFromIcon(context,
|
database.iconDrawableFactory.getBitmapFromIcon(context,
|
||||||
remoteViewsIcon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
remoteViewsIcon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
||||||
presentation.setImageViewBitmap(R.id.autofill_entry_icon, bitmap)
|
remoteViews.setImageViewBitmap(R.id.autofill_entry_icon, bitmap)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return presentation
|
return remoteViews
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildDataset(context: Context,
|
private fun Dataset.Builder.addValueToDatasetBuilder(
|
||||||
database: Database,
|
id: AutofillId,
|
||||||
entryInfo: EntryInfo,
|
autofillValue: AutofillValue?
|
||||||
struct: StructureParser.Result,
|
): Dataset.Builder {
|
||||||
additionalBuild: ((build: Dataset.Builder) -> Unit)? = null): Dataset? {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
val title = makeEntryTitle(entryInfo)
|
setField(
|
||||||
val views = newRemoteViews(context, database, title, entryInfo.icon)
|
id, autofillValue?.let {
|
||||||
val builder = Dataset.Builder(views)
|
Field.Builder()
|
||||||
builder.setId(entryInfo.id.toString())
|
.setValue(it)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
setValue(id, autofillValue)
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Set Autofill value $autofillValue for id $id")
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildDatasetForEntry(context: Context,
|
||||||
|
database: ContextualDatabase,
|
||||||
|
entryInfo: EntryInfo,
|
||||||
|
struct: StructureParser.Result,
|
||||||
|
inlinePresentation: InlinePresentation?): Dataset {
|
||||||
|
val remoteViews: RemoteViews = newRemoteViews(context, database, makeEntryTitle(entryInfo), entryInfo.icon)
|
||||||
|
|
||||||
|
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
Dataset.Builder(Presentations.Builder()
|
||||||
|
.apply {
|
||||||
|
inlinePresentation?.let {
|
||||||
|
setInlinePresentation(inlinePresentation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setDialogPresentation(remoteViews)
|
||||||
|
.setMenuPresentation(remoteViews)
|
||||||
|
.build())
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
Dataset.Builder(remoteViews).apply {
|
||||||
|
inlinePresentation?.let {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
setInlinePresentation(inlinePresentation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
datasetBuilder.setId(entryInfo.id.toString())
|
||||||
|
|
||||||
struct.usernameId?.let { usernameId ->
|
struct.usernameId?.let { usernameId ->
|
||||||
builder.setValue(usernameId, AutofillValue.forText(entryInfo.username))
|
datasetBuilder.addValueToDatasetBuilder(
|
||||||
|
usernameId,
|
||||||
|
AutofillValue.forText(entryInfo.username)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
struct.passwordId?.let { passwordId ->
|
struct.passwordId?.let { passwordId ->
|
||||||
builder.setValue(passwordId, AutofillValue.forText(entryInfo.password))
|
datasetBuilder.addValueToDatasetBuilder(
|
||||||
|
passwordId,
|
||||||
|
AutofillValue.forText(entryInfo.password)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entryInfo.expires) {
|
if (entryInfo.expires) {
|
||||||
val year = entryInfo.expiryTime.getYearInt()
|
val year = entryInfo.expiryTime.getYear()
|
||||||
val month = entryInfo.expiryTime.getMonthInt()
|
val month = entryInfo.expiryTime.getMonth()
|
||||||
val monthString = month.toString().padStart(2, '0')
|
val monthString = month.toString().padStart(2, '0')
|
||||||
val day = entryInfo.expiryTime.getDay()
|
val day = entryInfo.expiryTime.getDay()
|
||||||
val dayString = day.toString().padStart(2, '0')
|
val dayString = day.toString().padStart(2, '0')
|
||||||
@@ -134,9 +185,15 @@ object AutofillHelper {
|
|||||||
struct.creditCardExpirationDateId?.let {
|
struct.creditCardExpirationDateId?.let {
|
||||||
if (struct.isWebView) {
|
if (struct.isWebView) {
|
||||||
// set date string as defined in https://html.spec.whatwg.org
|
// set date string as defined in https://html.spec.whatwg.org
|
||||||
builder.setValue(it, AutofillValue.forText("$year\u002D$monthString"))
|
datasetBuilder.addValueToDatasetBuilder(
|
||||||
|
it,
|
||||||
|
AutofillValue.forText("$year\u002D$monthString")
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
builder.setValue(it, AutofillValue.forDate(entryInfo.expiryTime.date.time))
|
datasetBuilder.addValueToDatasetBuilder(
|
||||||
|
it,
|
||||||
|
AutofillValue.forDate(entryInfo.expiryTime.toMilliseconds())
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
struct.creditCardExpirationYearId?.let {
|
struct.creditCardExpirationYearId?.let {
|
||||||
@@ -150,34 +207,58 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
if (yearIndex != -1) {
|
if (yearIndex != -1) {
|
||||||
autofillValue = AutofillValue.forList(yearIndex)
|
autofillValue = AutofillValue.forList(yearIndex)
|
||||||
builder.setValue(it, autofillValue)
|
datasetBuilder.addValueToDatasetBuilder(
|
||||||
|
it,
|
||||||
|
autofillValue
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (autofillValue == null) {
|
if (autofillValue == null) {
|
||||||
builder.setValue(it, AutofillValue.forText(year.toString()))
|
datasetBuilder.addValueToDatasetBuilder(
|
||||||
|
it,
|
||||||
|
AutofillValue.forText(year.toString())
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
struct.creditCardExpirationMonthId?.let {
|
struct.creditCardExpirationMonthId?.let {
|
||||||
if (struct.isWebView) {
|
if (struct.isWebView) {
|
||||||
builder.setValue(it, AutofillValue.forText(monthString))
|
datasetBuilder.addValueToDatasetBuilder(
|
||||||
|
it,
|
||||||
|
AutofillValue.forText(monthString)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
if (struct.creditCardExpirationMonthOptions != null) {
|
if (struct.creditCardExpirationMonthOptions != null) {
|
||||||
// index starts at 0
|
// index starts at 0
|
||||||
builder.setValue(it, AutofillValue.forList(month - 1))
|
datasetBuilder.addValueToDatasetBuilder(
|
||||||
|
it,
|
||||||
|
AutofillValue.forList(month - 1)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
builder.setValue(it, AutofillValue.forText(monthString))
|
datasetBuilder.addValueToDatasetBuilder(
|
||||||
|
it,
|
||||||
|
AutofillValue.forText(monthString)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
struct.creditCardExpirationDayId?.let {
|
struct.creditCardExpirationDayId?.let {
|
||||||
if (struct.isWebView) {
|
if (struct.isWebView) {
|
||||||
builder.setValue(it, AutofillValue.forText(dayString))
|
datasetBuilder.addValueToDatasetBuilder(
|
||||||
|
it,
|
||||||
|
AutofillValue.forText(dayString)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
if (struct.creditCardExpirationDayOptions != null) {
|
if (struct.creditCardExpirationDayOptions != null) {
|
||||||
builder.setValue(it, AutofillValue.forList(day - 1))
|
datasetBuilder.addValueToDatasetBuilder(
|
||||||
|
it,
|
||||||
|
AutofillValue.forList(day - 1)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
builder.setValue(it, AutofillValue.forText(dayString))
|
datasetBuilder.addValueToDatasetBuilder(
|
||||||
|
it,
|
||||||
|
AutofillValue.forText(dayString)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,36 +266,39 @@ object AutofillHelper {
|
|||||||
for (field in entryInfo.customFields) {
|
for (field in entryInfo.customFields) {
|
||||||
if (field.name == TemplateField.LABEL_HOLDER) {
|
if (field.name == TemplateField.LABEL_HOLDER) {
|
||||||
struct.creditCardHolderId?.let { ccNameId ->
|
struct.creditCardHolderId?.let { ccNameId ->
|
||||||
builder.setValue(ccNameId, AutofillValue.forText(field.protectedValue.stringValue))
|
datasetBuilder.addValueToDatasetBuilder(
|
||||||
|
ccNameId,
|
||||||
|
AutofillValue.forText(field.protectedValue.stringValue)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (field.name == TemplateField.LABEL_NUMBER) {
|
if (field.name == TemplateField.LABEL_NUMBER) {
|
||||||
struct.creditCardNumberId?.let { ccnId ->
|
struct.creditCardNumberId?.let { ccnId ->
|
||||||
builder.setValue(ccnId, AutofillValue.forText(field.protectedValue.stringValue))
|
datasetBuilder.addValueToDatasetBuilder(
|
||||||
|
ccnId,
|
||||||
|
AutofillValue.forText(field.protectedValue.stringValue)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (field.name == TemplateField.LABEL_CVV) {
|
if (field.name == TemplateField.LABEL_CVV) {
|
||||||
struct.cardVerificationValueId?.let { cvvId ->
|
struct.cardVerificationValueId?.let { cvvId ->
|
||||||
builder.setValue(cvvId, AutofillValue.forText(field.protectedValue.stringValue))
|
datasetBuilder.addValueToDatasetBuilder(
|
||||||
|
cvvId,
|
||||||
|
AutofillValue.forText(field.protectedValue.stringValue)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val dataset = datasetBuilder.build()
|
||||||
additionalBuild?.invoke(builder)
|
Log.d(TAG, "Autofill Dataset $dataset created")
|
||||||
|
return dataset
|
||||||
return try {
|
|
||||||
builder.build()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// at least one value must be set
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to assign a drawable to a new icon from a database icon
|
* Method to assign a drawable to a new icon from a database icon
|
||||||
*/
|
*/
|
||||||
private fun buildIconFromEntry(context: Context,
|
private fun buildIconFromEntry(context: Context,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
entryInfo: EntryInfo): Icon? {
|
entryInfo: EntryInfo): Icon? {
|
||||||
try {
|
try {
|
||||||
database.iconDrawableFactory.getBitmapFromIcon(context,
|
database.iconDrawableFactory.getBitmapFromIcon(context,
|
||||||
@@ -227,10 +311,10 @@ object AutofillHelper {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
private fun buildInlinePresentationForEntry(context: Context,
|
private fun buildInlinePresentationForEntry(context: Context,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
|
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest,
|
||||||
positionItem: Int,
|
positionItem: Int,
|
||||||
entryInfo: EntryInfo): InlinePresentation? {
|
entryInfo: EntryInfo): InlinePresentation? {
|
||||||
@@ -238,10 +322,11 @@ object AutofillHelper {
|
|||||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
||||||
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
val maxSuggestion = inlineSuggestionsRequest.maxSuggestionCount
|
||||||
|
|
||||||
if (positionItem <= maxSuggestion - 1
|
if (positionItem <= maxSuggestion - 1) {
|
||||||
&& inlinePresentationSpecs.size > positionItem
|
|
||||||
) {
|
// If positionItem is larger than the number of specs in the list, then
|
||||||
val inlinePresentationSpec = inlinePresentationSpecs[positionItem]
|
// the last spec is used for the remainder of the suggestions
|
||||||
|
val inlinePresentationSpec = inlinePresentationSpecs[min(positionItem, inlinePresentationSpecs.size - 1)]
|
||||||
|
|
||||||
// Make sure that the IME spec claims support for v1 UI template.
|
// Make sure that the IME spec claims support for v1 UI template.
|
||||||
val imeStyle = inlinePresentationSpec.style
|
val imeStyle = inlinePresentationSpec.style
|
||||||
@@ -302,7 +387,7 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun buildResponse(context: Context,
|
fun buildResponse(context: Context,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
entriesInfo: List<EntryInfo>,
|
entriesInfo: List<EntryInfo>,
|
||||||
parseResult: StructureParser.Result,
|
parseResult: StructureParser.Result,
|
||||||
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? {
|
compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest?): FillResponse? {
|
||||||
@@ -334,25 +419,33 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entriesInfo.forEachIndexed { _, entry ->
|
entriesInfo.forEachIndexed { _, entry ->
|
||||||
if (numberInlineSuggestions > 0
|
try {
|
||||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
// Build inline presentation for compatible keyboard
|
||||||
&& compatInlineSuggestionsRequest != null) {
|
var inlinePresentation: InlinePresentation? = null
|
||||||
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult) { builder ->
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
buildInlinePresentationForEntry(context, database,
|
&& numberInlineSuggestions > 0
|
||||||
compatInlineSuggestionsRequest, numberInlineSuggestions--, entry
|
&& compatInlineSuggestionsRequest != null) {
|
||||||
)?.let { inlinePresentation ->
|
inlinePresentation = buildInlinePresentationForEntry(
|
||||||
builder.setInlinePresentation(inlinePresentation)
|
context,
|
||||||
}
|
database,
|
||||||
})
|
compatInlineSuggestionsRequest,
|
||||||
} else {
|
numberInlineSuggestions--,
|
||||||
responseBuilder.addDataset(buildDataset(context, database, entry, parseResult))
|
entry
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Create dataset for each entry
|
||||||
|
responseBuilder.addDataset(
|
||||||
|
buildDatasetForEntry(context, database, entry, parseResult, inlinePresentation)
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to add dataset")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a new dataset for manual selection
|
||||||
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
if (PreferencesUtil.isAutofillManualSelectionEnable(context)) {
|
||||||
val searchInfo = SearchInfo().apply {
|
val searchInfo = SearchInfo().apply {
|
||||||
applicationId = parseResult.applicationId
|
applicationId = parseResult.applicationId
|
||||||
@@ -361,25 +454,51 @@ object AutofillHelper {
|
|||||||
manualSelection = true
|
manualSelection = true
|
||||||
}
|
}
|
||||||
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
|
val manualSelectionView = RemoteViews(context.packageName, R.layout.item_autofill_select_entry)
|
||||||
val pendingIntent = AutofillLauncherActivity.getPendingIntentForSelection(context,
|
AutofillLauncherActivity.getPendingIntentForSelection(context,
|
||||||
searchInfo, compatInlineSuggestionsRequest)
|
searchInfo, compatInlineSuggestionsRequest)?.let { pendingIntent ->
|
||||||
|
|
||||||
parseResult.allAutofillIds().let { autofillIds ->
|
var inlinePresentation: InlinePresentation? = null
|
||||||
autofillIds.forEach { id ->
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
val builder = Dataset.Builder(manualSelectionView)
|
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||||
|
val inlinePresentationSpec =
|
||||||
|
inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
||||||
|
inlinePresentation = buildInlinePresentationForManualSelection(
|
||||||
|
context,
|
||||||
|
inlinePresentationSpec,
|
||||||
|
pendingIntent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
val datasetBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
compatInlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
Dataset.Builder(Presentations.Builder()
|
||||||
val inlinePresentationSpec = inlineSuggestionsRequest.inlinePresentationSpecs[0]
|
.apply {
|
||||||
val inlinePresentation = buildInlinePresentationForManualSelection(context, inlinePresentationSpec, pendingIntent)
|
|
||||||
inlinePresentation?.let {
|
inlinePresentation?.let {
|
||||||
builder.setInlinePresentation(it)
|
setInlinePresentation(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setDialogPresentation(manualSelectionView)
|
||||||
|
.setMenuPresentation(manualSelectionView)
|
||||||
|
.build())
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
Dataset.Builder(manualSelectionView).apply {
|
||||||
|
inlinePresentation?.let {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
setInlinePresentation(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
builder.setValue(id, null)
|
}
|
||||||
builder.setAuthentication(pendingIntent.intentSender)
|
|
||||||
responseBuilder.addDataset(builder.build())
|
parseResult.allAutofillIds().let { autofillIds ->
|
||||||
|
autofillIds.forEach { id ->
|
||||||
|
datasetBuilder.addValueToDatasetBuilder(id, null)
|
||||||
|
datasetBuilder.setAuthentication(pendingIntent.intentSender)
|
||||||
|
}
|
||||||
|
val dataset = datasetBuilder.build()
|
||||||
|
Log.d(TAG, "Autofill Dataset for manual selection $dataset created")
|
||||||
|
responseBuilder.addDataset(dataset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -387,6 +506,7 @@ object AutofillHelper {
|
|||||||
return try {
|
return try {
|
||||||
responseBuilder.build()
|
responseBuilder.build()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to create Autofill response", e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -395,7 +515,7 @@ object AutofillHelper {
|
|||||||
* Build the Autofill response for one entry
|
* Build the Autofill response for one entry
|
||||||
*/
|
*/
|
||||||
fun buildResponseAndSetResult(activity: Activity,
|
fun buildResponseAndSetResult(activity: Activity,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
entryInfo: EntryInfo) {
|
entryInfo: EntryInfo) {
|
||||||
buildResponseAndSetResult(activity, database, ArrayList<EntryInfo>().apply { add(entryInfo) })
|
buildResponseAndSetResult(activity, database, ArrayList<EntryInfo>().apply { add(entryInfo) })
|
||||||
}
|
}
|
||||||
@@ -404,17 +524,17 @@ object AutofillHelper {
|
|||||||
* Build the Autofill response for many entry
|
* Build the Autofill response for many entry
|
||||||
*/
|
*/
|
||||||
fun buildResponseAndSetResult(activity: Activity,
|
fun buildResponseAndSetResult(activity: Activity,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
entriesInfo: List<EntryInfo>) {
|
entriesInfo: List<EntryInfo>) {
|
||||||
if (entriesInfo.isEmpty()) {
|
if (entriesInfo.isEmpty()) {
|
||||||
activity.setResult(Activity.RESULT_CANCELED)
|
activity.setResult(Activity.RESULT_CANCELED)
|
||||||
} else {
|
} else {
|
||||||
var setResultOk = false
|
var setResultOk = false
|
||||||
activity.intent?.getParcelableExtra<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { structure ->
|
activity.intent?.getParcelableExtraCompat<AssistStructure>(EXTRA_ASSIST_STRUCTURE)?.let { structure ->
|
||||||
StructureParser(structure).parse()?.let { result ->
|
StructureParser(structure).parse()?.let { result ->
|
||||||
// New Response
|
// New Response
|
||||||
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtra<CompatInlineSuggestionsRequest?>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtraCompat<CompatInlineSuggestionsRequest>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
||||||
if (compatInlineSuggestionsRequest != null) {
|
if (compatInlineSuggestionsRequest != null) {
|
||||||
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
@@ -423,7 +543,7 @@ object AutofillHelper {
|
|||||||
buildResponse(activity, database, entriesInfo, result, null)
|
buildResponse(activity, database, entriesInfo, result, null)
|
||||||
}
|
}
|
||||||
val mReplyIntent = Intent()
|
val mReplyIntent = Intent()
|
||||||
Log.d(activity.javaClass.name, "Successed Autofill auth.")
|
Log.d(activity.javaClass.name, "Success Autofill auth.")
|
||||||
mReplyIntent.putExtra(
|
mReplyIntent.putExtra(
|
||||||
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
AutofillManager.EXTRA_AUTHENTICATION_RESULT,
|
||||||
response)
|
response)
|
||||||
@@ -478,4 +598,6 @@ object AutofillHelper {
|
|||||||
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
|
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
|
||||||
activityResultLauncher?.launch(intent)
|
activityResultLauncher?.launch(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val TAG = AutofillHelper::class.java.name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import android.os.Parcelable
|
|||||||
import android.service.autofill.FillRequest
|
import android.service.autofill.FillRequest
|
||||||
import android.view.inputmethod.InlineSuggestionsRequest
|
import android.view.inputmethod.InlineSuggestionsRequest
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import com.kunzisoft.keepass.utils.readParcelableCompat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class only to prevent java.lang.NoClassDefFoundError for old Android version and new lib compilation
|
* Utility class only to prevent java.lang.NoClassDefFoundError for old Android version and new lib compilation
|
||||||
@@ -52,8 +53,7 @@ class CompatInlineSuggestionsRequest : Parcelable {
|
|||||||
|
|
||||||
constructor(parcel: Parcel) {
|
constructor(parcel: Parcel) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
this.inlineSuggestionsRequest =
|
this.inlineSuggestionsRequest = parcel.readParcelableCompat()
|
||||||
parcel.readParcelable(FillRequest::class.java.classLoader)
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.inlineSuggestionsRequest = null
|
this.inlineSuggestionsRequest = null
|
||||||
|
|||||||
@@ -21,43 +21,51 @@ package com.kunzisoft.keepass.autofill
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.BlendMode
|
import android.graphics.BlendMode
|
||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.CancellationSignal
|
import android.os.CancellationSignal
|
||||||
import android.service.autofill.*
|
import android.service.autofill.AutofillService
|
||||||
|
import android.service.autofill.FillCallback
|
||||||
|
import android.service.autofill.FillRequest
|
||||||
|
import android.service.autofill.FillResponse
|
||||||
|
import android.service.autofill.InlinePresentation
|
||||||
|
import android.service.autofill.Presentations
|
||||||
|
import android.service.autofill.SaveCallback
|
||||||
|
import android.service.autofill.SaveInfo
|
||||||
|
import android.service.autofill.SaveRequest
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.autofill.AutofillId
|
import android.view.autofill.AutofillId
|
||||||
import android.view.inputmethod.InlineSuggestionsRequest
|
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.autofill.inline.UiVersions
|
import androidx.autofill.inline.UiVersions
|
||||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
||||||
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
|
import com.kunzisoft.keepass.autofill.StructureParser.Companion.APPLICATION_ID_POPUP_WINDOW
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
import com.kunzisoft.keepass.model.CreditCard
|
import com.kunzisoft.keepass.model.CreditCard
|
||||||
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.AutofillSettingsActivity
|
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.WebDomain
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class KeeAutofillService : AutofillService() {
|
class KeeAutofillService : AutofillService() {
|
||||||
|
|
||||||
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
||||||
private var mDatabase: Database? = null
|
private var mDatabase: ContextualDatabase? = null
|
||||||
private var applicationIdBlocklist: Set<String>? = null
|
private var applicationIdBlocklist: Set<String>? = null
|
||||||
private var webDomainBlocklist: Set<String>? = null
|
private var webDomainBlocklist: Set<String>? = null
|
||||||
private var askToSaveData: Boolean = false
|
private var askToSaveData: Boolean = false
|
||||||
private var autofillInlineSuggestionsEnabled: Boolean = false
|
private var autofillInlineSuggestionsEnabled: Boolean = false
|
||||||
private var mLock = AtomicBoolean()
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
@@ -90,41 +98,47 @@ class KeeAutofillService : AutofillService() {
|
|||||||
|
|
||||||
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
|
cancellationSignal.setOnCancelListener { Log.w(TAG, "Cancel autofill.") }
|
||||||
|
|
||||||
// Lock
|
if (request.flags and FillRequest.FLAG_COMPATIBILITY_MODE_REQUEST != 0) {
|
||||||
if (!mLock.get()) {
|
Log.d(TAG, "Autofill requested in compatibility mode")
|
||||||
mLock.set(true)
|
} else {
|
||||||
// Check user's settings for authenticating Responses and Datasets.
|
Log.d(TAG, "Autofill requested in native mode")
|
||||||
val latestStructure = request.fillContexts.last().structure
|
}
|
||||||
StructureParser(latestStructure).parse()?.let { parseResult ->
|
|
||||||
|
|
||||||
// Build search info only if applicationId or webDomain are not blocked
|
// Check user's settings for authenticating Responses and Datasets.
|
||||||
if (autofillAllowedFor(parseResult.applicationId, applicationIdBlocklist)
|
val latestStructure = request.fillContexts.last().structure
|
||||||
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
|
StructureParser(latestStructure).parse()?.let { parseResult ->
|
||||||
val searchInfo = SearchInfo().apply {
|
|
||||||
applicationId = parseResult.applicationId
|
// Build search info only if applicationId or webDomain are not blocked
|
||||||
webDomain = parseResult.webDomain
|
if (autofillAllowedFor(
|
||||||
webScheme = parseResult.webScheme
|
applicationId = parseResult.applicationId,
|
||||||
}
|
applicationIdBlocklist = applicationIdBlocklist,
|
||||||
SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
|
webDomain = parseResult.webDomain,
|
||||||
searchInfo.webDomain = webDomainWithoutSubDomain
|
webDomainBlocklist = webDomainBlocklist)
|
||||||
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
) {
|
||||||
&& autofillInlineSuggestionsEnabled) {
|
val searchInfo = SearchInfo().apply {
|
||||||
CompatInlineSuggestionsRequest(request)
|
applicationId = parseResult.applicationId
|
||||||
} else {
|
webDomain = parseResult.webDomain
|
||||||
null
|
webScheme = parseResult.webScheme
|
||||||
}
|
}
|
||||||
launchSelection(mDatabase,
|
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
|
||||||
searchInfo,
|
searchInfo.webDomain = webDomainWithoutSubDomain
|
||||||
parseResult,
|
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
inlineSuggestionsRequest,
|
&& autofillInlineSuggestionsEnabled) {
|
||||||
callback)
|
CompatInlineSuggestionsRequest(request)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
launchSelection(mDatabase,
|
||||||
|
searchInfo,
|
||||||
|
parseResult,
|
||||||
|
inlineSuggestionsRequest,
|
||||||
|
callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchSelection(database: Database?,
|
private fun launchSelection(database: ContextualDatabase?,
|
||||||
searchInfo: SearchInfo,
|
searchInfo: SearchInfo,
|
||||||
parseResult: StructureParser.Result,
|
parseResult: StructureParser.Result,
|
||||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||||
@@ -153,149 +167,207 @@ class KeeAutofillService : AutofillService() {
|
|||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
private fun showUIForEntrySelection(parseResult: StructureParser.Result,
|
||||||
database: Database?,
|
database: ContextualDatabase?,
|
||||||
searchInfo: SearchInfo,
|
searchInfo: SearchInfo,
|
||||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||||
callback: FillCallback) {
|
callback: FillCallback) {
|
||||||
|
var success = false
|
||||||
parseResult.allAutofillIds().let { autofillIds ->
|
parseResult.allAutofillIds().let { autofillIds ->
|
||||||
if (autofillIds.isNotEmpty()) {
|
if (autofillIds.isNotEmpty()) {
|
||||||
// If the entire Autofill Response is authenticated, AuthActivity is used
|
// If the entire Autofill Response is authenticated, AuthActivity is used
|
||||||
// to generate Response.
|
// to generate Response.
|
||||||
val intentSender = AutofillLauncherActivity.getPendingIntentForSelection(this,
|
AutofillLauncherActivity.getPendingIntentForSelection(this,
|
||||||
searchInfo, inlineSuggestionsRequest).intentSender
|
searchInfo, inlineSuggestionsRequest)?.intentSender?.let { intentSender ->
|
||||||
val responseBuilder = FillResponse.Builder()
|
val responseBuilder = FillResponse.Builder()
|
||||||
val remoteViewsUnlock: RemoteViews = if (database == null) {
|
val remoteViewsUnlock: RemoteViews = if (database == null) {
|
||||||
if (!parseResult.webDomain.isNullOrEmpty()) {
|
if (!parseResult.webDomain.isNullOrEmpty()) {
|
||||||
RemoteViews(
|
RemoteViews(
|
||||||
packageName,
|
packageName,
|
||||||
R.layout.item_autofill_unlock_web_domain
|
R.layout.item_autofill_unlock_web_domain
|
||||||
).apply {
|
).apply {
|
||||||
setTextViewText(
|
setTextViewText(
|
||||||
R.id.autofill_web_domain_text,
|
R.id.autofill_web_domain_text,
|
||||||
parseResult.webDomain
|
parseResult.webDomain
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
||||||
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
|
RemoteViews(packageName, R.layout.item_autofill_unlock_app_id).apply {
|
||||||
setTextViewText(
|
setTextViewText(
|
||||||
R.id.autofill_app_id_text,
|
R.id.autofill_app_id_text,
|
||||||
parseResult.applicationId
|
parseResult.applicationId
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RemoteViews(packageName, R.layout.item_autofill_unlock)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
RemoteViews(packageName, R.layout.item_autofill_unlock)
|
if (!parseResult.webDomain.isNullOrEmpty()) {
|
||||||
}
|
RemoteViews(
|
||||||
} else {
|
packageName,
|
||||||
if (!parseResult.webDomain.isNullOrEmpty()) {
|
R.layout.item_autofill_select_entry_web_domain
|
||||||
RemoteViews(
|
).apply {
|
||||||
packageName,
|
setTextViewText(
|
||||||
R.layout.item_autofill_select_entry_web_domain
|
R.id.autofill_web_domain_text,
|
||||||
).apply {
|
parseResult.webDomain
|
||||||
setTextViewText(
|
)
|
||||||
R.id.autofill_web_domain_text,
|
}
|
||||||
parseResult.webDomain
|
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
||||||
)
|
RemoteViews(
|
||||||
|
packageName,
|
||||||
|
R.layout.item_autofill_select_entry_app_id
|
||||||
|
).apply {
|
||||||
|
setTextViewText(
|
||||||
|
R.id.autofill_app_id_text,
|
||||||
|
parseResult.applicationId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RemoteViews(packageName, R.layout.item_autofill_select_entry)
|
||||||
}
|
}
|
||||||
} else if (!parseResult.applicationId.isNullOrEmpty()) {
|
|
||||||
RemoteViews(packageName, R.layout.item_autofill_select_entry_app_id).apply {
|
|
||||||
setTextViewText(
|
|
||||||
R.id.autofill_app_id_text,
|
|
||||||
parseResult.applicationId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
RemoteViews(packageName, R.layout.item_autofill_select_entry)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Tell the autofill framework the interest to save credentials
|
// Tell the autofill framework the interest to save credentials
|
||||||
if (askToSaveData) {
|
if (askToSaveData) {
|
||||||
var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC
|
var types: Int = SaveInfo.SAVE_DATA_TYPE_GENERIC
|
||||||
val requiredIds = ArrayList<AutofillId>()
|
val requiredIds = ArrayList<AutofillId>()
|
||||||
val optionalIds = ArrayList<AutofillId>()
|
val optionalIds = ArrayList<AutofillId>()
|
||||||
|
|
||||||
// Only if at least a password
|
// Only if at least a password
|
||||||
parseResult.passwordId?.let { passwordInfo ->
|
parseResult.passwordId?.let { passwordInfo ->
|
||||||
parseResult.usernameId?.let { usernameInfo ->
|
parseResult.usernameId?.let { usernameInfo ->
|
||||||
types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME
|
types = types or SaveInfo.SAVE_DATA_TYPE_USERNAME
|
||||||
requiredIds.add(usernameInfo)
|
requiredIds.add(usernameInfo)
|
||||||
|
}
|
||||||
|
types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD
|
||||||
|
requiredIds.add(passwordInfo)
|
||||||
}
|
}
|
||||||
types = types or SaveInfo.SAVE_DATA_TYPE_PASSWORD
|
// or a credit card form
|
||||||
requiredIds.add(passwordInfo)
|
if (requiredIds.isEmpty()) {
|
||||||
}
|
parseResult.creditCardNumberId?.let { numberId ->
|
||||||
// or a credit card form
|
types = types or SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD
|
||||||
if (requiredIds.isEmpty()) {
|
requiredIds.add(numberId)
|
||||||
parseResult.creditCardNumberId?.let { numberId ->
|
Log.d(TAG, "Asking to save credit card number")
|
||||||
types = types or SaveInfo.SAVE_DATA_TYPE_CREDIT_CARD
|
}
|
||||||
requiredIds.add(numberId)
|
parseResult.creditCardExpirationDateId?.let { id -> optionalIds.add(id) }
|
||||||
Log.d(TAG, "Asking to save credit card number")
|
parseResult.creditCardExpirationYearId?.let { id -> optionalIds.add(id) }
|
||||||
|
parseResult.creditCardExpirationMonthId?.let { id -> optionalIds.add(id) }
|
||||||
|
parseResult.creditCardHolderId?.let { id -> optionalIds.add(id) }
|
||||||
|
parseResult.cardVerificationValueId?.let { id -> optionalIds.add(id) }
|
||||||
}
|
}
|
||||||
parseResult.creditCardExpirationDateId?.let { id -> optionalIds.add(id) }
|
if (requiredIds.isNotEmpty()) {
|
||||||
parseResult.creditCardExpirationYearId?.let { id -> optionalIds.add(id) }
|
val builder = SaveInfo.Builder(types, requiredIds.toTypedArray())
|
||||||
parseResult.creditCardExpirationMonthId?.let { id -> optionalIds.add(id) }
|
if (optionalIds.isNotEmpty()) {
|
||||||
parseResult.creditCardHolderId?.let { id -> optionalIds.add(id) }
|
builder.setOptionalIds(optionalIds.toTypedArray())
|
||||||
parseResult.cardVerificationValueId?.let { id -> optionalIds.add(id) }
|
}
|
||||||
}
|
responseBuilder.setSaveInfo(builder.build())
|
||||||
if (requiredIds.isNotEmpty()) {
|
|
||||||
val builder = SaveInfo.Builder(types, requiredIds.toTypedArray())
|
|
||||||
if (optionalIds.isNotEmpty()) {
|
|
||||||
builder.setOptionalIds(optionalIds.toTypedArray())
|
|
||||||
}
|
}
|
||||||
responseBuilder.setSaveInfo(builder.build())
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Build inline presentation
|
// Build inline presentation
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
&& autofillInlineSuggestionsEnabled) {
|
&& autofillInlineSuggestionsEnabled
|
||||||
var inlinePresentation: InlinePresentation? = null
|
) {
|
||||||
inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
var inlinePresentation: InlinePresentation? = null
|
||||||
val inlinePresentationSpecs = inlineSuggestionsRequest.inlinePresentationSpecs
|
inlineSuggestionsRequest?.inlineSuggestionsRequest?.let { inlineSuggestionsRequest ->
|
||||||
if (inlineSuggestionsRequest.maxSuggestionCount > 0
|
val inlinePresentationSpecs =
|
||||||
&& inlinePresentationSpecs.size > 0) {
|
inlineSuggestionsRequest.inlinePresentationSpecs
|
||||||
val inlinePresentationSpec = inlinePresentationSpecs[0]
|
if (inlineSuggestionsRequest.maxSuggestionCount > 0
|
||||||
|
&& inlinePresentationSpecs.isNotEmpty()
|
||||||
|
) {
|
||||||
|
val inlinePresentationSpec = inlinePresentationSpecs[0]
|
||||||
|
|
||||||
// Make sure that the IME spec claims support for v1 UI template.
|
// Make sure that the IME spec claims support for v1 UI template.
|
||||||
val imeStyle = inlinePresentationSpec.style
|
val imeStyle = inlinePresentationSpec.style
|
||||||
if (UiVersions.getVersions(imeStyle).contains(UiVersions.INLINE_UI_VERSION_1)) {
|
if (UiVersions.getVersions(imeStyle)
|
||||||
// Build the content for IME UI
|
.contains(UiVersions.INLINE_UI_VERSION_1)
|
||||||
inlinePresentation = InlinePresentation(
|
) {
|
||||||
|
// Build the content for IME UI
|
||||||
|
inlinePresentation = InlinePresentation(
|
||||||
InlineSuggestionUi.newContentBuilder(
|
InlineSuggestionUi.newContentBuilder(
|
||||||
PendingIntent.getActivity(this,
|
PendingIntent.getActivity(
|
||||||
0,
|
this,
|
||||||
Intent(this, AutofillSettingsActivity::class.java),
|
0,
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
Intent(this, AutofillSettingsActivity::class.java),
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
} else {
|
)
|
||||||
0
|
|
||||||
})
|
|
||||||
).apply {
|
).apply {
|
||||||
setContentDescription(getString(R.string.autofill_sign_in_prompt))
|
setContentDescription(getString(R.string.autofill_sign_in_prompt))
|
||||||
setTitle(getString(R.string.autofill_sign_in_prompt))
|
setTitle(getString(R.string.autofill_sign_in_prompt))
|
||||||
setStartIcon(Icon.createWithResource(this@KeeAutofillService, R.mipmap.ic_launcher_round).apply {
|
setStartIcon(
|
||||||
setTintBlendMode(BlendMode.DST)
|
Icon.createWithResource(
|
||||||
})
|
this@KeeAutofillService,
|
||||||
}.build().slice, inlinePresentationSpec, false)
|
R.mipmap.ic_launcher_round
|
||||||
|
).apply {
|
||||||
|
setTintBlendMode(BlendMode.DST)
|
||||||
|
})
|
||||||
|
}.build().slice, inlinePresentationSpec, false
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build response
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
try {
|
||||||
|
// Buggy method on some API 33 devices
|
||||||
|
responseBuilder.setAuthentication(
|
||||||
|
autofillIds,
|
||||||
|
intentSender,
|
||||||
|
Presentations.Builder().apply {
|
||||||
|
inlinePresentation?.let {
|
||||||
|
setInlinePresentation(it)
|
||||||
|
}
|
||||||
|
setDialogPresentation(remoteViewsUnlock)
|
||||||
|
}.build()
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to use the new setAuthentication method.", e)
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
responseBuilder.setAuthentication(
|
||||||
|
autofillIds,
|
||||||
|
intentSender,
|
||||||
|
remoteViewsUnlock,
|
||||||
|
inlinePresentation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
responseBuilder.setAuthentication(
|
||||||
|
autofillIds,
|
||||||
|
intentSender,
|
||||||
|
remoteViewsUnlock,
|
||||||
|
inlinePresentation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
responseBuilder.setAuthentication(
|
||||||
|
autofillIds,
|
||||||
|
intentSender,
|
||||||
|
remoteViewsUnlock
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// Build response
|
success = true
|
||||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock, inlinePresentation)
|
callback.onSuccess(responseBuilder.build())
|
||||||
} else {
|
|
||||||
responseBuilder.setAuthentication(autofillIds, intentSender, remoteViewsUnlock)
|
|
||||||
}
|
}
|
||||||
callback.onSuccess(responseBuilder.build())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!success)
|
||||||
|
callback.onFailure("Unable to get Autofill ids for UI selection")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
|
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
|
||||||
|
var success = false
|
||||||
if (askToSaveData) {
|
if (askToSaveData) {
|
||||||
val latestStructure = request.fillContexts.last().structure
|
val latestStructure = request.fillContexts.last().structure
|
||||||
StructureParser(latestStructure).parse(true)?.let { parseResult ->
|
StructureParser(latestStructure).parse(true)?.let { parseResult ->
|
||||||
|
|
||||||
if (autofillAllowedFor(parseResult.applicationId, applicationIdBlocklist)
|
if (autofillAllowedFor(
|
||||||
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
|
applicationId = parseResult.applicationId,
|
||||||
|
applicationIdBlocklist = applicationIdBlocklist,
|
||||||
|
webDomain = parseResult.webDomain,
|
||||||
|
webDomainBlocklist = webDomainBlocklist)
|
||||||
|
) {
|
||||||
Log.d(TAG, "autofill onSaveRequest password")
|
Log.d(TAG, "autofill onSaveRequest password")
|
||||||
|
|
||||||
// Build expiration from date or from year and month
|
// Build expiration from date or from year and month
|
||||||
@@ -332,14 +404,16 @@ class KeeAutofillService : AutofillService() {
|
|||||||
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,
|
// callback.onSuccess(AutofillLauncherActivity.getAuthIntentSenderForRegistration(this,
|
||||||
// registerInfo))
|
// registerInfo))
|
||||||
//} else {
|
//} else {
|
||||||
AutofillLauncherActivity.launchForRegistration(this, registerInfo)
|
AutofillLauncherActivity.launchForRegistration(this, registerInfo)
|
||||||
callback.onSuccess()
|
success = true
|
||||||
|
callback.onSuccess()
|
||||||
//}
|
//}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
callback.onFailure("Saving form values is not allowed")
|
if (!success) {
|
||||||
|
callback.onFailure("Saving form values is not allowed")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConnected() {
|
override fun onConnected() {
|
||||||
@@ -348,13 +422,34 @@ class KeeAutofillService : AutofillService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDisconnected() {
|
override fun onDisconnected() {
|
||||||
mLock.set(false)
|
|
||||||
Log.d(TAG, "onDisconnected")
|
Log.d(TAG, "onDisconnected")
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = KeeAutofillService::class.java.name
|
private val TAG = KeeAutofillService::class.java.name
|
||||||
|
|
||||||
|
fun autofillAllowedFor(applicationId: String?,
|
||||||
|
webDomain: String?,
|
||||||
|
context: Context
|
||||||
|
): Boolean {
|
||||||
|
return autofillAllowedFor(
|
||||||
|
applicationId = applicationId,
|
||||||
|
applicationIdBlocklist = PreferencesUtil.applicationIdBlocklist(context),
|
||||||
|
webDomain = webDomain,
|
||||||
|
webDomainBlocklist = PreferencesUtil.webDomainBlocklist(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun autofillAllowedFor(applicationId: String?,
|
||||||
|
applicationIdBlocklist: Set<String>?,
|
||||||
|
webDomain: String?,
|
||||||
|
webDomainBlocklist: Set<String>?
|
||||||
|
): Boolean {
|
||||||
|
return autofillAllowedFor(applicationId, applicationIdBlocklist)
|
||||||
|
// To prevent unrecognized autofill popup id
|
||||||
|
&& applicationId?.contains(APPLICATION_ID_POPUP_WINDOW) != true
|
||||||
|
&& autofillAllowedFor(webDomain, webDomainBlocklist)
|
||||||
|
}
|
||||||
|
|
||||||
fun autofillAllowedFor(element: String?, blockList: Set<String>?): Boolean {
|
fun autofillAllowedFor(element: String?, blockList: Set<String>?): Boolean {
|
||||||
element?.let { elementNotNull ->
|
element?.let { elementNotNull ->
|
||||||
if (blockList?.any { appIdBlocked ->
|
if (blockList?.any { appIdBlocked ->
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ import android.view.autofill.AutofillId
|
|||||||
import android.view.autofill.AutofillValue
|
import android.view.autofill.AutofillValue
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,7 +36,6 @@ import kotlin.collections.ArrayList
|
|||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class StructureParser(private val structure: AssistStructure) {
|
class StructureParser(private val structure: AssistStructure) {
|
||||||
private var result: Result? = null
|
private var result: Result? = null
|
||||||
private var usernameNeeded = true
|
|
||||||
private var usernameIdCandidate: AutofillId? = null
|
private var usernameIdCandidate: AutofillId? = null
|
||||||
private var usernameValueCandidate: AutofillValue? = null
|
private var usernameValueCandidate: AutofillValue? = null
|
||||||
|
|
||||||
@@ -53,7 +51,7 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
applicationId = windowNode.title.toString().split("/")[0]
|
applicationId = windowNode.title.toString().split("/")[0]
|
||||||
Log.d(TAG, "Autofill applicationId: $applicationId")
|
Log.d(TAG, "Autofill applicationId: $applicationId")
|
||||||
|
|
||||||
if (applicationId?.contains("PopupWindow:") == false) {
|
if (applicationId?.contains(APPLICATION_ID_POPUP_WINDOW) == false) {
|
||||||
if (parseViewNode(windowNode.rootViewNode))
|
if (parseViewNode(windowNode.rootViewNode))
|
||||||
break@mainLoop
|
break@mainLoop
|
||||||
}
|
}
|
||||||
@@ -105,7 +103,7 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
if (node.autofillId != null) {
|
if (node.autofillId != null) {
|
||||||
// Parse methods
|
// Parse methods
|
||||||
val hints = node.autofillHints
|
val hints = node.autofillHints
|
||||||
if (hints != null && hints.isNotEmpty()) {
|
if (!hints.isNullOrEmpty()) {
|
||||||
if (parseNodeByAutofillHint(node))
|
if (parseNodeByAutofillHint(node))
|
||||||
returnValue = true
|
returnValue = true
|
||||||
} else if (parseNodeByHtmlAttributes(node))
|
} else if (parseNodeByHtmlAttributes(node))
|
||||||
@@ -134,16 +132,37 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
it.contains(View.AUTOFILL_HINT_USERNAME, true)
|
it.contains(View.AUTOFILL_HINT_USERNAME, true)
|
||||||
|| it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|
|| it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
|
||||||
|| it.contains("email", true)
|
|| it.contains("email", true)
|
||||||
|| it.contains(View.AUTOFILL_HINT_PHONE, true) -> {
|
|| it.contains("login", true) -> {
|
||||||
result?.usernameId = autofillId
|
// Replace username until we have a password
|
||||||
result?.usernameValue = node.autofillValue
|
if (result?.passwordId == null) {
|
||||||
Log.d(TAG, "Autofill username hint")
|
result?.usernameId = autofillId
|
||||||
|
result?.usernameValue = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill username hint if no password")
|
||||||
|
} else {
|
||||||
|
usernameIdCandidate = autofillId
|
||||||
|
usernameValueCandidate = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill username hint if password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.contains(View.AUTOFILL_HINT_PHONE, true) -> {
|
||||||
|
if (usernameIdCandidate == null) {
|
||||||
|
usernameIdCandidate = autofillId
|
||||||
|
usernameValueCandidate = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill phone")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
it.contains(View.AUTOFILL_HINT_PASSWORD, true) -> {
|
it.contains(View.AUTOFILL_HINT_PASSWORD, true) -> {
|
||||||
|
// Password Id changed if it's the second times we are here,
|
||||||
|
// So the last username candidate is most appropriate
|
||||||
|
if (result?.passwordId != null) {
|
||||||
|
result?.usernameId = usernameIdCandidate
|
||||||
|
result?.usernameValue = usernameValueCandidate
|
||||||
|
}
|
||||||
result?.passwordId = autofillId
|
result?.passwordId = autofillId
|
||||||
result?.passwordValue = node.autofillValue
|
result?.passwordValue = node.autofillValue
|
||||||
Log.d(TAG, "Autofill password hint")
|
Log.d(TAG, "Autofill password hint")
|
||||||
return true
|
// Comment "return" to check all the tree
|
||||||
|
// return true
|
||||||
}
|
}
|
||||||
it.equals("cc-name", true) -> {
|
it.equals("cc-name", true) -> {
|
||||||
Log.d(TAG, "Autofill credit card name hint")
|
Log.d(TAG, "Autofill credit card name hint")
|
||||||
@@ -279,14 +298,19 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
"type" -> {
|
"type" -> {
|
||||||
when (pairAttribute.second.lowercase(Locale.ENGLISH)) {
|
when (pairAttribute.second.lowercase(Locale.ENGLISH)) {
|
||||||
"tel", "email" -> {
|
"tel", "email" -> {
|
||||||
result?.usernameId = autofillId
|
if (result?.passwordId == null) {
|
||||||
result?.usernameValue = node.autofillValue
|
result?.usernameId = autofillId
|
||||||
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
result?.usernameValue = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"text" -> {
|
"text" -> {
|
||||||
usernameIdCandidate = autofillId
|
// Assume username is before password
|
||||||
usernameValueCandidate = node.autofillValue
|
if (result?.passwordId == null) {
|
||||||
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
usernameIdCandidate = autofillId
|
||||||
|
usernameValueCandidate = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"password" -> {
|
"password" -> {
|
||||||
result?.passwordId = autofillId
|
result?.passwordId = autofillId
|
||||||
@@ -315,85 +339,128 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
return "0x${"%08x".format(inputType)}"
|
return "0x${"%08x".format(inputType)}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun manageTypeText(
|
||||||
|
node: AssistStructure.ViewNode,
|
||||||
|
autofillId: AutofillId?,
|
||||||
|
inputType: Int
|
||||||
|
): Boolean {
|
||||||
|
when {
|
||||||
|
inputIsVariationType(inputType,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS) -> {
|
||||||
|
if (result?.passwordId == null) {
|
||||||
|
result?.usernameId = autofillId
|
||||||
|
result?.usernameValue = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill username android text type: ${showHexInputType(inputType)}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inputIsVariationType(inputType,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_NORMAL,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> {
|
||||||
|
// Assume the username field is before the password field
|
||||||
|
if (result?.passwordId == null) {
|
||||||
|
usernameIdCandidate = autofillId
|
||||||
|
usernameValueCandidate = node.autofillValue
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
|
||||||
|
}
|
||||||
|
inputIsVariationType(inputType,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {
|
||||||
|
// Some forms used visible password as username
|
||||||
|
if (result?.passwordId == null &&
|
||||||
|
usernameIdCandidate == null && usernameValueCandidate == null) {
|
||||||
|
usernameIdCandidate = autofillId
|
||||||
|
usernameValueCandidate = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill visible password android text type (as username): ${showHexInputType(inputType)}")
|
||||||
|
} else if (result?.passwordId == null && result?.passwordValue == null) {
|
||||||
|
result?.passwordId = autofillId
|
||||||
|
result?.passwordValue = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill visible password android text type (as password): ${showHexInputType(inputType)}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inputIsVariationType(inputType,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_PASSWORD,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) -> {
|
||||||
|
result?.passwordId = autofillId
|
||||||
|
result?.passwordValue = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill password android text type: ${showHexInputType(inputType)}")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
inputIsVariationType(inputType,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_FILTER,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_PHONETIC,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE,
|
||||||
|
InputType.TYPE_TEXT_VARIATION_URI) -> {
|
||||||
|
// Type not used
|
||||||
|
Log.d(TAG, "Autofill not used android text type: ${showHexInputType(inputType)}")
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.d(TAG, "Autofill unknown android text type: ${showHexInputType(inputType)}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun manageTypeNumber(
|
||||||
|
node: AssistStructure.ViewNode,
|
||||||
|
autofillId: AutofillId?,
|
||||||
|
inputType: Int
|
||||||
|
): Boolean {
|
||||||
|
when {
|
||||||
|
inputIsVariationType(inputType,
|
||||||
|
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
|
||||||
|
if (usernameIdCandidate == null) {
|
||||||
|
usernameIdCandidate = autofillId
|
||||||
|
usernameValueCandidate = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill username candidate android number type: ${showHexInputType(inputType)}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inputIsVariationType(inputType,
|
||||||
|
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {
|
||||||
|
result?.passwordId = autofillId
|
||||||
|
result?.passwordValue = node.autofillValue
|
||||||
|
Log.d(TAG, "Autofill password android number type: ${showHexInputType(inputType)}")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.d(TAG, "Autofill unknown android number type: ${showHexInputType(inputType)}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun manageTypeNull(
|
||||||
|
node: AssistStructure.ViewNode,
|
||||||
|
autofillId: AutofillId?,
|
||||||
|
inputType: Int
|
||||||
|
): Boolean {
|
||||||
|
if (node.className == "android.widget.EditText") {
|
||||||
|
Log.d(TAG, "Autofill null android input type class: ${showHexInputType(inputType)}" +
|
||||||
|
", get the EditText node class name!")
|
||||||
|
if (result?.passwordId == null) {
|
||||||
|
usernameIdCandidate = autofillId
|
||||||
|
usernameValueCandidate = node.autofillValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseNodeByAndroidInput(node: AssistStructure.ViewNode): Boolean {
|
private fun parseNodeByAndroidInput(node: AssistStructure.ViewNode): Boolean {
|
||||||
val autofillId = node.autofillId
|
val autofillId = node.autofillId
|
||||||
val inputType = node.inputType
|
val inputType = node.inputType
|
||||||
when (inputType and InputType.TYPE_MASK_CLASS) {
|
when (inputType and InputType.TYPE_MASK_CLASS) {
|
||||||
InputType.TYPE_CLASS_TEXT -> {
|
InputType.TYPE_CLASS_TEXT -> {
|
||||||
when {
|
return manageTypeText(node, autofillId, inputType)
|
||||||
inputIsVariationType(inputType,
|
|
||||||
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
|
|
||||||
InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS) -> {
|
|
||||||
result?.usernameId = autofillId
|
|
||||||
result?.usernameValue = node.autofillValue
|
|
||||||
Log.d(TAG, "Autofill username android text type: ${showHexInputType(inputType)}")
|
|
||||||
}
|
|
||||||
inputIsVariationType(inputType,
|
|
||||||
InputType.TYPE_TEXT_VARIATION_NORMAL,
|
|
||||||
InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
|
|
||||||
InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> {
|
|
||||||
usernameIdCandidate = autofillId
|
|
||||||
usernameValueCandidate = node.autofillValue
|
|
||||||
Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
|
|
||||||
}
|
|
||||||
inputIsVariationType(inputType,
|
|
||||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {
|
|
||||||
// Some forms used visible password as username
|
|
||||||
if (usernameIdCandidate == null && usernameValueCandidate == null) {
|
|
||||||
usernameIdCandidate = autofillId
|
|
||||||
usernameValueCandidate = node.autofillValue
|
|
||||||
Log.d(TAG, "Autofill visible password android text type (as username): ${showHexInputType(inputType)}")
|
|
||||||
} else if (result?.passwordId == null && result?.passwordValue == null) {
|
|
||||||
result?.passwordId = autofillId
|
|
||||||
result?.passwordValue = node.autofillValue
|
|
||||||
Log.d(TAG, "Autofill visible password android text type (as password): ${showHexInputType(inputType)}")
|
|
||||||
usernameNeeded = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
inputIsVariationType(inputType,
|
|
||||||
InputType.TYPE_TEXT_VARIATION_PASSWORD,
|
|
||||||
InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) -> {
|
|
||||||
result?.passwordId = autofillId
|
|
||||||
result?.passwordValue = node.autofillValue
|
|
||||||
Log.d(TAG, "Autofill password android text type: ${showHexInputType(inputType)}")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
inputIsVariationType(inputType,
|
|
||||||
InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT,
|
|
||||||
InputType.TYPE_TEXT_VARIATION_FILTER,
|
|
||||||
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE,
|
|
||||||
InputType.TYPE_TEXT_VARIATION_PHONETIC,
|
|
||||||
InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS,
|
|
||||||
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE,
|
|
||||||
InputType.TYPE_TEXT_VARIATION_URI) -> {
|
|
||||||
// Type not used
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Log.d(TAG, "Autofill unknown android text type: ${showHexInputType(inputType)}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
InputType.TYPE_CLASS_NUMBER -> {
|
InputType.TYPE_CLASS_NUMBER -> {
|
||||||
when {
|
return manageTypeNumber(node, autofillId, inputType)
|
||||||
inputIsVariationType(inputType,
|
}
|
||||||
InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
|
InputType.TYPE_NULL -> {
|
||||||
if (usernameIdCandidate == null) {
|
return manageTypeNull(node, autofillId, inputType)
|
||||||
usernameIdCandidate = autofillId
|
|
||||||
usernameValueCandidate = node.autofillValue
|
|
||||||
Log.d(TAG, "Autofill username candidate android number type: ${showHexInputType(inputType)}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
inputIsVariationType(inputType,
|
|
||||||
InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {
|
|
||||||
result?.passwordId = autofillId
|
|
||||||
result?.passwordValue = node.autofillValue
|
|
||||||
Log.d(TAG, "Autofill password android number type: ${showHexInputType(inputType)}")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Log.d(TAG, "Autofill unknown android number type: ${showHexInputType(inputType)}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -422,58 +489,14 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
var creditCardExpirationDayOptions: Array<CharSequence>? = null
|
var creditCardExpirationDayOptions: Array<CharSequence>? = null
|
||||||
|
|
||||||
var usernameId: AutofillId? = null
|
var usernameId: AutofillId? = null
|
||||||
set(value) {
|
|
||||||
if (field == null)
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var passwordId: AutofillId? = null
|
var passwordId: AutofillId? = null
|
||||||
set(value) {
|
|
||||||
if (field == null)
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var creditCardHolderId: AutofillId? = null
|
var creditCardHolderId: AutofillId? = null
|
||||||
set(value) {
|
|
||||||
if (field == null)
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var creditCardNumberId: AutofillId? = null
|
var creditCardNumberId: AutofillId? = null
|
||||||
set(value) {
|
|
||||||
if (field == null)
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var creditCardExpirationDateId: AutofillId? = null
|
var creditCardExpirationDateId: AutofillId? = null
|
||||||
set(value) {
|
|
||||||
if (field == null)
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var creditCardExpirationYearId: AutofillId? = null
|
var creditCardExpirationYearId: AutofillId? = null
|
||||||
set(value) {
|
|
||||||
if (field == null)
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var creditCardExpirationMonthId: AutofillId? = null
|
var creditCardExpirationMonthId: AutofillId? = null
|
||||||
set(value) {
|
|
||||||
if (field == null)
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var creditCardExpirationDayId: AutofillId? = null
|
var creditCardExpirationDayId: AutofillId? = null
|
||||||
set(value) {
|
|
||||||
if (field == null)
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var cardVerificationValueId: AutofillId? = null
|
var cardVerificationValueId: AutofillId? = null
|
||||||
set(value) {
|
|
||||||
if (field == null)
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun allAutofillIds(): Array<AutofillId> {
|
fun allAutofillIds(): Array<AutofillId> {
|
||||||
val all = ArrayList<AutofillId>()
|
val all = ArrayList<AutofillId>()
|
||||||
@@ -500,13 +523,13 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
|
|
||||||
var usernameValue: AutofillValue? = null
|
var usernameValue: AutofillValue? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
if (allowSaveValues && field == null)
|
if (allowSaveValues)
|
||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
|
|
||||||
var passwordValue: AutofillValue? = null
|
var passwordValue: AutofillValue? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
if (allowSaveValues && field == null)
|
if (allowSaveValues)
|
||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,5 +582,7 @@ class StructureParser(private val structure: AssistStructure) {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = StructureParser::class.java.name
|
private val TAG = StructureParser::class.java.name
|
||||||
|
|
||||||
|
const val APPLICATION_ID_POPUP_WINDOW = "PopupWindow:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.biometric
|
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
|
|
||||||
data class AdvancedUnlockCryptoPrompt(var cipher: Cipher,
|
|
||||||
@StringRes var promptTitleId: Int,
|
|
||||||
@StringRes var promptDescriptionId: Int? = null,
|
|
||||||
var isDeviceCredentialOperation: Boolean,
|
|
||||||
var isBiometricOperation: Boolean)
|
|
||||||
@@ -1,671 +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.biometric
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.provider.Settings
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.*
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.biometric.BiometricManager
|
|
||||||
import androidx.biometric.BiometricPrompt
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.getkeepsafe.taptargetview.TapTargetView
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
|
||||||
import com.kunzisoft.keepass.database.exception.IODatabaseException
|
|
||||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
|
||||||
import com.kunzisoft.keepass.model.CipherDecryptDatabase
|
|
||||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
|
||||||
import com.kunzisoft.keepass.model.CredentialStorage
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
|
||||||
import com.kunzisoft.keepass.view.hideByFading
|
|
||||||
import com.kunzisoft.keepass.view.showByFading
|
|
||||||
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedUnlockCallback {
|
|
||||||
|
|
||||||
private var mBuilderListener: BuilderListener? = null
|
|
||||||
|
|
||||||
private var mAdvancedUnlockEnabled = false
|
|
||||||
private var mAutoOpenPromptEnabled = false
|
|
||||||
|
|
||||||
private var advancedUnlockManager: AdvancedUnlockManager? = null
|
|
||||||
private var biometricMode: Mode = Mode.BIOMETRIC_UNAVAILABLE
|
|
||||||
private var mAdvancedUnlockInfoView: AdvancedUnlockInfoView? = null
|
|
||||||
|
|
||||||
var databaseFileUri: Uri? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
// TODO Retrieve credential storage from app database
|
|
||||||
var credentialDatabaseStorage: CredentialStorage = CredentialStorage.DEFAULT
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manage setting to auto open biometric prompt
|
|
||||||
*/
|
|
||||||
private var mAutoOpenPrompt: Boolean
|
|
||||||
get() {
|
|
||||||
return mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt && mAutoOpenPromptEnabled
|
|
||||||
}
|
|
||||||
set(value) {
|
|
||||||
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Variable to check if the prompt can be open (if the right activity is currently shown)
|
|
||||||
// checkBiometricAvailability() allows open biometric prompt and onDestroy() removes the authorization
|
|
||||||
private var allowOpenBiometricPrompt = false
|
|
||||||
|
|
||||||
private lateinit var cipherDatabaseAction : CipherDatabaseAction
|
|
||||||
|
|
||||||
private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null
|
|
||||||
|
|
||||||
private val mAdvancedUnlockViewModel: AdvancedUnlockViewModel by activityViewModels()
|
|
||||||
|
|
||||||
// Only to fix multiple fingerprint menu #332
|
|
||||||
private var mAllowAdvancedUnlockMenu = false
|
|
||||||
private var mAddBiometricMenuInProgress = false
|
|
||||||
|
|
||||||
// Only keep connection when we request a device credential activity
|
|
||||||
private var keepConnection = false
|
|
||||||
|
|
||||||
private var mDeviceCredentialResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
|
||||||
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
|
|
||||||
// To wait resume
|
|
||||||
if (keepConnection) {
|
|
||||||
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = result.resultCode == Activity.RESULT_OK
|
|
||||||
}
|
|
||||||
keepConnection = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
|
|
||||||
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(context)
|
|
||||||
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(context)
|
|
||||||
try {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
mBuilderListener = context as BuilderListener
|
|
||||||
}
|
|
||||||
} catch (e: ClassCastException) {
|
|
||||||
throw ClassCastException(context.toString()
|
|
||||||
+ " must implement " + BuilderListener::class.java.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
setHasOptionsMenu(true)
|
|
||||||
|
|
||||||
cipherDatabaseAction = CipherDatabaseAction.getInstance(requireContext().applicationContext)
|
|
||||||
|
|
||||||
mAdvancedUnlockViewModel.onInitAdvancedUnlockModeRequested.observe(this) {
|
|
||||||
initAdvancedUnlockMode()
|
|
||||||
}
|
|
||||||
|
|
||||||
mAdvancedUnlockViewModel.onUnlockAvailabilityCheckRequested.observe(this) {
|
|
||||||
checkUnlockAvailability()
|
|
||||||
}
|
|
||||||
|
|
||||||
mAdvancedUnlockViewModel.onDatabaseFileLoaded.observe(this) {
|
|
||||||
onDatabaseLoaded(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
||||||
super.onCreateView(inflater, container, savedInstanceState)
|
|
||||||
|
|
||||||
val rootView = inflater.cloneInContext(contextThemed)
|
|
||||||
.inflate(R.layout.fragment_advanced_unlock, container, false)
|
|
||||||
|
|
||||||
mAdvancedUnlockInfoView = rootView.findViewById(R.id.advanced_unlock_view)
|
|
||||||
|
|
||||||
return rootView
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
context?.let {
|
|
||||||
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(it)
|
|
||||||
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(it)
|
|
||||||
}
|
|
||||||
keepConnection = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
|
||||||
super.onCreateOptionsMenu(menu, inflater)
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
// biometric menu
|
|
||||||
if (mAllowAdvancedUnlockMenu)
|
|
||||||
inflater.inflate(R.menu.advanced_unlock, menu)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.menu_keystore_remove_key -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
deleteEncryptedDatabaseKey()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onDatabaseLoaded(databaseUri: Uri?) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
// To get device credential unlock result, only if same database uri
|
|
||||||
if (databaseUri != null
|
|
||||||
&& mAdvancedUnlockEnabled) {
|
|
||||||
val deviceCredentialAuthSucceeded = mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded
|
|
||||||
deviceCredentialAuthSucceeded?.let {
|
|
||||||
if (databaseUri == databaseFileUri) {
|
|
||||||
if (deviceCredentialAuthSucceeded == true) {
|
|
||||||
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationSucceeded()
|
|
||||||
} else {
|
|
||||||
advancedUnlockManager?.advancedUnlockCallback?.onAuthenticationFailed()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
disconnect()
|
|
||||||
}
|
|
||||||
} ?: run {
|
|
||||||
if (databaseUri != databaseFileUri) {
|
|
||||||
connect(databaseUri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
disconnect()
|
|
||||||
}
|
|
||||||
mAdvancedUnlockViewModel.deviceCredentialAuthSucceeded = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check unlock availability and change the current mode depending of device's state
|
|
||||||
*/
|
|
||||||
private fun checkUnlockAvailability() {
|
|
||||||
context?.let { context ->
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
allowOpenBiometricPrompt = true
|
|
||||||
if (PreferencesUtil.isBiometricUnlockEnable(context)) {
|
|
||||||
mAdvancedUnlockInfoView?.setIconResource(R.drawable.fingerprint)
|
|
||||||
|
|
||||||
// biometric not supported (by API level or hardware) so keep option hidden
|
|
||||||
// or manually disable
|
|
||||||
val biometricCanAuthenticate = AdvancedUnlockManager.canAuthenticate(context)
|
|
||||||
if (!PreferencesUtil.isAdvancedUnlockEnable(context)
|
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
|
|
||||||
toggleMode(Mode.BIOMETRIC_UNAVAILABLE)
|
|
||||||
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) {
|
|
||||||
toggleMode(Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED)
|
|
||||||
} else {
|
|
||||||
// biometric is available but not configured, show icon but in disabled state with some information
|
|
||||||
if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
|
|
||||||
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
|
|
||||||
} else {
|
|
||||||
selectMode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
|
|
||||||
mAdvancedUnlockInfoView?.setIconResource(R.drawable.bolt)
|
|
||||||
if (AdvancedUnlockManager.isDeviceSecure(context)) {
|
|
||||||
selectMode()
|
|
||||||
} else {
|
|
||||||
toggleMode(Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
private fun selectMode() {
|
|
||||||
// Check if fingerprint well init (be called the first time the fingerprint is configured
|
|
||||||
// and the activity still active)
|
|
||||||
if (advancedUnlockManager?.isKeyManagerInitialized != true) {
|
|
||||||
advancedUnlockManager = AdvancedUnlockManager { requireActivity() }
|
|
||||||
// callback for fingerprint findings
|
|
||||||
advancedUnlockManager?.advancedUnlockCallback = this
|
|
||||||
}
|
|
||||||
// Recheck to change the mode
|
|
||||||
if (advancedUnlockManager?.isKeyManagerInitialized != true) {
|
|
||||||
toggleMode(Mode.KEY_MANAGER_UNAVAILABLE)
|
|
||||||
} else {
|
|
||||||
if (mBuilderListener?.conditionToStoreCredential() == true) {
|
|
||||||
// listen for encryption
|
|
||||||
toggleMode(Mode.STORE_CREDENTIAL)
|
|
||||||
} else {
|
|
||||||
databaseFileUri?.let { databaseUri ->
|
|
||||||
cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher ->
|
|
||||||
// biometric available but no stored password found yet for this DB so show info don't listen
|
|
||||||
toggleMode(if (containsCipher) {
|
|
||||||
// listen for decryption
|
|
||||||
Mode.EXTRACT_CREDENTIAL
|
|
||||||
} else {
|
|
||||||
// wait for typing
|
|
||||||
Mode.WAIT_CREDENTIAL
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
private fun toggleMode(newBiometricMode: Mode) {
|
|
||||||
if (newBiometricMode != biometricMode) {
|
|
||||||
biometricMode = newBiometricMode
|
|
||||||
initAdvancedUnlockMode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
private fun initNotAvailable() {
|
|
||||||
showViews(false)
|
|
||||||
|
|
||||||
mAdvancedUnlockInfoView?.setIconViewClickListener(false, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
private fun openBiometricSetting() {
|
|
||||||
mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
|
|
||||||
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
|
|
||||||
context?.startActivity(Intent(Settings.ACTION_SETTINGS))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
private fun initSecurityUpdateRequired() {
|
|
||||||
showViews(true)
|
|
||||||
setAdvancedUnlockedTitleView(R.string.biometric_security_update_required)
|
|
||||||
|
|
||||||
openBiometricSetting()
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
private fun initNotConfigured() {
|
|
||||||
showViews(true)
|
|
||||||
setAdvancedUnlockedTitleView(R.string.configure_biometric)
|
|
||||||
setAdvancedUnlockedMessageView("")
|
|
||||||
|
|
||||||
openBiometricSetting()
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
private fun initKeyManagerNotAvailable() {
|
|
||||||
showViews(true)
|
|
||||||
setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
|
|
||||||
|
|
||||||
openBiometricSetting()
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
private fun initWaitData() {
|
|
||||||
showViews(true)
|
|
||||||
setAdvancedUnlockedTitleView(R.string.no_credentials_stored)
|
|
||||||
setAdvancedUnlockedMessageView("")
|
|
||||||
|
|
||||||
context?.let { context ->
|
|
||||||
mAdvancedUnlockInfoView?.setIconViewClickListener(false) {
|
|
||||||
onAuthenticationError(BiometricPrompt.ERROR_UNABLE_TO_PROCESS,
|
|
||||||
context.getString(R.string.credential_before_click_advanced_unlock_button))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
private fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt) {
|
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
|
||||||
if (allowOpenBiometricPrompt) {
|
|
||||||
if (cryptoPrompt.isDeviceCredentialOperation)
|
|
||||||
keepConnection = true
|
|
||||||
try {
|
|
||||||
advancedUnlockManager?.openAdvancedUnlockPrompt(cryptoPrompt,
|
|
||||||
mDeviceCredentialResultLauncher)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to open advanced unlock prompt", e)
|
|
||||||
setAdvancedUnlockedTitleView(R.string.advanced_unlock_prompt_not_initialized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
private fun initEncryptData() {
|
|
||||||
showViews(true)
|
|
||||||
setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_store_credential)
|
|
||||||
setAdvancedUnlockedMessageView("")
|
|
||||||
|
|
||||||
advancedUnlockManager?.initEncryptData { cryptoPrompt ->
|
|
||||||
// Set listener to open the biometric dialog and save credential
|
|
||||||
mAdvancedUnlockInfoView?.setIconViewClickListener { _ ->
|
|
||||||
openAdvancedUnlockPrompt(cryptoPrompt)
|
|
||||||
}
|
|
||||||
} ?: throw Exception("AdvancedUnlockManager not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
private fun initDecryptData() {
|
|
||||||
showViews(true)
|
|
||||||
setAdvancedUnlockedTitleView(R.string.open_advanced_unlock_prompt_unlock_database)
|
|
||||||
setAdvancedUnlockedMessageView("")
|
|
||||||
|
|
||||||
advancedUnlockManager?.let { unlockHelper ->
|
|
||||||
databaseFileUri?.let { databaseUri ->
|
|
||||||
cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase ->
|
|
||||||
cipherDatabase?.let {
|
|
||||||
unlockHelper.initDecryptData(it.specParameters) { cryptoPrompt ->
|
|
||||||
|
|
||||||
// Set listener to open the biometric dialog and check credential
|
|
||||||
mAdvancedUnlockInfoView?.setIconViewClickListener { _ ->
|
|
||||||
openAdvancedUnlockPrompt(cryptoPrompt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto open the biometric prompt
|
|
||||||
if (mAutoOpenPrompt) {
|
|
||||||
mAutoOpenPrompt = false
|
|
||||||
openAdvancedUnlockPrompt(cryptoPrompt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: deleteEncryptedDatabaseKey()
|
|
||||||
}
|
|
||||||
} ?: throw IODatabaseException()
|
|
||||||
} ?: throw Exception("AdvancedUnlockManager not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initAdvancedUnlockMode() {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
mAllowAdvancedUnlockMenu = false
|
|
||||||
try {
|
|
||||||
when (biometricMode) {
|
|
||||||
Mode.BIOMETRIC_UNAVAILABLE -> initNotAvailable()
|
|
||||||
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> initSecurityUpdateRequired()
|
|
||||||
Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> initNotConfigured()
|
|
||||||
Mode.KEY_MANAGER_UNAVAILABLE -> initKeyManagerNotAvailable()
|
|
||||||
Mode.WAIT_CREDENTIAL -> initWaitData()
|
|
||||||
Mode.STORE_CREDENTIAL -> initEncryptData()
|
|
||||||
Mode.EXTRACT_CREDENTIAL -> initDecryptData()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
onGenericException(e)
|
|
||||||
}
|
|
||||||
invalidateBiometricMenu()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun invalidateBiometricMenu() {
|
|
||||||
// Show fingerprint key deletion
|
|
||||||
if (!mAddBiometricMenuInProgress) {
|
|
||||||
mAddBiometricMenuInProgress = true
|
|
||||||
databaseFileUri?.let { databaseUri ->
|
|
||||||
cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher ->
|
|
||||||
mAllowAdvancedUnlockMenu = containsCipher
|
|
||||||
&& (biometricMode != Mode.BIOMETRIC_UNAVAILABLE
|
|
||||||
&& biometricMode != Mode.KEY_MANAGER_UNAVAILABLE)
|
|
||||||
mAddBiometricMenuInProgress = false
|
|
||||||
activity?.invalidateOptionsMenu()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
fun connect(databaseUri: Uri) {
|
|
||||||
showViews(true)
|
|
||||||
this.databaseFileUri = databaseUri
|
|
||||||
cipherDatabaseListener = object: CipherDatabaseAction.CipherDatabaseListener {
|
|
||||||
override fun onCipherDatabaseCleared() {
|
|
||||||
advancedUnlockManager?.closeBiometricPrompt()
|
|
||||||
checkUnlockAvailability()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cipherDatabaseAction.apply {
|
|
||||||
reloadPreferences()
|
|
||||||
cipherDatabaseListener?.let {
|
|
||||||
registerDatabaseListener(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
checkUnlockAvailability()
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
fun disconnect(hideViews: Boolean = true,
|
|
||||||
closePrompt: Boolean = true) {
|
|
||||||
this.databaseFileUri = null
|
|
||||||
// Close the biometric prompt
|
|
||||||
allowOpenBiometricPrompt = false
|
|
||||||
if (closePrompt)
|
|
||||||
advancedUnlockManager?.closeBiometricPrompt()
|
|
||||||
cipherDatabaseListener?.let {
|
|
||||||
cipherDatabaseAction.unregisterDatabaseListener(it)
|
|
||||||
}
|
|
||||||
biometricMode = Mode.BIOMETRIC_UNAVAILABLE
|
|
||||||
if (hideViews) {
|
|
||||||
showViews(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
fun deleteEncryptedDatabaseKey() {
|
|
||||||
mAllowAdvancedUnlockMenu = false
|
|
||||||
advancedUnlockManager?.closeBiometricPrompt()
|
|
||||||
databaseFileUri?.let { databaseUri ->
|
|
||||||
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
|
|
||||||
checkUnlockAvailability()
|
|
||||||
}
|
|
||||||
} ?: checkUnlockAvailability()
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
|
||||||
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
|
|
||||||
setAdvancedUnlockedMessageView(errString.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
override fun onAuthenticationFailed() {
|
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
|
||||||
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
|
|
||||||
setAdvancedUnlockedMessageView(R.string.advanced_unlock_not_recognized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
override fun onAuthenticationSucceeded() {
|
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
|
||||||
when (biometricMode) {
|
|
||||||
Mode.BIOMETRIC_UNAVAILABLE -> {
|
|
||||||
}
|
|
||||||
Mode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> {
|
|
||||||
}
|
|
||||||
Mode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> {
|
|
||||||
}
|
|
||||||
Mode.KEY_MANAGER_UNAVAILABLE -> {
|
|
||||||
}
|
|
||||||
Mode.WAIT_CREDENTIAL -> {
|
|
||||||
}
|
|
||||||
Mode.STORE_CREDENTIAL -> {
|
|
||||||
// newly store the entered password in encrypted way
|
|
||||||
mBuilderListener?.retrieveCredentialForEncryption()?.let { credential ->
|
|
||||||
advancedUnlockManager?.encryptData(credential)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Mode.EXTRACT_CREDENTIAL -> {
|
|
||||||
// retrieve the encrypted value from preferences
|
|
||||||
databaseFileUri?.let { databaseUri ->
|
|
||||||
cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase ->
|
|
||||||
cipherDatabase?.encryptedValue?.let { value ->
|
|
||||||
advancedUnlockManager?.decryptData(value)
|
|
||||||
} ?: deleteEncryptedDatabaseKey()
|
|
||||||
}
|
|
||||||
} ?: run {
|
|
||||||
onAuthenticationError(-1, getString(R.string.error_database_uri_null))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {
|
|
||||||
databaseFileUri?.let { databaseUri ->
|
|
||||||
mBuilderListener?.onCredentialEncrypted(
|
|
||||||
CipherEncryptDatabase().apply {
|
|
||||||
this.databaseUri = databaseUri
|
|
||||||
this.credentialStorage = credentialDatabaseStorage
|
|
||||||
this.encryptedValue = encryptedValue
|
|
||||||
this.specParameters = ivSpec
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handleDecryptedResult(decryptedValue: ByteArray) {
|
|
||||||
// Load database directly with password retrieve
|
|
||||||
databaseFileUri?.let { databaseUri ->
|
|
||||||
mBuilderListener?.onCredentialDecrypted(
|
|
||||||
CipherDecryptDatabase().apply {
|
|
||||||
this.databaseUri = databaseUri
|
|
||||||
this.credentialStorage = credentialDatabaseStorage
|
|
||||||
this.decryptedValue = decryptedValue
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
override fun onUnrecoverableKeyException(e: Exception) {
|
|
||||||
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
override fun onInvalidKeyException(e: Exception) {
|
|
||||||
setAdvancedUnlockedMessageView(R.string.advanced_unlock_invalid_key)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
override fun onGenericException(e: Exception) {
|
|
||||||
val errorMessage = e.cause?.localizedMessage ?: e.localizedMessage ?: ""
|
|
||||||
setAdvancedUnlockedMessageView(errorMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showViews(show: Boolean) {
|
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
|
||||||
if (show) {
|
|
||||||
if (mAdvancedUnlockInfoView?.visibility != View.VISIBLE)
|
|
||||||
mAdvancedUnlockInfoView?.showByFading()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (mAdvancedUnlockInfoView?.visibility == View.VISIBLE)
|
|
||||||
mAdvancedUnlockInfoView?.hideByFading()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
private fun setAdvancedUnlockedTitleView(textId: Int) {
|
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
|
||||||
mAdvancedUnlockInfoView?.setTitle(textId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
private fun setAdvancedUnlockedMessageView(textId: Int) {
|
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
|
||||||
mAdvancedUnlockInfoView?.setMessage(textId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
private fun setAdvancedUnlockedMessageView(text: CharSequence) {
|
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
|
||||||
mAdvancedUnlockInfoView?.message = text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class Mode {
|
|
||||||
BIOMETRIC_UNAVAILABLE,
|
|
||||||
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
|
|
||||||
DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED,
|
|
||||||
KEY_MANAGER_UNAVAILABLE,
|
|
||||||
WAIT_CREDENTIAL,
|
|
||||||
STORE_CREDENTIAL,
|
|
||||||
EXTRACT_CREDENTIAL
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BuilderListener {
|
|
||||||
fun retrieveCredentialForEncryption(): ByteArray
|
|
||||||
fun conditionToStoreCredential(): Boolean
|
|
||||||
fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase)
|
|
||||||
fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
if (!keepConnection) {
|
|
||||||
// If close prompt, bug "user not authenticated in Android R"
|
|
||||||
disconnect(false)
|
|
||||||
advancedUnlockManager = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
mAdvancedUnlockInfoView = null
|
|
||||||
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
disconnect()
|
|
||||||
advancedUnlockManager = null
|
|
||||||
mBuilderListener = null
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetach() {
|
|
||||||
mBuilderListener = null
|
|
||||||
|
|
||||||
super.onDetach()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private val TAG = AdvancedUnlockFragment::class.java.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,513 +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.biometric
|
|
||||||
|
|
||||||
import android.app.KeyguardManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Build
|
|
||||||
import android.security.keystore.KeyGenParameterSpec
|
|
||||||
import android.security.keystore.KeyPermanentlyInvalidatedException
|
|
||||||
import android.security.keystore.KeyProperties
|
|
||||||
import android.util.Log
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.biometric.BiometricManager
|
|
||||||
import androidx.biometric.BiometricManager.Authenticators.*
|
|
||||||
import androidx.biometric.BiometricPrompt
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import java.security.KeyStore
|
|
||||||
import java.security.UnrecoverableKeyException
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import javax.crypto.BadPaddingException
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.KeyGenerator
|
|
||||||
import javax.crypto.SecretKey
|
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
|
||||||
class AdvancedUnlockManager(private var retrieveContext: () -> FragmentActivity) {
|
|
||||||
|
|
||||||
private var keyStore: KeyStore? = null
|
|
||||||
private var keyGenerator: KeyGenerator? = null
|
|
||||||
private var cipher: Cipher? = null
|
|
||||||
|
|
||||||
private var biometricPrompt: BiometricPrompt? = null
|
|
||||||
private var authenticationCallback = object: BiometricPrompt.AuthenticationCallback() {
|
|
||||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
|
||||||
advancedUnlockCallback?.onAuthenticationSucceeded()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAuthenticationFailed() {
|
|
||||||
advancedUnlockCallback?.onAuthenticationFailed()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
|
||||||
advancedUnlockCallback?.onAuthenticationError(errorCode, errString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var advancedUnlockCallback: AdvancedUnlockCallback? = null
|
|
||||||
|
|
||||||
private var isKeyManagerInit = false
|
|
||||||
|
|
||||||
private val biometricUnlockEnable = PreferencesUtil.isBiometricUnlockEnable(retrieveContext())
|
|
||||||
private val deviceCredentialUnlockEnable = PreferencesUtil.isDeviceCredentialUnlockEnable(retrieveContext())
|
|
||||||
|
|
||||||
val isKeyManagerInitialized: Boolean
|
|
||||||
get() {
|
|
||||||
if (!isKeyManagerInit) {
|
|
||||||
advancedUnlockCallback?.onGenericException(Exception("Biometric not initialized"))
|
|
||||||
}
|
|
||||||
return isKeyManagerInit
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isBiometricOperation(): Boolean {
|
|
||||||
return biometricUnlockEnable || isDeviceCredentialBiometricOperation()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since Android 30, device credential is also a biometric operation
|
|
||||||
private fun isDeviceCredentialOperation(): Boolean {
|
|
||||||
return Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
|
||||||
&& deviceCredentialUnlockEnable
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isDeviceCredentialBiometricOperation(): Boolean {
|
|
||||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
|
||||||
&& deviceCredentialUnlockEnable
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (isDeviceSecure(retrieveContext())
|
|
||||||
&& (biometricUnlockEnable || deviceCredentialUnlockEnable)) {
|
|
||||||
try {
|
|
||||||
this.keyStore = KeyStore.getInstance(ADVANCED_UNLOCK_KEYSTORE)
|
|
||||||
this.keyGenerator = KeyGenerator.getInstance(ADVANCED_UNLOCK_KEY_ALGORITHM, ADVANCED_UNLOCK_KEYSTORE)
|
|
||||||
this.cipher = Cipher.getInstance(
|
|
||||||
ADVANCED_UNLOCK_KEY_ALGORITHM + "/"
|
|
||||||
+ ADVANCED_UNLOCK_BLOCKS_MODES + "/"
|
|
||||||
+ ADVANCED_UNLOCK_ENCRYPTION_PADDING)
|
|
||||||
isKeyManagerInit = (keyStore != null
|
|
||||||
&& keyGenerator != null
|
|
||||||
&& cipher != null)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to initialize the keystore", e)
|
|
||||||
isKeyManagerInit = false
|
|
||||||
advancedUnlockCallback?.onGenericException(e)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// really not much to do when no fingerprint support found
|
|
||||||
isKeyManagerInit = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized private fun getSecretKey(): SecretKey? {
|
|
||||||
if (!isKeyManagerInitialized) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// Create new key if needed
|
|
||||||
keyStore?.let { keyStore ->
|
|
||||||
keyStore.load(null)
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!keyStore.containsAlias(ADVANCED_UNLOCK_KEYSTORE_KEY)) {
|
|
||||||
// Set the alias of the entry in Android KeyStore where the key will appear
|
|
||||||
// and the constrains (purposes) in the constructor of the Builder
|
|
||||||
keyGenerator?.init(
|
|
||||||
KeyGenParameterSpec.Builder(
|
|
||||||
ADVANCED_UNLOCK_KEYSTORE_KEY,
|
|
||||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
|
||||||
.setBlockModes(ADVANCED_UNLOCK_BLOCKS_MODES)
|
|
||||||
.setEncryptionPaddings(ADVANCED_UNLOCK_ENCRYPTION_PADDING)
|
|
||||||
.apply {
|
|
||||||
// Require the user to authenticate with a fingerprint to authorize every use
|
|
||||||
// of the key, don't use it for device credential because it's the user authentication
|
|
||||||
if (biometricUnlockEnable) {
|
|
||||||
setUserAuthenticationRequired(true)
|
|
||||||
}
|
|
||||||
// To store in the security chip
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
|
||||||
&& retrieveContext().packageManager.hasSystemFeature(
|
|
||||||
PackageManager.FEATURE_STRONGBOX_KEYSTORE)) {
|
|
||||||
setIsStrongBoxBacked(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.build())
|
|
||||||
keyGenerator?.generateKey()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to create a key in keystore", e)
|
|
||||||
advancedUnlockCallback?.onGenericException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
return keyStore.getKey(ADVANCED_UNLOCK_KEYSTORE_KEY, null) as SecretKey?
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to retrieve the key in keystore", e)
|
|
||||||
advancedUnlockCallback?.onGenericException(e)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,) {
|
|
||||||
initEncryptData(actionIfCypherInit, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized private fun initEncryptData(actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
|
|
||||||
firstLaunch: Boolean) {
|
|
||||||
if (!isKeyManagerInitialized) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
getSecretKey()?.let { secretKey ->
|
|
||||||
cipher?.let { cipher ->
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
|
||||||
|
|
||||||
actionIfCypherInit.invoke(
|
|
||||||
AdvancedUnlockCryptoPrompt(
|
|
||||||
cipher,
|
|
||||||
R.string.advanced_unlock_prompt_store_credential_title,
|
|
||||||
R.string.advanced_unlock_prompt_store_credential_message,
|
|
||||||
isDeviceCredentialOperation(), isBiometricOperation())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
|
||||||
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
|
|
||||||
advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException)
|
|
||||||
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
|
||||||
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
|
|
||||||
if (firstLaunch) {
|
|
||||||
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
|
|
||||||
initEncryptData(actionIfCypherInit, false)
|
|
||||||
} else {
|
|
||||||
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to initialize encrypt data", e)
|
|
||||||
advancedUnlockCallback?.onGenericException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized fun encryptData(value: ByteArray) {
|
|
||||||
if (!isKeyManagerInitialized) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
val encrypted = cipher?.doFinal(value) ?: byteArrayOf()
|
|
||||||
// passes updated iv spec on to callback so this can be stored for decryption
|
|
||||||
cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec ->
|
|
||||||
advancedUnlockCallback?.handleEncryptedResult(encrypted, spec.iv)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to encrypt data", e)
|
|
||||||
advancedUnlockCallback?.onGenericException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized fun initDecryptData(ivSpecValue: ByteArray,
|
|
||||||
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit) {
|
|
||||||
initDecryptData(ivSpecValue, actionIfCypherInit, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized private fun initDecryptData(ivSpecValue: ByteArray,
|
|
||||||
actionIfCypherInit: (cryptoPrompt: AdvancedUnlockCryptoPrompt) -> Unit,
|
|
||||||
firstLaunch: Boolean = true) {
|
|
||||||
if (!isKeyManagerInitialized) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// important to restore spec here that was used for decryption
|
|
||||||
val spec = IvParameterSpec(ivSpecValue)
|
|
||||||
getSecretKey()?.let { secretKey ->
|
|
||||||
cipher?.let { cipher ->
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
|
|
||||||
|
|
||||||
actionIfCypherInit.invoke(
|
|
||||||
AdvancedUnlockCryptoPrompt(
|
|
||||||
cipher,
|
|
||||||
R.string.advanced_unlock_prompt_extract_credential_title,
|
|
||||||
null,
|
|
||||||
isDeviceCredentialOperation(), isBiometricOperation())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
|
||||||
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
|
|
||||||
if (firstLaunch) {
|
|
||||||
deleteKeystoreKey()
|
|
||||||
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
|
|
||||||
} else {
|
|
||||||
advancedUnlockCallback?.onUnrecoverableKeyException(unrecoverableKeyException)
|
|
||||||
}
|
|
||||||
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
|
||||||
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
|
|
||||||
if (firstLaunch) {
|
|
||||||
deleteAllEntryKeysInKeystoreForBiometric(retrieveContext())
|
|
||||||
initDecryptData(ivSpecValue, actionIfCypherInit, firstLaunch)
|
|
||||||
} else {
|
|
||||||
advancedUnlockCallback?.onInvalidKeyException(invalidKeyException)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to initialize decrypt data", e)
|
|
||||||
advancedUnlockCallback?.onGenericException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized fun decryptData(encryptedValue: ByteArray) {
|
|
||||||
if (!isKeyManagerInitialized) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// actual decryption here
|
|
||||||
cipher?.doFinal(encryptedValue)?.let { decrypted ->
|
|
||||||
advancedUnlockCallback?.handleDecryptedResult(decrypted)
|
|
||||||
}
|
|
||||||
} catch (badPaddingException: BadPaddingException) {
|
|
||||||
Log.e(TAG, "Unable to decrypt data", badPaddingException)
|
|
||||||
advancedUnlockCallback?.onInvalidKeyException(badPaddingException)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to decrypt data", e)
|
|
||||||
advancedUnlockCallback?.onGenericException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized fun deleteKeystoreKey() {
|
|
||||||
try {
|
|
||||||
keyStore?.load(null)
|
|
||||||
keyStore?.deleteEntry(ADVANCED_UNLOCK_KEYSTORE_KEY)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to delete entry key in keystore", e)
|
|
||||||
advancedUnlockCallback?.onGenericException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized fun openAdvancedUnlockPrompt(cryptoPrompt: AdvancedUnlockCryptoPrompt,
|
|
||||||
deviceCredentialResultLauncher: ActivityResultLauncher<Intent>
|
|
||||||
) {
|
|
||||||
// Init advanced unlock prompt
|
|
||||||
if (biometricPrompt == null) {
|
|
||||||
biometricPrompt = BiometricPrompt(retrieveContext(),
|
|
||||||
Executors.newSingleThreadExecutor(),
|
|
||||||
authenticationCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
val promptTitle = retrieveContext().getString(cryptoPrompt.promptTitleId)
|
|
||||||
val promptDescription = cryptoPrompt.promptDescriptionId?.let { descriptionId ->
|
|
||||||
retrieveContext().getString(descriptionId)
|
|
||||||
} ?: ""
|
|
||||||
|
|
||||||
if (cryptoPrompt.isBiometricOperation) {
|
|
||||||
val promptInfoExtractCredential = BiometricPrompt.PromptInfo.Builder().apply {
|
|
||||||
setTitle(promptTitle)
|
|
||||||
if (promptDescription.isNotEmpty())
|
|
||||||
setDescription(promptDescription)
|
|
||||||
setConfirmationRequired(false)
|
|
||||||
if (isDeviceCredentialBiometricOperation()) {
|
|
||||||
setAllowedAuthenticators(DEVICE_CREDENTIAL)
|
|
||||||
} else {
|
|
||||||
setNegativeButtonText(retrieveContext().getString(android.R.string.cancel))
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
biometricPrompt?.authenticate(
|
|
||||||
promptInfoExtractCredential,
|
|
||||||
BiometricPrompt.CryptoObject(cryptoPrompt.cipher))
|
|
||||||
}
|
|
||||||
else if (cryptoPrompt.isDeviceCredentialOperation) {
|
|
||||||
val keyGuardManager = ContextCompat.getSystemService(retrieveContext(), KeyguardManager::class.java)
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
deviceCredentialResultLauncher.launch(
|
|
||||||
keyGuardManager?.createConfirmDeviceCredentialIntent(promptTitle, promptDescription)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized fun closeBiometricPrompt() {
|
|
||||||
biometricPrompt?.cancelAuthentication()
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AdvancedUnlockErrorCallback {
|
|
||||||
fun onUnrecoverableKeyException(e: Exception)
|
|
||||||
fun onInvalidKeyException(e: Exception)
|
|
||||||
fun onGenericException(e: Exception)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AdvancedUnlockCallback : AdvancedUnlockErrorCallback {
|
|
||||||
fun onAuthenticationSucceeded()
|
|
||||||
fun onAuthenticationFailed()
|
|
||||||
fun onAuthenticationError(errorCode: Int, errString: CharSequence)
|
|
||||||
fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray)
|
|
||||||
fun handleDecryptedResult(decryptedValue: ByteArray)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private val TAG = AdvancedUnlockManager::class.java.name
|
|
||||||
|
|
||||||
private const val ADVANCED_UNLOCK_KEYSTORE = "AndroidKeyStore"
|
|
||||||
private const val ADVANCED_UNLOCK_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key"
|
|
||||||
private const val ADVANCED_UNLOCK_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
|
|
||||||
private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
|
|
||||||
private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
|
||||||
fun canAuthenticate(context: Context): Int {
|
|
||||||
return try {
|
|
||||||
BiometricManager.from(context).canAuthenticate(
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
|
||||||
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
|
|
||||||
BIOMETRIC_STRONG or DEVICE_CREDENTIAL
|
|
||||||
} else {
|
|
||||||
BIOMETRIC_STRONG
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
|
|
||||||
try {
|
|
||||||
BiometricManager.from(context).canAuthenticate(
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
|
||||||
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
|
|
||||||
BIOMETRIC_WEAK or DEVICE_CREDENTIAL
|
|
||||||
} else {
|
|
||||||
BIOMETRIC_WEAK
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
|
||||||
fun isDeviceSecure(context: Context): Boolean {
|
|
||||||
val keyguardManager = ContextCompat.getSystemService(context, KeyguardManager::class.java)
|
|
||||||
return keyguardManager?.isDeviceSecure ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
|
||||||
fun biometricUnlockSupported(context: Context): Boolean {
|
|
||||||
val biometricCanAuthenticate = try {
|
|
||||||
BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
|
|
||||||
try {
|
|
||||||
BiometricManager.from(context).canAuthenticate(BIOMETRIC_WEAK)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
|
|
||||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
|
||||||
fun deviceCredentialUnlockSupported(context: Context): Boolean {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL)
|
|
||||||
return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
|
|
||||||
)
|
|
||||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
ContextCompat.getSystemService(context, KeyguardManager::class.java)?.apply {
|
|
||||||
return isDeviceSecure
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove entry key in keystore
|
|
||||||
*/
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
|
||||||
fun deleteEntryKeyInKeystoreForBiometric(fragmentActivity: FragmentActivity,
|
|
||||||
advancedCallback: AdvancedUnlockErrorCallback) {
|
|
||||||
AdvancedUnlockManager{ fragmentActivity }.apply {
|
|
||||||
advancedUnlockCallback = object : AdvancedUnlockCallback {
|
|
||||||
override fun onAuthenticationSucceeded() {}
|
|
||||||
|
|
||||||
override fun onAuthenticationFailed() {}
|
|
||||||
|
|
||||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {}
|
|
||||||
|
|
||||||
override fun handleEncryptedResult(encryptedValue: ByteArray, ivSpec: ByteArray) {}
|
|
||||||
|
|
||||||
override fun handleDecryptedResult(decryptedValue: ByteArray) {}
|
|
||||||
|
|
||||||
override fun onUnrecoverableKeyException(e: Exception) {
|
|
||||||
advancedCallback.onUnrecoverableKeyException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInvalidKeyException(e: Exception) {
|
|
||||||
advancedCallback.onInvalidKeyException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onGenericException(e: Exception) {
|
|
||||||
advancedCallback.onGenericException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deleteKeystoreKey()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteAllEntryKeysInKeystoreForBiometric(activity: FragmentActivity) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
deleteEntryKeyInKeystoreForBiometric(
|
|
||||||
activity,
|
|
||||||
object : AdvancedUnlockErrorCallback {
|
|
||||||
fun showException(e: Exception) {
|
|
||||||
Toast.makeText(activity,
|
|
||||||
activity.getString(R.string.advanced_unlock_scanning_error, e.localizedMessage),
|
|
||||||
Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUnrecoverableKeyException(e: Exception) {
|
|
||||||
showException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInvalidKeyException(e: Exception) {
|
|
||||||
showException(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onGenericException(e: Exception) {
|
|
||||||
showException(e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
CipherDatabaseAction.getInstance(activity.applicationContext).deleteAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.kunzisoft.keepass.biometric
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
|
||||||
|
data class DeviceUnlockCryptoPrompt(
|
||||||
|
var type: DeviceUnlockCryptoPromptType,
|
||||||
|
var cipher: Cipher,
|
||||||
|
@StringRes var titleId: Int,
|
||||||
|
@StringRes var descriptionId: Int? = null,
|
||||||
|
var isDeviceCredentialOperation: Boolean,
|
||||||
|
var isBiometricOperation: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class DeviceUnlockCryptoPromptType {
|
||||||
|
CREDENTIAL_ENCRYPTION, CREDENTIAL_DECRYPTION
|
||||||
|
}
|
||||||
@@ -0,0 +1,384 @@
|
|||||||
|
/*
|
||||||
|
* 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.biometric
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.KeyguardManager
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||||
|
import androidx.biometric.BiometricPrompt
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.MenuProvider
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity.Companion.UI_VISIBLE_DURING_LOCK
|
||||||
|
import com.kunzisoft.keepass.view.DeviceUnlockView
|
||||||
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
|
import com.kunzisoft.keepass.view.showByFading
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DeviceUnlockPromptMode
|
||||||
|
import com.kunzisoft.keepass.viewmodels.DeviceUnlockViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
class DeviceUnlockFragment: Fragment() {
|
||||||
|
|
||||||
|
private var mDeviceUnlockView: DeviceUnlockView? = null
|
||||||
|
|
||||||
|
private val mDeviceUnlockViewModel: DeviceUnlockViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private var mBiometricPrompt: BiometricPrompt? = null
|
||||||
|
|
||||||
|
// Only to fix multiple fingerprint menu #332
|
||||||
|
private var mAllowAdvancedUnlockMenu = false
|
||||||
|
|
||||||
|
private var mDeviceCredentialResultLauncher: ActivityResultLauncher<Intent>? = registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
mDeviceUnlockViewModel.onAuthenticationSucceeded(result)
|
||||||
|
} else {
|
||||||
|
setAuthenticationFailed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var biometricAuthenticationCallback = object: BiometricPrompt.AuthenticationCallback() {
|
||||||
|
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||||
|
mDeviceUnlockViewModel.onAuthenticationSucceeded(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationFailed() {
|
||||||
|
setAuthenticationFailed()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||||
|
setAuthenticationError(errorCode, errString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val menuProvider: MenuProvider = object: MenuProvider {
|
||||||
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
|
// biometric menu
|
||||||
|
if (mAllowAdvancedUnlockMenu)
|
||||||
|
menuInflater.inflate(R.menu.advanced_unlock, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||||
|
when (menuItem.itemId) {
|
||||||
|
R.id.menu_keystore_remove_key ->
|
||||||
|
deleteEncryptedDatabaseKey()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
|
val rootView = inflater.inflate(R.layout.fragment_advanced_unlock, container, false)
|
||||||
|
|
||||||
|
mDeviceUnlockView = rootView.findViewById(R.id.advanced_unlock_view)
|
||||||
|
|
||||||
|
return rootView
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
// Init device unlock prompt
|
||||||
|
mBiometricPrompt = BiometricPrompt(
|
||||||
|
this@DeviceUnlockFragment,
|
||||||
|
Executors.newSingleThreadExecutor(),
|
||||||
|
biometricAuthenticationCallback
|
||||||
|
)
|
||||||
|
|
||||||
|
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
mDeviceUnlockViewModel.uiState.collect { uiState ->
|
||||||
|
// Change mode
|
||||||
|
toggleDeviceCredentialMode(uiState.newDeviceUnlockMode)
|
||||||
|
// Prompt
|
||||||
|
manageDeviceCredentialPrompt(uiState.cryptoPromptState)
|
||||||
|
// Advanced menu
|
||||||
|
mAllowAdvancedUnlockMenu = uiState.allowAdvancedUnlockMenu
|
||||||
|
activity?.invalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
// Don't allow auto open prompt if lock become when UI visible
|
||||||
|
if (UI_VISIBLE_DURING_LOCK) {
|
||||||
|
mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt = false
|
||||||
|
}
|
||||||
|
|
||||||
|
mDeviceUnlockViewModel.checkUnlockAvailability()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelBiometricPrompt() {
|
||||||
|
mBiometricPrompt?.cancelAuthentication()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toggleDeviceCredentialMode(deviceUnlockMode: DeviceUnlockMode) {
|
||||||
|
try {
|
||||||
|
when (deviceUnlockMode) {
|
||||||
|
DeviceUnlockMode.BIOMETRIC_UNAVAILABLE -> setNotAvailableMode()
|
||||||
|
DeviceUnlockMode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> setSecurityUpdateRequiredMode()
|
||||||
|
DeviceUnlockMode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> setNotConfiguredMode()
|
||||||
|
DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE -> setKeyManagerNotAvailableMode()
|
||||||
|
DeviceUnlockMode.WAIT_CREDENTIAL -> setWaitCredentialMode()
|
||||||
|
DeviceUnlockMode.STORE_CREDENTIAL -> setStoreCredentialMode()
|
||||||
|
DeviceUnlockMode.EXTRACT_CREDENTIAL -> setExtractCredentialMode()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
mDeviceUnlockViewModel.setException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun manageDeviceCredentialPrompt(
|
||||||
|
state: DeviceUnlockPromptMode
|
||||||
|
) {
|
||||||
|
mDeviceUnlockViewModel.cryptoPrompt?.let { prompt ->
|
||||||
|
when (state) {
|
||||||
|
DeviceUnlockPromptMode.IDLE -> {}
|
||||||
|
DeviceUnlockPromptMode.SHOW -> {
|
||||||
|
openPrompt(prompt)
|
||||||
|
mDeviceUnlockViewModel.promptShown()
|
||||||
|
}
|
||||||
|
DeviceUnlockPromptMode.CLOSE -> {
|
||||||
|
cancelBiometricPrompt()
|
||||||
|
mDeviceUnlockViewModel.biometricPromptClosed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) {
|
||||||
|
try {
|
||||||
|
val promptTitle = getString(cryptoPrompt.titleId)
|
||||||
|
val promptDescription = cryptoPrompt.descriptionId?.let { descriptionId ->
|
||||||
|
getString(descriptionId)
|
||||||
|
} ?: ""
|
||||||
|
|
||||||
|
if (cryptoPrompt.isBiometricOperation) {
|
||||||
|
mBiometricPrompt?.authenticate(
|
||||||
|
BiometricPrompt.PromptInfo.Builder().apply {
|
||||||
|
setTitle(promptTitle)
|
||||||
|
if (promptDescription.isNotEmpty())
|
||||||
|
setDescription(promptDescription)
|
||||||
|
setConfirmationRequired(false)
|
||||||
|
if (isDeviceCredentialBiometricOperation(context)) {
|
||||||
|
setAllowedAuthenticators(DEVICE_CREDENTIAL)
|
||||||
|
} else {
|
||||||
|
setNegativeButtonText(getString(android.R.string.cancel))
|
||||||
|
}
|
||||||
|
}.build(),
|
||||||
|
BiometricPrompt.CryptoObject(cryptoPrompt.cipher))
|
||||||
|
} else if (cryptoPrompt.isDeviceCredentialOperation) {
|
||||||
|
context?.let { context ->
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
mDeviceCredentialResultLauncher?.launch(
|
||||||
|
ContextCompat.getSystemService(
|
||||||
|
context,
|
||||||
|
KeyguardManager::class.java
|
||||||
|
)?.createConfirmDeviceCredentialIntent(
|
||||||
|
promptTitle,
|
||||||
|
promptDescription
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to open prompt", e)
|
||||||
|
mDeviceUnlockViewModel.setException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setNotAvailableMode() {
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
showViews(false)
|
||||||
|
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openBiometricSetting() {
|
||||||
|
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener {
|
||||||
|
try {
|
||||||
|
when {
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
|
||||||
|
context?.startActivity(Intent(Settings.ACTION_BIOMETRIC_ENROLL))
|
||||||
|
}
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> {
|
||||||
|
@Suppress("DEPRECATION") context
|
||||||
|
?.startActivity(Intent(Settings.ACTION_FINGERPRINT_ENROLL))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
context?.startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// ACTION_SECURITY_SETTINGS does not contain fingerprint enrollment on some devices...
|
||||||
|
context?.startActivity(Intent(Settings.ACTION_SETTINGS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setSecurityUpdateRequiredMode() {
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
showViews(true)
|
||||||
|
setAdvancedUnlockedTitleView(R.string.biometric_security_update_required)
|
||||||
|
openBiometricSetting()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setNotConfiguredMode() {
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
showViews(true)
|
||||||
|
setAdvancedUnlockedTitleView(R.string.configure_biometric)
|
||||||
|
openBiometricSetting()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setKeyManagerNotAvailableMode() {
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
showViews(true)
|
||||||
|
setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
|
||||||
|
openBiometricSetting()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setWaitCredentialMode() {
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
showViews(true)
|
||||||
|
setAdvancedUnlockedTitleView(R.string.unavailable)
|
||||||
|
context?.let { context ->
|
||||||
|
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener {
|
||||||
|
mDeviceUnlockViewModel.setException(SecurityException(
|
||||||
|
context.getString(R.string.credential_before_click_advanced_unlock_button)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setStoreCredentialMode() {
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
showViews(true)
|
||||||
|
setAdvancedUnlockedTitleView(R.string.unlock_and_link_biometric)
|
||||||
|
context?.let { context ->
|
||||||
|
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { view ->
|
||||||
|
mDeviceUnlockViewModel.showPrompt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setExtractCredentialMode() {
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
showViews(true)
|
||||||
|
setAdvancedUnlockedTitleView(R.string.unlock)
|
||||||
|
context?.let { context ->
|
||||||
|
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { view ->
|
||||||
|
mDeviceUnlockViewModel.showPrompt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteEncryptedDatabaseKey() {
|
||||||
|
mDeviceUnlockViewModel.deleteEncryptedDatabaseKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showViews(show: Boolean) {
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
if (show) {
|
||||||
|
if (mDeviceUnlockView?.visibility != View.VISIBLE)
|
||||||
|
mDeviceUnlockView?.showByFading()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (mDeviceUnlockView?.visibility == View.VISIBLE)
|
||||||
|
mDeviceUnlockView?.hideByFading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAdvancedUnlockedTitleView(textId: Int) {
|
||||||
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
mDeviceUnlockView?.setTitle(textId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||||
|
Log.e(TAG, "Biometric authentication error. Code : $errorCode Error : $errString")
|
||||||
|
when (errorCode) {
|
||||||
|
BiometricPrompt.ERROR_CANCELED,
|
||||||
|
BiometricPrompt.ERROR_NEGATIVE_BUTTON,
|
||||||
|
BiometricPrompt.ERROR_USER_CANCELED -> {
|
||||||
|
// Ignore negative button
|
||||||
|
}
|
||||||
|
else ->
|
||||||
|
mDeviceUnlockViewModel.setException(SecurityException(errString.toString()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAuthenticationFailed() {
|
||||||
|
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
|
||||||
|
mDeviceUnlockViewModel.setException(
|
||||||
|
SecurityException(getString(R.string.advanced_unlock_not_recognized))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
mDeviceUnlockView = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
mDeviceUnlockViewModel.disconnect()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = DeviceUnlockFragment::class.java.name
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,430 @@
|
|||||||
|
/*
|
||||||
|
* 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.biometric
|
||||||
|
|
||||||
|
import android.app.KeyguardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
|
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||||
|
import android.security.keystore.KeyProperties
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
|
||||||
|
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
|
||||||
|
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import java.security.KeyStore
|
||||||
|
import java.security.UnrecoverableKeyException
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.KeyGenerator
|
||||||
|
import javax.crypto.SecretKey
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
|
class DeviceUnlockManager(private var appContext: Context) {
|
||||||
|
|
||||||
|
private var keyStore: KeyStore? = null
|
||||||
|
private var keyGenerator: KeyGenerator? = null
|
||||||
|
private var cipher: Cipher? = null
|
||||||
|
|
||||||
|
private var biometricUnlockEnable = isBiometricUnlockEnable(appContext)
|
||||||
|
private var deviceCredentialUnlockEnable = isDeviceCredentialUnlockEnable(appContext)
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (biometricUnlockEnable || deviceCredentialUnlockEnable) {
|
||||||
|
if (isDeviceSecure(appContext)) {
|
||||||
|
try {
|
||||||
|
this.keyStore = KeyStore.getInstance(ADVANCED_UNLOCK_KEYSTORE)
|
||||||
|
this.keyGenerator = KeyGenerator.getInstance(
|
||||||
|
ADVANCED_UNLOCK_KEY_ALGORITHM,
|
||||||
|
ADVANCED_UNLOCK_KEYSTORE
|
||||||
|
)
|
||||||
|
this.cipher = Cipher.getInstance(
|
||||||
|
ADVANCED_UNLOCK_KEY_ALGORITHM + "/"
|
||||||
|
+ ADVANCED_UNLOCK_BLOCKS_MODES + "/"
|
||||||
|
+ ADVANCED_UNLOCK_ENCRYPTION_PADDING
|
||||||
|
)
|
||||||
|
if (keyStore == null) {
|
||||||
|
throw SecurityException("Unable to initialize the keystore")
|
||||||
|
}
|
||||||
|
if (keyGenerator == null) {
|
||||||
|
throw SecurityException("Unable to initialize the key generator")
|
||||||
|
}
|
||||||
|
if (cipher == null) {
|
||||||
|
throw SecurityException("Unable to initialize the cipher")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to initialize the device unlock manager", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw SecurityException("Device not secure enough")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized private fun getSecretKey(): SecretKey? {
|
||||||
|
try {
|
||||||
|
// Create new key if needed
|
||||||
|
keyStore?.let { keyStore ->
|
||||||
|
keyStore.load(null)
|
||||||
|
try {
|
||||||
|
if (!keyStore.containsAlias(ADVANCED_UNLOCK_KEYSTORE_KEY)) {
|
||||||
|
// Set the alias of the entry in Android KeyStore where the key will appear
|
||||||
|
// and the constrains (purposes) in the constructor of the Builder
|
||||||
|
keyGenerator?.init(
|
||||||
|
KeyGenParameterSpec.Builder(
|
||||||
|
ADVANCED_UNLOCK_KEYSTORE_KEY,
|
||||||
|
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||||
|
.setBlockModes(ADVANCED_UNLOCK_BLOCKS_MODES)
|
||||||
|
.setEncryptionPaddings(ADVANCED_UNLOCK_ENCRYPTION_PADDING)
|
||||||
|
.apply {
|
||||||
|
// Require the user to authenticate with a fingerprint to authorize every use
|
||||||
|
// of the key, don't use it for device credential because it's the user authentication
|
||||||
|
if (biometricUnlockEnable) {
|
||||||
|
setUserAuthenticationRequired(true)
|
||||||
|
}
|
||||||
|
// To store in the security chip
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||||
|
&& appContext.packageManager.hasSystemFeature(
|
||||||
|
PackageManager.FEATURE_STRONGBOX_KEYSTORE)) {
|
||||||
|
setIsStrongBoxBacked(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.build())
|
||||||
|
keyGenerator?.generateKey()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to create a key in keystore", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
return keyStore.getKey(ADVANCED_UNLOCK_KEYSTORE_KEY, null) as SecretKey?
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to retrieve the key in keystore", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized fun initEncryptData(
|
||||||
|
actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit
|
||||||
|
) {
|
||||||
|
initEncryptData(true, actionIfCypherInit)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized private fun initEncryptData(
|
||||||
|
firstLaunch: Boolean,
|
||||||
|
actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
getSecretKey()?.let { secretKey ->
|
||||||
|
cipher?.let { cipher ->
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
||||||
|
actionIfCypherInit.invoke(
|
||||||
|
DeviceUnlockCryptoPrompt(
|
||||||
|
type = DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION,
|
||||||
|
cipher = cipher,
|
||||||
|
titleId = R.string.advanced_unlock_prompt_store_credential_title,
|
||||||
|
descriptionId = R.string.advanced_unlock_prompt_store_credential_message,
|
||||||
|
isDeviceCredentialOperation = isDeviceCredentialOperation(
|
||||||
|
deviceCredentialUnlockEnable
|
||||||
|
),
|
||||||
|
isBiometricOperation = isBiometricOperation(
|
||||||
|
biometricUnlockEnable, deviceCredentialUnlockEnable
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
||||||
|
Log.e(TAG, "Unable to initialize encrypt data", unrecoverableKeyException)
|
||||||
|
throw unrecoverableKeyException
|
||||||
|
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
||||||
|
Log.e(TAG, "Unable to initialize encrypt data", invalidKeyException)
|
||||||
|
if (firstLaunch) {
|
||||||
|
deleteAllEntryKeysInKeystoreForBiometric(appContext)
|
||||||
|
initEncryptData(false, actionIfCypherInit)
|
||||||
|
} else {
|
||||||
|
throw invalidKeyException
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to initialize encrypt data", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized fun encryptData(
|
||||||
|
value: ByteArray,
|
||||||
|
cipher: Cipher?,
|
||||||
|
handleEncryptedResult: (encryptedValue: ByteArray, ivSpec: ByteArray) -> Unit
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
val encrypted = cipher?.doFinal(value) ?: byteArrayOf()
|
||||||
|
// passes updated iv spec on to callback so this can be stored for decryption
|
||||||
|
cipher?.parameters?.getParameterSpec(IvParameterSpec::class.java)?.let{ spec ->
|
||||||
|
handleEncryptedResult.invoke(encrypted, spec.iv)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to encrypt data", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized fun initDecryptData(
|
||||||
|
ivSpecValue: ByteArray,
|
||||||
|
actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit
|
||||||
|
) {
|
||||||
|
initDecryptData(ivSpecValue, true, actionIfCypherInit)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized private fun initDecryptData(
|
||||||
|
ivSpecValue: ByteArray,
|
||||||
|
firstLaunch: Boolean = true,
|
||||||
|
actionIfCypherInit: (cryptoPrompt: DeviceUnlockCryptoPrompt) -> Unit
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// important to restore spec here that was used for decryption
|
||||||
|
val spec = IvParameterSpec(ivSpecValue)
|
||||||
|
getSecretKey()?.let { secretKey ->
|
||||||
|
cipher?.let { cipher ->
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
|
||||||
|
actionIfCypherInit.invoke(
|
||||||
|
DeviceUnlockCryptoPrompt(
|
||||||
|
type = DeviceUnlockCryptoPromptType.CREDENTIAL_DECRYPTION,
|
||||||
|
cipher = cipher,
|
||||||
|
titleId = R.string.advanced_unlock_prompt_extract_credential_title,
|
||||||
|
descriptionId = null,
|
||||||
|
isDeviceCredentialOperation = isDeviceCredentialOperation(
|
||||||
|
deviceCredentialUnlockEnable
|
||||||
|
),
|
||||||
|
isBiometricOperation = isBiometricOperation(
|
||||||
|
biometricUnlockEnable, deviceCredentialUnlockEnable
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (unrecoverableKeyException: UnrecoverableKeyException) {
|
||||||
|
Log.e(TAG, "Unable to initialize decrypt data", unrecoverableKeyException)
|
||||||
|
if (firstLaunch) {
|
||||||
|
deleteKeystoreKey()
|
||||||
|
initDecryptData(ivSpecValue, false, actionIfCypherInit)
|
||||||
|
} else {
|
||||||
|
throw unrecoverableKeyException
|
||||||
|
}
|
||||||
|
} catch (invalidKeyException: KeyPermanentlyInvalidatedException) {
|
||||||
|
Log.e(TAG, "Unable to initialize decrypt data", invalidKeyException)
|
||||||
|
if (firstLaunch) {
|
||||||
|
deleteAllEntryKeysInKeystoreForBiometric(appContext)
|
||||||
|
initDecryptData(ivSpecValue, false, actionIfCypherInit)
|
||||||
|
} else {
|
||||||
|
throw invalidKeyException
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to initialize decrypt data", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized fun decryptData(
|
||||||
|
encryptedValue: ByteArray,
|
||||||
|
cipher: Cipher?,
|
||||||
|
handleDecryptedResult: (decryptedValue: ByteArray) -> Unit
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// actual decryption here
|
||||||
|
cipher?.doFinal(encryptedValue)?.let { decrypted ->
|
||||||
|
handleDecryptedResult.invoke(decrypted)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to decrypt data", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized fun deleteKeystoreKey() {
|
||||||
|
try {
|
||||||
|
keyStore?.load(null)
|
||||||
|
keyStore?.deleteEntry(ADVANCED_UNLOCK_KEYSTORE_KEY)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to delete entry key in keystore", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val TAG = DeviceUnlockManager::class.java.name
|
||||||
|
|
||||||
|
private const val ADVANCED_UNLOCK_KEYSTORE = "AndroidKeyStore"
|
||||||
|
private const val ADVANCED_UNLOCK_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key"
|
||||||
|
private const val ADVANCED_UNLOCK_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
|
||||||
|
private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
|
||||||
|
private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
|
fun canAuthenticate(context: Context): Int {
|
||||||
|
return try {
|
||||||
|
BiometricManager.from(context).canAuthenticate(
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
|
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
|
||||||
|
BIOMETRIC_STRONG or DEVICE_CREDENTIAL
|
||||||
|
} else {
|
||||||
|
BIOMETRIC_STRONG
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
|
||||||
|
try {
|
||||||
|
BiometricManager.from(context).canAuthenticate(
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
|
&& PreferencesUtil.isDeviceCredentialUnlockEnable(context)) {
|
||||||
|
BIOMETRIC_WEAK or DEVICE_CREDENTIAL
|
||||||
|
} else {
|
||||||
|
BIOMETRIC_WEAK
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
|
||||||
|
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isDeviceSecure(context: Context): Boolean {
|
||||||
|
return ContextCompat.getSystemService(context, KeyguardManager::class.java)
|
||||||
|
?.isDeviceSecure ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun biometricUnlockSupported(context: Context): Boolean {
|
||||||
|
val biometricCanAuthenticate = try {
|
||||||
|
BiometricManager.from(context).canAuthenticate(BIOMETRIC_STRONG)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to authenticate with strong biometric.", e)
|
||||||
|
try {
|
||||||
|
BiometricManager.from(context).canAuthenticate(BIOMETRIC_WEAK)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to authenticate with weak biometric.", e)
|
||||||
|
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deviceCredentialUnlockSupported(context: Context): Boolean {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
val biometricCanAuthenticate = BiometricManager.from(context).canAuthenticate(DEVICE_CREDENTIAL)
|
||||||
|
(biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_STATUS_UNKNOWN
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove entry key in keystore
|
||||||
|
*/
|
||||||
|
fun deleteEntryKeyInKeystoreForBiometric(
|
||||||
|
appContext: Context
|
||||||
|
) {
|
||||||
|
DeviceUnlockManager(appContext).apply {
|
||||||
|
deleteKeystoreKey()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteAllEntryKeysInKeystoreForBiometric(appContext: Context) {
|
||||||
|
try {
|
||||||
|
deleteEntryKeyInKeystoreForBiometric(appContext)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(appContext,
|
||||||
|
deviceUnlockError(e, appContext),
|
||||||
|
Toast.LENGTH_SHORT).show()
|
||||||
|
} finally {
|
||||||
|
CipherDatabaseAction.getInstance(appContext).deleteAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deviceUnlockError(error: Exception, context: Context): String {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||||
|
&& (error is UnrecoverableKeyException
|
||||||
|
|| error is KeyPermanentlyInvalidatedException)) {
|
||||||
|
context.getString(R.string.advanced_unlock_invalid_key)
|
||||||
|
} else
|
||||||
|
error.cause?.localizedMessage
|
||||||
|
?: error.localizedMessage
|
||||||
|
?: error.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isBiometricUnlockEnable(appContext: Context) =
|
||||||
|
PreferencesUtil.isBiometricUnlockEnable(appContext)
|
||||||
|
|
||||||
|
fun isDeviceCredentialUnlockEnable(appContext: Context) =
|
||||||
|
PreferencesUtil.isDeviceCredentialUnlockEnable(appContext)
|
||||||
|
|
||||||
|
private fun isBiometricOperation(
|
||||||
|
biometricUnlockEnable: Boolean,
|
||||||
|
deviceCredentialUnlockEnable: Boolean
|
||||||
|
): Boolean {
|
||||||
|
return biometricUnlockEnable
|
||||||
|
|| isDeviceCredentialBiometricOperation(deviceCredentialUnlockEnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since Android 30, device credential is also a biometric operation
|
||||||
|
private fun isDeviceCredentialOperation(
|
||||||
|
deviceCredentialUnlockEnable: Boolean
|
||||||
|
): Boolean {
|
||||||
|
return Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
||||||
|
&& deviceCredentialUnlockEnable
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isDeviceCredentialBiometricOperation(
|
||||||
|
deviceCredentialUnlockEnable: Boolean
|
||||||
|
): Boolean {
|
||||||
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
|
&& deviceCredentialUnlockEnable
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isDeviceCredentialBiometricOperation(context: Context?): Boolean {
|
||||||
|
if (context == null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return isDeviceCredentialBiometricOperation(
|
||||||
|
isDeviceCredentialUnlockEnable(context)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.kunzisoft.keepass.biometric
|
||||||
|
|
||||||
|
enum class DeviceUnlockMode {
|
||||||
|
BIOMETRIC_UNAVAILABLE,
|
||||||
|
BIOMETRIC_SECURITY_UPDATE_REQUIRED,
|
||||||
|
DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED,
|
||||||
|
KEY_MANAGER_UNAVAILABLE,
|
||||||
|
WAIT_CREDENTIAL,
|
||||||
|
STORE_CREDENTIAL,
|
||||||
|
EXTRACT_CREDENTIAL
|
||||||
|
}
|
||||||
@@ -1,63 +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.biometric
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import android.widget.ImageView
|
|
||||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
|
||||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
|
||||||
class FingerPrintAnimatedVector(context: Context, imageView: ImageView) {
|
|
||||||
|
|
||||||
private val scanFingerprint: AnimatedVectorDrawableCompat? =
|
|
||||||
AnimatedVectorDrawableCompat.create(context, R.drawable.scan_fingerprint)
|
|
||||||
|
|
||||||
init {
|
|
||||||
imageView.setImageDrawable(scanFingerprint)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var animationCallback = object : Animatable2Compat.AnimationCallback() {
|
|
||||||
override fun onAnimationEnd(drawable: Drawable) {
|
|
||||||
imageView.post {
|
|
||||||
scanFingerprint?.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startScan() {
|
|
||||||
scanFingerprint?.registerAnimationCallback(animationCallback)
|
|
||||||
|
|
||||||
if (scanFingerprint?.isRunning != true)
|
|
||||||
scanFingerprint?.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stopScan() {
|
|
||||||
scanFingerprint?.unregisterAnimationCallback(animationCallback)
|
|
||||||
|
|
||||||
if (scanFingerprint?.isRunning == true)
|
|
||||||
scanFingerprint.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.kunzisoft.keepass.database
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
|
||||||
|
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||||
|
import com.kunzisoft.keepass.utils.SingletonHolder
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class ContextualDatabase: Database() {
|
||||||
|
|
||||||
|
var fileUri: Uri? = null
|
||||||
|
|
||||||
|
val iconDrawableFactory = IconDrawableFactory(
|
||||||
|
retrieveBinaryCache = { binaryCache },
|
||||||
|
retrieveCustomIconBinary = { iconId -> getBinaryForCustomIcon(iconId) }
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun removeCustomIcon(customIcon: IconImageCustom) {
|
||||||
|
iconDrawableFactory.clearFromCache(customIcon)
|
||||||
|
super.removeCustomIcon(customIcon)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearIndexesAndBinaries(filesDirectory: File?) {
|
||||||
|
iconDrawableFactory.clearCache()
|
||||||
|
super.clearIndexesAndBinaries(filesDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearAndClose(filesDirectory: File?) {
|
||||||
|
super.clearAndClose(filesDirectory)
|
||||||
|
this.fileUri = null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : SingletonHolder<ContextualDatabase>(::ContextualDatabase) {
|
||||||
|
private val TAG = ContextualDatabase::class.java.name
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,17 +17,30 @@
|
|||||||
* 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.database.action
|
package com.kunzisoft.keepass.database
|
||||||
|
|
||||||
import android.app.Service
|
import android.Manifest
|
||||||
import android.content.*
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context.*
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Context.BIND_ABOVE_CLIENT
|
||||||
|
import android.content.Context.BIND_AUTO_CREATE
|
||||||
|
import android.content.Context.BIND_IMPORTANT
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
@@ -35,7 +48,6 @@ import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment
|
|||||||
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
|
import com.kunzisoft.keepass.activities.dialogs.DatabaseChangedDialogFragment.Companion.DATABASE_CHANGED_DIALOG_TAG
|
||||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
@@ -43,10 +55,10 @@ import com.kunzisoft.keepass.database.element.node.Node
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
|
||||||
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_CHALLENGE_RESPONDED
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK
|
||||||
@@ -82,26 +94,33 @@ import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
|||||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
|
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
|
||||||
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
|
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
|
||||||
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
||||||
|
import com.kunzisoft.keepass.utils.putParcelableList
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class to connect an activity or a service to the DatabaseTaskNotificationService,
|
* Utility class to connect an activity or a service to the DatabaseTaskNotificationService,
|
||||||
* Useful to retrieve a database instance and sending tasks commands
|
* Useful to retrieve a database instance and sending tasks commands
|
||||||
*/
|
*/
|
||||||
class DatabaseTaskProvider {
|
class DatabaseTaskProvider(
|
||||||
|
private var context: Context,
|
||||||
|
private var showDialog: Boolean = true
|
||||||
|
) {
|
||||||
|
|
||||||
private var activity: FragmentActivity? = null
|
// To show dialog only if context is an activity
|
||||||
private var service: Service? = null
|
private var activity: FragmentActivity? = try { context as? FragmentActivity? }
|
||||||
private var context: Context
|
catch (_: Exception) { null }
|
||||||
|
|
||||||
var onDatabaseRetrieved: ((database: Database?) -> Unit)? = null
|
var onDatabaseRetrieved: ((database: ContextualDatabase?) -> Unit)? = null
|
||||||
|
|
||||||
var onActionFinish: ((database: Database,
|
var onActionFinish: ((database: ContextualDatabase,
|
||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result) -> Unit)? = null
|
result: ActionRunnable.Result) -> Unit)? = null
|
||||||
|
|
||||||
private var intentDatabaseTask: Intent
|
private var intentDatabaseTask: Intent = Intent(
|
||||||
|
context.applicationContext,
|
||||||
|
DatabaseTaskNotificationService::class.java
|
||||||
|
)
|
||||||
|
|
||||||
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
|
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
|
||||||
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
|
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
|
||||||
@@ -111,34 +130,49 @@ class DatabaseTaskProvider {
|
|||||||
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
|
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
|
||||||
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
|
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
|
||||||
|
|
||||||
constructor(activity: FragmentActivity) {
|
fun destroy() {
|
||||||
this.activity = activity
|
this.activity = null
|
||||||
this.context = activity
|
this.onDatabaseRetrieved = null
|
||||||
this.intentDatabaseTask = Intent(activity.applicationContext,
|
this.onActionFinish = null
|
||||||
DatabaseTaskNotificationService::class.java)
|
this.databaseTaskBroadcastReceiver = null
|
||||||
}
|
this.mBinder = null
|
||||||
|
this.serviceConnection = null
|
||||||
constructor(service: Service) {
|
this.progressTaskDialogFragment = null
|
||||||
this.service = service
|
this.databaseChangedDialogFragment = null
|
||||||
this.context = service
|
|
||||||
this.intentDatabaseTask = Intent(service.applicationContext,
|
|
||||||
DatabaseTaskNotificationService::class.java)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
|
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
|
||||||
override fun onStartAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) {
|
override fun onActionStarted(
|
||||||
startDialog(titleId, messageId, warningId)
|
database: ContextualDatabase,
|
||||||
|
progressMessage: ProgressMessage
|
||||||
|
) {
|
||||||
|
if (showDialog)
|
||||||
|
startDialog(progressMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUpdateAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) {
|
override fun onActionUpdated(
|
||||||
updateDialog(titleId, messageId, warningId)
|
database: ContextualDatabase,
|
||||||
|
progressMessage: ProgressMessage
|
||||||
|
) {
|
||||||
|
if (showDialog)
|
||||||
|
updateDialog(progressMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStopAction(database: Database, actionTask: String, result: ActionRunnable.Result) {
|
override fun onActionStopped(
|
||||||
onActionFinish?.invoke(database, actionTask, result)
|
database: ContextualDatabase
|
||||||
|
) {
|
||||||
// Remove the progress task
|
// Remove the progress task
|
||||||
stopDialog()
|
stopDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onActionFinished(
|
||||||
|
database: ContextualDatabase,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
onActionFinish?.invoke(database, actionTask, result)
|
||||||
|
onActionStopped(database)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mActionDatabaseListener = object: DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
|
private val mActionDatabaseListener = object: DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
|
||||||
@@ -147,9 +181,13 @@ class DatabaseTaskProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var databaseInfoListener = object: DatabaseTaskNotificationService.DatabaseInfoListener {
|
private var databaseInfoListener = object:
|
||||||
override fun onDatabaseInfoChanged(previousDatabaseInfo: SnapFileDatabaseInfo,
|
DatabaseTaskNotificationService.DatabaseInfoListener {
|
||||||
newDatabaseInfo: SnapFileDatabaseInfo) {
|
override fun onDatabaseInfoChanged(
|
||||||
|
previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
newDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
readOnlyDatabase: Boolean
|
||||||
|
) {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
activity.lifecycleScope.launch {
|
activity.lifecycleScope.launch {
|
||||||
if (databaseChangedDialogFragment == null) {
|
if (databaseChangedDialogFragment == null) {
|
||||||
@@ -161,7 +199,8 @@ class DatabaseTaskProvider {
|
|||||||
if (progressTaskDialogFragment == null) {
|
if (progressTaskDialogFragment == null) {
|
||||||
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
|
databaseChangedDialogFragment = DatabaseChangedDialogFragment.getInstance(
|
||||||
previousDatabaseInfo,
|
previousDatabaseInfo,
|
||||||
newDatabaseInfo
|
newDatabaseInfo,
|
||||||
|
readOnlyDatabase
|
||||||
)
|
)
|
||||||
databaseChangedDialogFragment?.actionDatabaseListener =
|
databaseChangedDialogFragment?.actionDatabaseListener =
|
||||||
mActionDatabaseListener
|
mActionDatabaseListener
|
||||||
@@ -176,14 +215,12 @@ class DatabaseTaskProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var databaseListener = object: DatabaseTaskNotificationService.DatabaseListener {
|
private var databaseListener = object: DatabaseTaskNotificationService.DatabaseListener {
|
||||||
override fun onDatabaseRetrieved(database: Database?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
onDatabaseRetrieved?.invoke(database)
|
onDatabaseRetrieved?.invoke(database)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startDialog(titleId: Int? = null,
|
private fun startDialog(progressMessage: ProgressMessage) {
|
||||||
messageId: Int? = null,
|
|
||||||
warningId: Int? = null) {
|
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
activity.lifecycleScope.launch {
|
activity.lifecycleScope.launch {
|
||||||
if (progressTaskDialogFragment == null) {
|
if (progressTaskDialogFragment == null) {
|
||||||
@@ -197,22 +234,17 @@ class DatabaseTaskProvider {
|
|||||||
PROGRESS_TASK_DIALOG_TAG
|
PROGRESS_TASK_DIALOG_TAG
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
updateDialog(titleId, messageId, warningId)
|
updateDialog(progressMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateDialog(titleId: Int?, messageId: Int?, warningId: Int?) {
|
private fun updateDialog(progressMessage: ProgressMessage) {
|
||||||
progressTaskDialogFragment?.apply {
|
progressTaskDialogFragment?.apply {
|
||||||
titleId?.let {
|
updateTitle(progressMessage.titleId)
|
||||||
updateTitle(it)
|
updateMessage(progressMessage.messageId)
|
||||||
}
|
updateWarning(progressMessage.warningId)
|
||||||
messageId?.let {
|
setCancellable(progressMessage.cancelable)
|
||||||
updateMessage(it)
|
|
||||||
}
|
|
||||||
warningId?.let {
|
|
||||||
updateWarning(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,11 +256,17 @@ class DatabaseTaskProvider {
|
|||||||
private fun initServiceConnection() {
|
private fun initServiceConnection() {
|
||||||
if (serviceConnection == null) {
|
if (serviceConnection == null) {
|
||||||
serviceConnection = object : ServiceConnection {
|
serviceConnection = object : ServiceConnection {
|
||||||
|
override fun onBindingDied(name: ComponentName?) {
|
||||||
|
stopDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNullBinding(name: ComponentName?) {
|
||||||
|
stopDialog()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||||
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
|
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
|
||||||
addDatabaseListener(databaseListener)
|
addServiceListeners(this)
|
||||||
addDatabaseFileInfoListener(databaseInfoListener)
|
|
||||||
addActionTaskListener(actionTaskListener)
|
|
||||||
getService().checkDatabase()
|
getService().checkDatabase()
|
||||||
getService().checkDatabaseInfo()
|
getService().checkDatabaseInfo()
|
||||||
getService().checkAction()
|
getService().checkAction()
|
||||||
@@ -236,19 +274,29 @@ class DatabaseTaskProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceDisconnected(name: ComponentName?) {
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
mBinder?.removeActionTaskListener(actionTaskListener)
|
removeServiceListeners(mBinder)
|
||||||
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
|
|
||||||
mBinder?.removeDatabaseListener(databaseListener)
|
|
||||||
mBinder = null
|
mBinder = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun addServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
|
||||||
|
service?.addDatabaseListener(databaseListener)
|
||||||
|
service?.addDatabaseFileInfoListener(databaseInfoListener)
|
||||||
|
service?.addActionTaskListener(actionTaskListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
|
||||||
|
service?.removeActionTaskListener(actionTaskListener)
|
||||||
|
service?.removeDatabaseFileInfoListener(databaseInfoListener)
|
||||||
|
service?.removeDatabaseListener(databaseListener)
|
||||||
|
}
|
||||||
|
|
||||||
private fun bindService() {
|
private fun bindService() {
|
||||||
initServiceConnection()
|
initServiceConnection()
|
||||||
serviceConnection?.let {
|
serviceConnection?.let {
|
||||||
context.bindService(intentDatabaseTask, it, BIND_AUTO_CREATE or BIND_NOT_FOREGROUND or BIND_ABOVE_CLIENT)
|
context.bindService(intentDatabaseTask, it, BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,19 +304,18 @@ class DatabaseTaskProvider {
|
|||||||
* Unbind the service and assign null to the service connection to check if already unbind or not
|
* Unbind the service and assign null to the service connection to check if already unbind or not
|
||||||
*/
|
*/
|
||||||
private fun unBindService() {
|
private fun unBindService() {
|
||||||
serviceConnection?.let {
|
try {
|
||||||
context.unbindService(it)
|
serviceConnection?.let {
|
||||||
|
context.unbindService(it)
|
||||||
|
}
|
||||||
|
} catch (e: java.lang.Exception) {
|
||||||
|
Log.e(TAG, "Unable to unbind the database task service", e)
|
||||||
|
} finally {
|
||||||
|
serviceConnection = null
|
||||||
}
|
}
|
||||||
serviceConnection = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isBinded(): Boolean {
|
|
||||||
return mBinder != null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun registerProgressTask() {
|
fun registerProgressTask() {
|
||||||
stopDialog()
|
|
||||||
|
|
||||||
// Register a database task receiver to stop loading dialog when service finish the task
|
// Register a database task receiver to stop loading dialog when service finish the task
|
||||||
databaseTaskBroadcastReceiver = object : BroadcastReceiver() {
|
databaseTaskBroadcastReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
@@ -279,17 +326,16 @@ class DatabaseTaskProvider {
|
|||||||
}
|
}
|
||||||
DATABASE_STOP_TASK_ACTION -> {
|
DATABASE_STOP_TASK_ACTION -> {
|
||||||
// Remove the progress task
|
// Remove the progress task
|
||||||
stopDialog()
|
|
||||||
unBindService()
|
unBindService()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
context.registerReceiver(databaseTaskBroadcastReceiver,
|
ContextCompat.registerReceiver(context, databaseTaskBroadcastReceiver,
|
||||||
IntentFilter().apply {
|
IntentFilter().apply {
|
||||||
addAction(DATABASE_START_TASK_ACTION)
|
addAction(DATABASE_START_TASK_ACTION)
|
||||||
addAction(DATABASE_STOP_TASK_ACTION)
|
addAction(DATABASE_STOP_TASK_ACTION)
|
||||||
}
|
}, RECEIVER_NOT_EXPORTED
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check if a service is currently running else do nothing
|
// Check if a service is currently running else do nothing
|
||||||
@@ -297,11 +343,7 @@ class DatabaseTaskProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun unregisterProgressTask() {
|
fun unregisterProgressTask() {
|
||||||
stopDialog()
|
removeServiceListeners(mBinder)
|
||||||
|
|
||||||
mBinder?.removeActionTaskListener(actionTaskListener)
|
|
||||||
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
|
|
||||||
mBinder?.removeDatabaseListener(databaseListener)
|
|
||||||
mBinder = null
|
mBinder = null
|
||||||
|
|
||||||
unBindService()
|
unBindService()
|
||||||
@@ -313,7 +355,50 @@ class DatabaseTaskProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val tempServiceParameters = mutableListOf<Pair<Bundle?, String>>()
|
||||||
|
private val requestPermissionLauncher = activity?.registerForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { _ ->
|
||||||
|
// Whether or not the user has accepted, the service can be started,
|
||||||
|
// There just won't be any notification if it's not allowed.
|
||||||
|
tempServiceParameters.removeFirstOrNull()?.let {
|
||||||
|
startService(it.first, it.second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun start(bundle: Bundle? = null, actionTask: String) {
|
private fun start(bundle: Bundle? = null, actionTask: String) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
val contextActivity = activity
|
||||||
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
== PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
startService(bundle, actionTask)
|
||||||
|
} else if (contextActivity != null && shouldShowRequestPermissionRationale(
|
||||||
|
contextActivity,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// it's not the first time, so the user deliberately chooses not to display the notification
|
||||||
|
startService(bundle, actionTask)
|
||||||
|
} else {
|
||||||
|
AlertDialog.Builder(context)
|
||||||
|
.setMessage(R.string.warning_database_notification_permission)
|
||||||
|
.setNegativeButton(R.string.later) { _, _ ->
|
||||||
|
// Refuses the notification, so start the service
|
||||||
|
startService(bundle, actionTask)
|
||||||
|
}
|
||||||
|
.setPositiveButton(R.string.ask) { _, _ ->
|
||||||
|
// Save the temp parameters to ask the permission
|
||||||
|
tempServiceParameters.add(Pair(bundle, actionTask))
|
||||||
|
requestPermissionLauncher?.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
}.create().show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
startService(bundle, actionTask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startService(bundle: Bundle? = null, actionTask: String) {
|
||||||
try {
|
try {
|
||||||
if (bundle != null)
|
if (bundle != null)
|
||||||
intentDatabaseTask.putExtras(bundle)
|
intentDatabaseTask.putExtras(bundle)
|
||||||
@@ -321,7 +406,7 @@ class DatabaseTaskProvider {
|
|||||||
context.startService(intentDatabaseTask)
|
context.startService(intentDatabaseTask)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to perform database action", e)
|
Log.e(TAG, "Unable to perform database action", e)
|
||||||
Toast.makeText(activity, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
|
Toast.makeText(context, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,12 +417,13 @@ class DatabaseTaskProvider {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
fun startDatabaseCreate(databaseUri: Uri,
|
fun startDatabaseCreate(databaseUri: Uri,
|
||||||
mainCredential: MainCredential) {
|
mainCredential: MainCredential
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_CREATE_TASK)
|
, ACTION_DATABASE_CREATE_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseLoad(databaseUri: Uri,
|
fun startDatabaseLoad(databaseUri: Uri,
|
||||||
@@ -352,12 +438,14 @@ class DatabaseTaskProvider {
|
|||||||
putParcelable(DatabaseTaskNotificationService.CIPHER_DATABASE_KEY, cipherEncryptDatabase)
|
putParcelable(DatabaseTaskNotificationService.CIPHER_DATABASE_KEY, cipherEncryptDatabase)
|
||||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_LOAD_TASK)
|
, ACTION_DATABASE_LOAD_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseMerge(fromDatabaseUri: Uri? = null,
|
fun startDatabaseMerge(save: Boolean,
|
||||||
|
fromDatabaseUri: Uri? = null,
|
||||||
mainCredential: MainCredential? = null) {
|
mainCredential: MainCredential? = null) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri)
|
||||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||||
}
|
}
|
||||||
@@ -368,7 +456,7 @@ class DatabaseTaskProvider {
|
|||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_RELOAD_TASK)
|
, ACTION_DATABASE_RELOAD_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun askToStartDatabaseReload(conditionToAsk: Boolean, approved: () -> Unit) {
|
fun askToStartDatabaseReload(conditionToAsk: Boolean, approved: () -> Unit) {
|
||||||
@@ -384,14 +472,15 @@ class DatabaseTaskProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseAssignPassword(databaseUri: Uri,
|
fun startDatabaseAssignCredential(databaseUri: Uri,
|
||||||
mainCredential: MainCredential) {
|
mainCredential: MainCredential
|
||||||
|
) {
|
||||||
|
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_ASSIGN_PASSWORD_TASK)
|
, ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -408,7 +497,7 @@ class DatabaseTaskProvider {
|
|||||||
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
|
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_CREATE_GROUP_TASK)
|
, ACTION_DATABASE_CREATE_GROUP_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseUpdateGroup(oldGroup: Group,
|
fun startDatabaseUpdateGroup(oldGroup: Group,
|
||||||
@@ -419,7 +508,7 @@ class DatabaseTaskProvider {
|
|||||||
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, groupToUpdate)
|
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, groupToUpdate)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_UPDATE_GROUP_TASK)
|
, ACTION_DATABASE_UPDATE_GROUP_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseCreateEntry(newEntry: Entry,
|
fun startDatabaseCreateEntry(newEntry: Entry,
|
||||||
@@ -430,7 +519,7 @@ class DatabaseTaskProvider {
|
|||||||
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
|
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_CREATE_ENTRY_TASK)
|
, ACTION_DATABASE_CREATE_ENTRY_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseUpdateEntry(oldEntry: Entry,
|
fun startDatabaseUpdateEntry(oldEntry: Entry,
|
||||||
@@ -441,7 +530,7 @@ class DatabaseTaskProvider {
|
|||||||
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, entryToUpdate)
|
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, entryToUpdate)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_UPDATE_ENTRY_TASK)
|
, ACTION_DATABASE_UPDATE_ENTRY_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startDatabaseActionListNodes(actionTask: String,
|
private fun startDatabaseActionListNodes(actionTask: String,
|
||||||
@@ -464,13 +553,13 @@ class DatabaseTaskProvider {
|
|||||||
|
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putAll(getBundleFromListNodes(nodesPaste))
|
putAll(getBundleFromListNodes(nodesPaste))
|
||||||
putParcelableArrayList(DatabaseTaskNotificationService.GROUPS_ID_KEY, groupsIdToCopy)
|
putParcelableList(DatabaseTaskNotificationService.GROUPS_ID_KEY, groupsIdToCopy)
|
||||||
putParcelableArrayList(DatabaseTaskNotificationService.ENTRIES_ID_KEY, entriesIdToCopy)
|
putParcelableList(DatabaseTaskNotificationService.ENTRIES_ID_KEY, entriesIdToCopy)
|
||||||
if (newParentId != null)
|
if (newParentId != null)
|
||||||
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, newParentId)
|
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, newParentId)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}
|
||||||
, actionTask)
|
, actionTask)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseCopyNodes(nodesToCopy: List<Node>,
|
fun startDatabaseCopyNodes(nodesToCopy: List<Node>,
|
||||||
@@ -504,7 +593,7 @@ class DatabaseTaskProvider {
|
|||||||
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
|
, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseDeleteEntryHistory(mainEntryId: NodeId<UUID>,
|
fun startDatabaseDeleteEntryHistory(mainEntryId: NodeId<UUID>,
|
||||||
@@ -515,7 +604,7 @@ class DatabaseTaskProvider {
|
|||||||
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_DELETE_ENTRY_HISTORY)
|
, ACTION_DATABASE_DELETE_ENTRY_HISTORY)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -532,7 +621,7 @@ class DatabaseTaskProvider {
|
|||||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newName)
|
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newName)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_UPDATE_NAME_TASK)
|
, ACTION_DATABASE_UPDATE_NAME_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveDescription(oldDescription: String,
|
fun startDatabaseSaveDescription(oldDescription: String,
|
||||||
@@ -543,7 +632,7 @@ class DatabaseTaskProvider {
|
|||||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDescription)
|
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDescription)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_UPDATE_DESCRIPTION_TASK)
|
, ACTION_DATABASE_UPDATE_DESCRIPTION_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveDefaultUsername(oldDefaultUsername: String,
|
fun startDatabaseSaveDefaultUsername(oldDefaultUsername: String,
|
||||||
@@ -554,7 +643,7 @@ class DatabaseTaskProvider {
|
|||||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDefaultUsername)
|
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDefaultUsername)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK)
|
, ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveColor(oldColor: String,
|
fun startDatabaseSaveColor(oldColor: String,
|
||||||
@@ -565,7 +654,7 @@ class DatabaseTaskProvider {
|
|||||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newColor)
|
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newColor)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_UPDATE_COLOR_TASK)
|
, ACTION_DATABASE_UPDATE_COLOR_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveCompression(oldCompression: CompressionAlgorithm,
|
fun startDatabaseSaveCompression(oldCompression: CompressionAlgorithm,
|
||||||
@@ -576,14 +665,14 @@ class DatabaseTaskProvider {
|
|||||||
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newCompression)
|
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newCompression)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
|
, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseRemoveUnlinkedData(save: Boolean) {
|
fun startDatabaseRemoveUnlinkedData(save: Boolean) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
|
, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveRecycleBin(oldRecycleBin: Group?,
|
fun startDatabaseSaveRecycleBin(oldRecycleBin: Group?,
|
||||||
@@ -616,7 +705,7 @@ class DatabaseTaskProvider {
|
|||||||
putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistoryItems)
|
putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistoryItems)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK)
|
, ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveMaxHistorySize(oldMaxHistorySize: Long,
|
fun startDatabaseSaveMaxHistorySize(oldMaxHistorySize: Long,
|
||||||
@@ -627,7 +716,7 @@ class DatabaseTaskProvider {
|
|||||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistorySize)
|
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistorySize)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK)
|
, ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -644,7 +733,7 @@ class DatabaseTaskProvider {
|
|||||||
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newEncryption)
|
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newEncryption)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_UPDATE_ENCRYPTION_TASK)
|
, ACTION_DATABASE_UPDATE_ENCRYPTION_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveKeyDerivation(oldKeyDerivation: KdfEngine,
|
fun startDatabaseSaveKeyDerivation(oldKeyDerivation: KdfEngine,
|
||||||
@@ -655,7 +744,7 @@ class DatabaseTaskProvider {
|
|||||||
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newKeyDerivation)
|
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newKeyDerivation)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK)
|
, ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveIterations(oldIterations: Long,
|
fun startDatabaseSaveIterations(oldIterations: Long,
|
||||||
@@ -666,7 +755,7 @@ class DatabaseTaskProvider {
|
|||||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newIterations)
|
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newIterations)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_UPDATE_ITERATIONS_TASK)
|
, ACTION_DATABASE_UPDATE_ITERATIONS_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveMemoryUsage(oldMemoryUsage: Long,
|
fun startDatabaseSaveMemoryUsage(oldMemoryUsage: Long,
|
||||||
@@ -677,7 +766,7 @@ class DatabaseTaskProvider {
|
|||||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMemoryUsage)
|
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMemoryUsage)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
|
, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveParallelism(oldParallelism: Long,
|
fun startDatabaseSaveParallelism(oldParallelism: Long,
|
||||||
@@ -688,7 +777,7 @@ class DatabaseTaskProvider {
|
|||||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newParallelism)
|
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newParallelism)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_UPDATE_PARALLELISM_TASK)
|
, ACTION_DATABASE_UPDATE_PARALLELISM_TASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -699,10 +788,17 @@ class DatabaseTaskProvider {
|
|||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, saveToUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, saveToUri)
|
||||||
}
|
}
|
||||||
, ACTION_DATABASE_SAVE)
|
, ACTION_DATABASE_SAVE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startChallengeResponded(response: ByteArray?) {
|
||||||
|
start(Bundle().apply {
|
||||||
|
putByteArray(DatabaseTaskNotificationService.DATA_BYTES, response)
|
||||||
|
}
|
||||||
|
, ACTION_CHALLENGE_RESPONDED)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = DatabaseTaskProvider::class.java.name
|
private val TAG = DatabaseTaskProvider::class.java.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
* 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.database
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.database.element.MasterCredential
|
||||||
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
|
import com.kunzisoft.keepass.utils.readParcelableCompat
|
||||||
|
import com.kunzisoft.keepass.utils.getUriInputStream
|
||||||
|
import com.kunzisoft.keepass.utils.readEnum
|
||||||
|
import com.kunzisoft.keepass.utils.writeEnum
|
||||||
|
|
||||||
|
data class MainCredential(var password: String? = null,
|
||||||
|
var keyFileUri: Uri? = null,
|
||||||
|
var hardwareKey: HardwareKey? = null): Parcelable {
|
||||||
|
|
||||||
|
constructor(parcel: Parcel) : this() {
|
||||||
|
password = parcel.readString()
|
||||||
|
keyFileUri = parcel.readParcelableCompat()
|
||||||
|
hardwareKey = parcel.readEnum<HardwareKey>()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeString(password)
|
||||||
|
parcel.writeParcelable(keyFileUri, flags)
|
||||||
|
parcel.writeEnum(hardwareKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun describeContents(): Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as MainCredential
|
||||||
|
|
||||||
|
if (password != other.password) return false
|
||||||
|
if (keyFileUri != other.keyFileUri) return false
|
||||||
|
if (hardwareKey != other.hardwareKey) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = password?.hashCode() ?: 0
|
||||||
|
result = 31 * result + (keyFileUri?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (hardwareKey?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toMasterCredential(contentResolver: ContentResolver): MasterCredential {
|
||||||
|
return MasterCredential(
|
||||||
|
this.password,
|
||||||
|
this.keyFileUri?.let {
|
||||||
|
getKeyFileData(contentResolver, it)
|
||||||
|
},
|
||||||
|
this.hardwareKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getKeyFileData(contentResolver: ContentResolver,
|
||||||
|
keyFileUri: Uri): ByteArray? {
|
||||||
|
contentResolver.getUriInputStream(keyFileUri)?.use { keyFileInputStream ->
|
||||||
|
return keyFileInputStream.readBytes()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object CREATOR : Parcelable.Creator<MainCredential> {
|
||||||
|
override fun createFromParcel(parcel: Parcel): MainCredential {
|
||||||
|
return MainCredential(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newArray(size: Int): Array<MainCredential?> {
|
||||||
|
return arrayOfNulls(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val TAG = MainCredential::class.java.simpleName
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.kunzisoft.keepass.database
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
|
||||||
|
data class ProgressMessage(
|
||||||
|
@StringRes
|
||||||
|
var titleId: Int,
|
||||||
|
@StringRes
|
||||||
|
var messageId: Int? = null,
|
||||||
|
@StringRes
|
||||||
|
var warningId: Int? = null,
|
||||||
|
var cancelable: (() -> Unit)? = null
|
||||||
|
)
|
||||||
@@ -1,83 +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.database.action
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
|
||||||
|
|
||||||
open class AssignMainCredentialInDatabaseRunnable (
|
|
||||||
context: Context,
|
|
||||||
database: Database,
|
|
||||||
protected val mDatabaseUri: Uri,
|
|
||||||
protected val mMainCredential: MainCredential)
|
|
||||||
: SaveDatabaseRunnable(context, database, true) {
|
|
||||||
|
|
||||||
private var mBackupKey: ByteArray? = null
|
|
||||||
|
|
||||||
override fun onStartRun() {
|
|
||||||
// Set key
|
|
||||||
try {
|
|
||||||
mBackupKey = ByteArray(database.masterKey.size)
|
|
||||||
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
|
|
||||||
|
|
||||||
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri)
|
|
||||||
database.assignMasterKey(mMainCredential.masterPassword, uriInputStream)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
erase(mBackupKey)
|
|
||||||
setError(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onStartRun()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFinishRun() {
|
|
||||||
super.onFinishRun()
|
|
||||||
|
|
||||||
// Erase the biometric
|
|
||||||
CipherDatabaseAction.getInstance(context)
|
|
||||||
.deleteByDatabaseUri(mDatabaseUri)
|
|
||||||
// Erase the register keyfile
|
|
||||||
FileDatabaseHistoryAction.getInstance(context)
|
|
||||||
.deleteKeyFileByDatabaseUri(mDatabaseUri)
|
|
||||||
|
|
||||||
if (!result.isSuccess) {
|
|
||||||
// Erase the current master key
|
|
||||||
erase(database.masterKey)
|
|
||||||
mBackupKey?.let {
|
|
||||||
database.masterKey = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Overwrite the array as soon as we don't need it to avoid keeping the extra data in memory
|
|
||||||
*/
|
|
||||||
private fun erase(array: ByteArray?) {
|
|
||||||
if (array == null) return
|
|
||||||
for (i in array.indices) {
|
|
||||||
array[i] = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,57 +21,46 @@ package com.kunzisoft.keepass.database.action
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.utils.getBinaryDir
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
|
|
||||||
class CreateDatabaseRunnable(context: Context,
|
|
||||||
private val mDatabase: Database,
|
|
||||||
databaseUri: Uri,
|
|
||||||
private val databaseName: String,
|
|
||||||
private val rootName: String,
|
|
||||||
private val templateGroupName: String?,
|
|
||||||
mainCredential: MainCredential,
|
|
||||||
private val createDatabaseResult: ((Result) -> Unit)?)
|
|
||||||
: AssignMainCredentialInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) {
|
|
||||||
|
|
||||||
|
class CreateDatabaseRunnable(
|
||||||
|
context: Context,
|
||||||
|
private val mDatabase: ContextualDatabase,
|
||||||
|
private val databaseUri: Uri,
|
||||||
|
private val databaseName: String,
|
||||||
|
private val rootName: String,
|
||||||
|
private val templateGroupName: String?,
|
||||||
|
val mainCredential: MainCredential,
|
||||||
|
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray
|
||||||
|
) : SaveDatabaseRunnable(
|
||||||
|
context,
|
||||||
|
mDatabase,
|
||||||
|
true,
|
||||||
|
mainCredential,
|
||||||
|
challengeResponseRetriever
|
||||||
|
) {
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
try {
|
try {
|
||||||
// Create new database record
|
// Create new database record
|
||||||
mDatabase.apply {
|
mDatabase.apply {
|
||||||
createData(mDatabaseUri, databaseName, rootName, templateGroupName)
|
this.fileUri = databaseUri
|
||||||
|
createData(databaseName, rootName, templateGroupName)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
mDatabase.clearAndClose(context)
|
mDatabase.clearAndClose(context.getBinaryDir())
|
||||||
setError(e)
|
setError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onStartRun()
|
super.onStartRun()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionRun() {
|
|
||||||
super.onActionRun()
|
|
||||||
|
|
||||||
if (result.isSuccess) {
|
|
||||||
// Add database to recent files
|
|
||||||
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
|
||||||
FileDatabaseHistoryAction.getInstance(context.applicationContext)
|
|
||||||
.addOrUpdateDatabaseUri(mDatabaseUri,
|
|
||||||
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register the current time to init the lock timer
|
|
||||||
PreferencesUtil.saveCurrentTime(context)
|
|
||||||
} else {
|
|
||||||
Log.e("CreateDatabaseRunnable", "Unable to create the database")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFinishRun() {
|
override fun onFinishRun() {
|
||||||
|
if (result.isSuccess) {
|
||||||
|
mDatabase.loaded = true
|
||||||
|
}
|
||||||
super.onFinishRun()
|
super.onFinishRun()
|
||||||
|
|
||||||
createDatabaseResult?.invoke(result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,73 +21,65 @@ package com.kunzisoft.keepass.database.action
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
|
||||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
import com.kunzisoft.keepass.database.exception.DatabaseInputException
|
||||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.getBinaryDir
|
||||||
|
import com.kunzisoft.keepass.utils.getUriInputStream
|
||||||
|
|
||||||
class LoadDatabaseRunnable(private val context: Context,
|
class LoadDatabaseRunnable(
|
||||||
private val mDatabase: Database,
|
private val context: Context,
|
||||||
private val mUri: Uri,
|
private val mDatabase: ContextualDatabase,
|
||||||
private val mMainCredential: MainCredential,
|
private val mDatabaseUri: Uri,
|
||||||
private val mReadonly: Boolean,
|
private val mMainCredential: MainCredential,
|
||||||
private val mCipherEncryptDatabase: CipherEncryptDatabase?,
|
private val mChallengeResponseRetriever: (hardwareKey: HardwareKey, seed: ByteArray?) -> ByteArray,
|
||||||
private val mFixDuplicateUUID: Boolean,
|
private val mReadonly: Boolean,
|
||||||
private val progressTaskUpdater: ProgressTaskUpdater?,
|
private val mFixDuplicateUUID: Boolean,
|
||||||
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
private val progressTaskUpdater: ProgressTaskUpdater?
|
||||||
: ActionRunnable() {
|
) : ActionRunnable() {
|
||||||
|
|
||||||
|
var afterLoadDatabase : ((Result) -> Unit)? = null
|
||||||
|
|
||||||
|
private val binaryDir = context.getBinaryDir()
|
||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
// Clear before we load
|
// Clear before we load
|
||||||
mDatabase.clearAndClose(context)
|
mDatabase.clearAndClose(binaryDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionRun() {
|
override fun onActionRun() {
|
||||||
try {
|
try {
|
||||||
mDatabase.loadData(mUri,
|
val contentResolver = context.contentResolver
|
||||||
mMainCredential,
|
// Save database URI
|
||||||
mReadonly,
|
mDatabase.fileUri = mDatabaseUri
|
||||||
context.contentResolver,
|
mDatabase.loadData(
|
||||||
UriUtil.getBinaryDir(context),
|
contentResolver.getUriInputStream(mDatabaseUri)
|
||||||
{ memoryWanted ->
|
?: throw UnknownDatabaseLocationException(),
|
||||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
mMainCredential.toMasterCredential(contentResolver),
|
||||||
},
|
mChallengeResponseRetriever,
|
||||||
mFixDuplicateUUID,
|
mReadonly,
|
||||||
progressTaskUpdater)
|
binaryDir,
|
||||||
}
|
{ memoryWanted ->
|
||||||
catch (e: LoadDatabaseException) {
|
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||||
|
},
|
||||||
|
mFixDuplicateUUID,
|
||||||
|
progressTaskUpdater
|
||||||
|
)
|
||||||
|
} catch (e: DatabaseInputException) {
|
||||||
setError(e)
|
setError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.isSuccess) {
|
if (!result.isSuccess) {
|
||||||
// Save keyFile in app database
|
mDatabase.clearAndClose(binaryDir)
|
||||||
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
|
||||||
FileDatabaseHistoryAction.getInstance(context)
|
|
||||||
.addOrUpdateDatabaseUri(mUri,
|
|
||||||
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register the biometric
|
|
||||||
mCipherEncryptDatabase?.let { cipherDatabase ->
|
|
||||||
CipherDatabaseAction.getInstance(context)
|
|
||||||
.addOrUpdateCipherDatabase(cipherDatabase) // return value not called
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register the current time to init the lock timer
|
|
||||||
PreferencesUtil.saveCurrentTime(context)
|
|
||||||
} else {
|
|
||||||
mDatabase.clearAndClose(context)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFinishRun() {
|
override fun onFinishRun() {
|
||||||
mLoadDatabaseResult?.invoke(result)
|
afterLoadDatabase?.invoke(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,47 +21,54 @@ package com.kunzisoft.keepass.database.action
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
import com.kunzisoft.keepass.database.exception.DatabaseException
|
||||||
import com.kunzisoft.keepass.model.MainCredential
|
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
|
import com.kunzisoft.keepass.utils.getUriInputStream
|
||||||
|
|
||||||
class MergeDatabaseRunnable(private val context: Context,
|
class MergeDatabaseRunnable(
|
||||||
private val mDatabase: Database,
|
context: Context,
|
||||||
private val mDatabaseToMergeUri: Uri?,
|
private val mDatabaseToMergeUri: Uri?,
|
||||||
private val mDatabaseToMergeMainCredential: MainCredential?,
|
private val mDatabaseToMergeMainCredential: MainCredential?,
|
||||||
private val progressTaskUpdater: ProgressTaskUpdater?,
|
private val mDatabaseToMergeChallengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
|
||||||
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
database: ContextualDatabase,
|
||||||
: ActionRunnable() {
|
saveDatabase: Boolean,
|
||||||
|
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
|
||||||
|
private val progressTaskUpdater: ProgressTaskUpdater?,
|
||||||
|
) : SaveDatabaseRunnable(
|
||||||
|
context,
|
||||||
|
database,
|
||||||
|
saveDatabase,
|
||||||
|
null,
|
||||||
|
challengeResponseRetriever
|
||||||
|
) {
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
mDatabase.wasReloaded = true
|
database.wasReloaded = true
|
||||||
|
super.onStartRun()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionRun() {
|
override fun onActionRun() {
|
||||||
try {
|
try {
|
||||||
mDatabase.mergeData(mDatabaseToMergeUri,
|
val contentResolver = context.contentResolver
|
||||||
mDatabaseToMergeMainCredential,
|
database.mergeData(
|
||||||
context.contentResolver,
|
context.contentResolver.getUriInputStream(
|
||||||
|
mDatabaseToMergeUri ?: database.fileUri
|
||||||
|
) ?: throw UnknownDatabaseLocationException(),
|
||||||
|
mDatabaseToMergeMainCredential?.toMasterCredential(contentResolver),
|
||||||
|
mDatabaseToMergeChallengeResponseRetriever,
|
||||||
{ memoryWanted ->
|
{ memoryWanted ->
|
||||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||||
},
|
},
|
||||||
progressTaskUpdater
|
progressTaskUpdater
|
||||||
)
|
)
|
||||||
} catch (e: LoadDatabaseException) {
|
} catch (e: DatabaseException) {
|
||||||
setError(e)
|
setError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.isSuccess) {
|
super.onActionRun()
|
||||||
// Register the current time to init the lock timer
|
|
||||||
PreferencesUtil.saveCurrentTime(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFinishRun() {
|
|
||||||
mLoadDatabaseResult?.invoke(result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,46 +20,50 @@
|
|||||||
package com.kunzisoft.keepass.database.action
|
package com.kunzisoft.keepass.database.action
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
import com.kunzisoft.keepass.database.exception.LoadDatabaseException
|
import com.kunzisoft.keepass.database.exception.DatabaseException
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.getBinaryDir
|
||||||
|
import com.kunzisoft.keepass.utils.getUriInputStream
|
||||||
|
|
||||||
class ReloadDatabaseRunnable(private val context: Context,
|
class ReloadDatabaseRunnable(
|
||||||
private val mDatabase: Database,
|
private val context: Context,
|
||||||
private val progressTaskUpdater: ProgressTaskUpdater?,
|
private val mDatabase: ContextualDatabase,
|
||||||
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
private val progressTaskUpdater: ProgressTaskUpdater?
|
||||||
: ActionRunnable() {
|
) : ActionRunnable() {
|
||||||
|
|
||||||
|
var afterReloadDatabase : ((Result) -> Unit)? = null
|
||||||
|
|
||||||
|
private val binaryDir = context.getBinaryDir()
|
||||||
|
|
||||||
override fun onStartRun() {
|
override fun onStartRun() {
|
||||||
// Clear before we load
|
// Clear before we load
|
||||||
mDatabase.clearIndexesAndBinaries(UriUtil.getBinaryDir(context))
|
mDatabase.clearIndexesAndBinaries(binaryDir)
|
||||||
mDatabase.wasReloaded = true
|
mDatabase.wasReloaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActionRun() {
|
override fun onActionRun() {
|
||||||
try {
|
try {
|
||||||
mDatabase.reloadData(context.contentResolver,
|
mDatabase.reloadData(
|
||||||
{ memoryWanted ->
|
context.contentResolver.getUriInputStream(mDatabase.fileUri)
|
||||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
?: throw UnknownDatabaseLocationException(),
|
||||||
},
|
{ memoryWanted ->
|
||||||
progressTaskUpdater)
|
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||||
} catch (e: LoadDatabaseException) {
|
},
|
||||||
|
progressTaskUpdater)
|
||||||
|
} catch (e: DatabaseException) {
|
||||||
setError(e)
|
setError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.isSuccess) {
|
if (!result.isSuccess) {
|
||||||
// Register the current time to init the lock timer
|
mDatabase.clearAndClose(binaryDir)
|
||||||
PreferencesUtil.saveCurrentTime(context)
|
|
||||||
} else {
|
|
||||||
mDatabase.clearAndClose(context)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFinishRun() {
|
override fun onFinishRun() {
|
||||||
mLoadDatabaseResult?.invoke(result)
|
afterReloadDatabase?.invoke(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,13 +20,21 @@
|
|||||||
package com.kunzisoft.keepass.database.action
|
package com.kunzisoft.keepass.database.action
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
|
|
||||||
class RemoveUnlinkedDataDatabaseRunnable (
|
class RemoveUnlinkedDataDatabaseRunnable (
|
||||||
context: Context,
|
context: Context,
|
||||||
database: Database,
|
database: ContextualDatabase,
|
||||||
saveDatabase: Boolean)
|
saveDatabase: Boolean,
|
||||||
: SaveDatabaseRunnable(context, database, saveDatabase) {
|
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray
|
||||||
|
) : SaveDatabaseRunnable(
|
||||||
|
context,
|
||||||
|
database,
|
||||||
|
saveDatabase,
|
||||||
|
null,
|
||||||
|
challengeResponseRetriever
|
||||||
|
) {
|
||||||
|
|
||||||
override fun onActionRun() {
|
override fun onActionRun() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user