mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
625 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 | ||
|
|
0474e5b1fe |
61
CHANGELOG
61
CHANGELOG
@@ -1,3 +1,64 @@
|
|||||||
|
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)
|
KeePassDX(4.0.3)
|
||||||
* Fix "Save as" in Read Only mode #1666
|
* Fix "Save as" in Read Only mode #1666
|
||||||
* Fix username autofill #1665 #530 #1572 #1426 #1523 #1556 #1653 #1658 #1508 #1667
|
* Fix username autofill #1665 #530 #1572 #1426 #1523 #1556 #1653 #1658 #1508 #1667
|
||||||
|
|||||||
172
Gemfile.lock
172
Gemfile.lock
@@ -1,43 +1,49 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
CFPropertyList (3.0.6)
|
CFPropertyList (3.0.7)
|
||||||
|
base64
|
||||||
|
nkf
|
||||||
rexml
|
rexml
|
||||||
addressable (2.8.4)
|
addressable (2.8.7)
|
||||||
public_suffix (>= 2.0.2, < 6.0)
|
public_suffix (>= 2.0.2, < 7.0)
|
||||||
artifactory (3.0.15)
|
artifactory (3.0.17)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
aws-eventstream (1.2.0)
|
aws-eventstream (1.4.0)
|
||||||
aws-partitions (1.794.0)
|
aws-partitions (1.1146.0)
|
||||||
aws-sdk-core (3.180.0)
|
aws-sdk-core (3.229.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.9)
|
||||||
|
base64
|
||||||
|
bigdecimal
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.71.0)
|
logger
|
||||||
aws-sdk-core (~> 3, >= 3.177.0)
|
aws-sdk-kms (1.110.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sdk-core (~> 3, >= 3.228.0)
|
||||||
aws-sdk-s3 (1.132.0)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-core (~> 3, >= 3.179.0)
|
aws-sdk-s3 (1.196.1)
|
||||||
|
aws-sdk-core (~> 3, >= 3.228.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.6)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sigv4 (1.6.0)
|
aws-sigv4 (1.12.1)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
babosa (1.0.4)
|
babosa (1.0.4)
|
||||||
|
base64 (0.3.0)
|
||||||
|
bigdecimal (3.2.2)
|
||||||
claide (1.1.0)
|
claide (1.1.0)
|
||||||
colored (1.2)
|
colored (1.2)
|
||||||
colored2 (3.1.2)
|
colored2 (3.1.2)
|
||||||
commander (4.6.0)
|
commander (4.6.0)
|
||||||
highline (~> 2.0.0)
|
highline (~> 2.0.0)
|
||||||
declarative (0.0.20)
|
declarative (0.0.20)
|
||||||
digest-crc (0.6.5)
|
digest-crc (0.7.0)
|
||||||
rake (>= 12.0.0, < 14.0.0)
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
domain_name (0.5.20190701)
|
domain_name (0.6.20240107)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
|
||||||
dotenv (2.8.1)
|
dotenv (2.8.1)
|
||||||
emoji_regex (3.2.3)
|
emoji_regex (3.2.3)
|
||||||
excon (0.100.0)
|
excon (0.112.0)
|
||||||
faraday (1.10.3)
|
faraday (1.10.4)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-em_http (~> 1.0)
|
||||||
faraday-em_synchrony (~> 1.0)
|
faraday-em_synchrony (~> 1.0)
|
||||||
faraday-excon (~> 1.1)
|
faraday-excon (~> 1.1)
|
||||||
@@ -53,27 +59,27 @@ GEM
|
|||||||
faraday (>= 0.8.0)
|
faraday (>= 0.8.0)
|
||||||
http-cookie (~> 1.0.0)
|
http-cookie (~> 1.0.0)
|
||||||
faraday-em_http (1.0.0)
|
faraday-em_http (1.0.0)
|
||||||
faraday-em_synchrony (1.0.0)
|
faraday-em_synchrony (1.0.1)
|
||||||
faraday-excon (1.1.0)
|
faraday-excon (1.1.0)
|
||||||
faraday-httpclient (1.0.1)
|
faraday-httpclient (1.0.1)
|
||||||
faraday-multipart (1.0.4)
|
faraday-multipart (1.1.1)
|
||||||
multipart-post (~> 2)
|
multipart-post (~> 2.0)
|
||||||
faraday-net_http (1.0.1)
|
faraday-net_http (1.0.2)
|
||||||
faraday-net_http_persistent (1.2.0)
|
faraday-net_http_persistent (1.2.0)
|
||||||
faraday-patron (1.0.0)
|
faraday-patron (1.0.0)
|
||||||
faraday-rack (1.0.0)
|
faraday-rack (1.0.0)
|
||||||
faraday-retry (1.0.3)
|
faraday-retry (1.0.3)
|
||||||
faraday_middleware (1.2.0)
|
faraday_middleware (1.2.1)
|
||||||
faraday (~> 1.0)
|
faraday (~> 1.0)
|
||||||
fastimage (2.2.7)
|
fastimage (2.4.0)
|
||||||
fastlane (2.214.0)
|
fastlane (2.228.0)
|
||||||
CFPropertyList (>= 2.3, < 4.0.0)
|
CFPropertyList (>= 2.3, < 4.0.0)
|
||||||
addressable (>= 2.8, < 3.0.0)
|
addressable (>= 2.8, < 3.0.0)
|
||||||
artifactory (~> 3.0)
|
artifactory (~> 3.0)
|
||||||
aws-sdk-s3 (~> 1.0)
|
aws-sdk-s3 (~> 1.0)
|
||||||
babosa (>= 1.0.3, < 2.0.0)
|
babosa (>= 1.0.3, < 2.0.0)
|
||||||
bundler (>= 1.12.0, < 3.0.0)
|
bundler (>= 1.12.0, < 3.0.0)
|
||||||
colored
|
colored (~> 1.2)
|
||||||
commander (~> 4.6)
|
commander (~> 4.6)
|
||||||
dotenv (>= 2.1.1, < 3.0.0)
|
dotenv (>= 2.1.1, < 3.0.0)
|
||||||
emoji_regex (>= 0.1, < 4.0)
|
emoji_regex (>= 0.1, < 4.0)
|
||||||
@@ -82,34 +88,39 @@ GEM
|
|||||||
faraday-cookie_jar (~> 0.0.6)
|
faraday-cookie_jar (~> 0.0.6)
|
||||||
faraday_middleware (~> 1.0)
|
faraday_middleware (~> 1.0)
|
||||||
fastimage (>= 2.1.0, < 3.0.0)
|
fastimage (>= 2.1.0, < 3.0.0)
|
||||||
|
fastlane-sirp (>= 1.0.0)
|
||||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||||
google-apis-androidpublisher_v3 (~> 0.3)
|
google-apis-androidpublisher_v3 (~> 0.3)
|
||||||
google-apis-playcustomapp_v1 (~> 0.1)
|
google-apis-playcustomapp_v1 (~> 0.1)
|
||||||
|
google-cloud-env (>= 1.6.0, < 2.0.0)
|
||||||
google-cloud-storage (~> 1.31)
|
google-cloud-storage (~> 1.31)
|
||||||
highline (~> 2.0)
|
highline (~> 2.0)
|
||||||
|
http-cookie (~> 1.0.5)
|
||||||
json (< 3.0.0)
|
json (< 3.0.0)
|
||||||
jwt (>= 2.1.0, < 3)
|
jwt (>= 2.1.0, < 3)
|
||||||
mini_magick (>= 4.9.4, < 5.0.0)
|
mini_magick (>= 4.9.4, < 5.0.0)
|
||||||
multipart-post (>= 2.0.0, < 3.0.0)
|
multipart-post (>= 2.0.0, < 3.0.0)
|
||||||
naturally (~> 2.2)
|
naturally (~> 2.2)
|
||||||
optparse (~> 0.1.1)
|
optparse (>= 0.1.1, < 1.0.0)
|
||||||
plist (>= 3.1.0, < 4.0.0)
|
plist (>= 3.1.0, < 4.0.0)
|
||||||
rubyzip (>= 2.0.0, < 3.0.0)
|
rubyzip (>= 2.0.0, < 3.0.0)
|
||||||
security (= 0.1.3)
|
security (= 0.1.5)
|
||||||
simctl (~> 1.6.3)
|
simctl (~> 1.6.3)
|
||||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||||
terminal-table (>= 1.4.5, < 2.0.0)
|
terminal-table (~> 3)
|
||||||
tty-screen (>= 0.6.3, < 1.0.0)
|
tty-screen (>= 0.6.3, < 1.0.0)
|
||||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||||
word_wrap (~> 1.0.0)
|
word_wrap (~> 1.0.0)
|
||||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||||
xcpretty (~> 0.3.0)
|
xcpretty (~> 0.4.1)
|
||||||
xcpretty-travis-formatter (>= 0.0.3)
|
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||||
fastlane-plugin-versioning_android (0.1.1)
|
fastlane-plugin-versioning_android (0.1.1)
|
||||||
|
fastlane-sirp (1.0.0)
|
||||||
|
sysrandom (~> 1.0)
|
||||||
gh_inspector (1.1.3)
|
gh_inspector (1.1.3)
|
||||||
google-apis-androidpublisher_v3 (0.46.0)
|
google-apis-androidpublisher_v3 (0.54.0)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
google-apis-core (0.11.1)
|
google-apis-core (0.11.3)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
httpclient (>= 2.8.1, < 3.a)
|
httpclient (>= 2.8.1, < 3.a)
|
||||||
@@ -117,64 +128,66 @@ GEM
|
|||||||
representable (~> 3.0)
|
representable (~> 3.0)
|
||||||
retriable (>= 2.0, < 4.a)
|
retriable (>= 2.0, < 4.a)
|
||||||
rexml
|
rexml
|
||||||
webrick
|
|
||||||
google-apis-iamcredentials_v1 (0.17.0)
|
google-apis-iamcredentials_v1 (0.17.0)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
google-apis-playcustomapp_v1 (0.13.0)
|
google-apis-playcustomapp_v1 (0.13.0)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
google-apis-storage_v1 (0.19.0)
|
google-apis-storage_v1 (0.31.0)
|
||||||
google-apis-core (>= 0.9.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
google-cloud-core (1.6.0)
|
google-cloud-core (1.8.0)
|
||||||
google-cloud-env (~> 1.0)
|
google-cloud-env (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-cloud-env (1.6.0)
|
google-cloud-env (1.6.0)
|
||||||
faraday (>= 0.17.3, < 3.0)
|
faraday (>= 0.17.3, < 3.0)
|
||||||
google-cloud-errors (1.3.1)
|
google-cloud-errors (1.5.0)
|
||||||
google-cloud-storage (1.44.0)
|
google-cloud-storage (1.47.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
digest-crc (~> 0.4)
|
digest-crc (~> 0.4)
|
||||||
google-apis-iamcredentials_v1 (~> 0.1)
|
google-apis-iamcredentials_v1 (~> 0.1)
|
||||||
google-apis-storage_v1 (~> 0.19.0)
|
google-apis-storage_v1 (~> 0.31.0)
|
||||||
google-cloud-core (~> 1.6)
|
google-cloud-core (~> 1.6)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
googleauth (1.7.0)
|
googleauth (1.8.1)
|
||||||
faraday (>= 0.17.3, < 3.a)
|
faraday (>= 0.17.3, < 3.a)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 3.0)
|
||||||
memoist (~> 0.16)
|
|
||||||
multi_json (~> 1.11)
|
multi_json (~> 1.11)
|
||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
highline (2.0.3)
|
highline (2.0.3)
|
||||||
http-cookie (1.0.5)
|
http-cookie (1.0.8)
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
httpclient (2.8.3)
|
httpclient (2.9.0)
|
||||||
|
mutex_m
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.6.3)
|
json (2.13.2)
|
||||||
jwt (2.7.1)
|
jwt (2.10.2)
|
||||||
memoist (0.16.2)
|
base64
|
||||||
mini_magick (4.12.0)
|
logger (1.7.0)
|
||||||
mini_mime (1.1.2)
|
mini_magick (4.13.2)
|
||||||
multi_json (1.15.0)
|
mini_mime (1.1.5)
|
||||||
multipart-post (2.3.0)
|
multi_json (1.17.0)
|
||||||
nanaimo (0.3.0)
|
multipart-post (2.4.1)
|
||||||
naturally (2.2.1)
|
mutex_m (0.3.0)
|
||||||
optparse (0.1.1)
|
nanaimo (0.4.0)
|
||||||
|
naturally (2.3.0)
|
||||||
|
nkf (0.2.0)
|
||||||
|
optparse (0.6.0)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
plist (3.7.0)
|
plist (3.7.2)
|
||||||
public_suffix (5.0.3)
|
public_suffix (6.0.2)
|
||||||
rake (13.0.6)
|
rake (13.3.0)
|
||||||
representable (3.2.0)
|
representable (3.2.0)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.2.6)
|
rexml (3.4.1)
|
||||||
rouge (2.0.7)
|
rouge (3.28.0)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rubyzip (2.3.2)
|
rubyzip (2.4.1)
|
||||||
security (0.1.3)
|
security (0.1.5)
|
||||||
signet (0.17.0)
|
signet (0.20.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
faraday (>= 0.17.5, < 3.a)
|
faraday (>= 0.17.5, < 3.a)
|
||||||
jwt (>= 1.5, < 3.0)
|
jwt (>= 1.5, < 3.0)
|
||||||
@@ -182,30 +195,27 @@ GEM
|
|||||||
simctl (1.6.10)
|
simctl (1.6.10)
|
||||||
CFPropertyList
|
CFPropertyList
|
||||||
naturally
|
naturally
|
||||||
|
sysrandom (1.0.5)
|
||||||
terminal-notifier (2.0.0)
|
terminal-notifier (2.0.0)
|
||||||
terminal-table (1.8.0)
|
terminal-table (3.0.2)
|
||||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
trailblazer-option (0.1.2)
|
trailblazer-option (0.1.2)
|
||||||
tty-cursor (0.7.1)
|
tty-cursor (0.7.1)
|
||||||
tty-screen (0.8.1)
|
tty-screen (0.8.2)
|
||||||
tty-spinner (0.9.3)
|
tty-spinner (0.9.3)
|
||||||
tty-cursor (~> 0.7)
|
tty-cursor (~> 0.7)
|
||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
unf (0.1.4)
|
unicode-display_width (2.6.0)
|
||||||
unf_ext
|
|
||||||
unf_ext (0.0.8.2)
|
|
||||||
unicode-display_width (1.8.0)
|
|
||||||
webrick (1.8.1)
|
|
||||||
word_wrap (1.0.0)
|
word_wrap (1.0.0)
|
||||||
xcodeproj (1.22.0)
|
xcodeproj (1.27.0)
|
||||||
CFPropertyList (>= 2.3.3, < 4.0)
|
CFPropertyList (>= 2.3.3, < 4.0)
|
||||||
atomos (~> 0.1.3)
|
atomos (~> 0.1.3)
|
||||||
claide (>= 1.0.2, < 2.0)
|
claide (>= 1.0.2, < 2.0)
|
||||||
colored2 (~> 3.1)
|
colored2 (~> 3.1)
|
||||||
nanaimo (~> 0.3.0)
|
nanaimo (~> 0.4.0)
|
||||||
rexml (~> 3.2.4)
|
rexml (>= 3.3.6, < 4.0)
|
||||||
xcpretty (0.3.0)
|
xcpretty (0.4.1)
|
||||||
rouge (~> 2.0.7)
|
rouge (~> 3.28.0)
|
||||||
xcpretty-travis-formatter (1.0.1)
|
xcpretty-travis-formatter (1.0.1)
|
||||||
xcpretty (~> 0.2, >= 0.0.7)
|
xcpretty (~> 0.2, >= 0.0.7)
|
||||||
|
|
||||||
@@ -217,4 +227,4 @@ DEPENDENCIES
|
|||||||
fastlane-plugin-versioning_android
|
fastlane-plugin-versioning_android
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.1.4
|
2.6.9
|
||||||
|
|||||||
50
README.md
50
README.md
@@ -1,8 +1,8 @@
|
|||||||
# 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
|
||||||
|
|
||||||
@@ -48,18 +48,40 @@ Optional visual styles are accessible after a contribution (and a congratulatory
|
|||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
*[F-Droid](https://f-droid.org/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.*
|
||||||
|
|
||||||
|
| Source | Status | [Version](https://github.com/Kunzisoft/KeePassDX/wiki/FAQ#why-a-libre-and-free-version) |
|
||||||
|
|--------|--------|---------|
|
||||||
|
| [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://f-droid.org/badge/get-it-on.png"
|
|
||||||
alt="Get it on F-Droid"
|
|
||||||
height="80">](https://f-droid.org/packages/com.kunzisoft.keepass.libre/)
|
|
||||||
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
|
|
||||||
alt="Get it on Google Play"
|
|
||||||
height="80">](https://play.google.com/store/apps/details?id=com.kunzisoft.keepass.free)
|
|
||||||
[<img src="https://raw.githubusercontent.com/Kunzisoft/Github-badge/main/get-it-on-github.png"
|
|
||||||
alt="Get it on Github"
|
|
||||||
height="80">](https://github.com/Kunzisoft/KeePassDX/releases)
|
|
||||||
|
|
||||||
## 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)
|
||||||
@@ -74,7 +96,7 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright © 2023 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.
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,14 @@ apply plugin: 'kotlin-kapt'
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.kunzisoft.keepass'
|
namespace 'com.kunzisoft.keepass'
|
||||||
compileSdkVersion 33
|
compileSdkVersion 34
|
||||||
buildToolsVersion "33.0.2"
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 15
|
minSdkVersion 15
|
||||||
targetSdkVersion 33
|
targetSdkVersion 34
|
||||||
versionCode = 126
|
versionCode = 136
|
||||||
versionName = "4.0.3"
|
versionName = "4.1.4"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
@@ -36,6 +35,13 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
@@ -85,12 +91,15 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +129,7 @@ dependencies {
|
|||||||
// 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
|
||||||
@@ -137,5 +146,4 @@ dependencies {
|
|||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
androidTestImplementation "androidx.test:runner:$android_test_version"
|
androidTestImplementation "androidx.test:runner:$android_test_version"
|
||||||
androidTestImplementation "androidx.test:rules:$android_test_version"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
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
|
<uses-permission
|
||||||
android:name="android.permission.POST_NOTIFICATIONS" />
|
android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
@@ -39,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
|
||||||
@@ -118,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"
|
||||||
@@ -145,7 +150,7 @@
|
|||||||
android:configChanges="keyboardHidden" />
|
android:configChanges="keyboardHidden" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
|
android:name="com.kunzisoft.keepass.activities.EntryEditActivity"
|
||||||
android:windowSoftInputMode="adjustPan" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
<!-- About and Settings -->
|
<!-- About and Settings -->
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
android:name="com.kunzisoft.keepass.activities.AboutActivity"
|
||||||
@@ -197,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 -->
|
||||||
@@ -235,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
|
||||||
@@ -202,7 +206,7 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
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
|
||||||
@@ -220,7 +224,7 @@ 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
|
||||||
|
|||||||
@@ -20,10 +20,12 @@
|
|||||||
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
|
||||||
@@ -76,6 +78,9 @@ 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 {
|
findViewById<TextView>(R.id.activity_about_privacy_text).apply {
|
||||||
|
|||||||
@@ -41,11 +41,9 @@ import com.kunzisoft.keepass.database.ContextualDatabase
|
|||||||
import com.kunzisoft.keepass.database.helper.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.getParcelableCompat
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
import com.kunzisoft.keepass.utils.WebDomain
|
|
||||||
import java.lang.RuntimeException
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class AutofillLauncherActivity : DatabaseModeActivity() {
|
class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||||
@@ -126,83 +124,85 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
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: ContextualDatabase?,
|
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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ 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.AdapterView
|
import android.widget.AdapterView
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.Spinner
|
import android.widget.Spinner
|
||||||
@@ -73,6 +72,7 @@ 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
|
||||||
@@ -87,6 +87,7 @@ 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.TimeUtil.datePickerToDataDate
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
import com.kunzisoft.keepass.view.ToolbarAction
|
import com.kunzisoft.keepass.view.ToolbarAction
|
||||||
@@ -96,7 +97,7 @@ import com.kunzisoft.keepass.view.asError
|
|||||||
import com.kunzisoft.keepass.view.hideByFading
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
||||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
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 java.util.UUID
|
import java.util.UUID
|
||||||
@@ -108,8 +109,8 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
ReplaceFileDialogFragment.ActionChooseListener {
|
ReplaceFileDialogFragment.ActionChooseListener {
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
private var footer: ViewGroup? = null
|
private var footer: View? = null
|
||||||
private var container: ViewGroup? = 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
|
||||||
@@ -179,8 +180,8 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
// To apply fit window with transparency
|
// To apply fit window with transparency
|
||||||
setTransparentNavigationBar(applyToStatusBar = true) {
|
setTransparentNavigationBar(applyToStatusBar = true) {
|
||||||
container?.applyWindowInsets(WindowInsetPosition.TOP)
|
container?.applyWindowInsets(WindowInsetPosition.TOP_BOTTOM_IME)
|
||||||
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
|
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM_IME)
|
||||||
}
|
}
|
||||||
|
|
||||||
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
||||||
@@ -301,7 +302,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
// Launch the time picker
|
// Launch the time picker
|
||||||
MaterialTimePicker.Builder().build().apply {
|
MaterialTimePicker.Builder().build().apply {
|
||||||
addOnPositiveButtonClickListener {
|
addOnPositiveButtonClickListener {
|
||||||
mEntryEditViewModel.selectTime(this.hour, this.minute)
|
mEntryEditViewModel.selectTime(DataTime(this.hour, this.minute))
|
||||||
}
|
}
|
||||||
show(supportFragmentManager, "TimePickerFragment")
|
show(supportFragmentManager, "TimePickerFragment")
|
||||||
}
|
}
|
||||||
@@ -309,7 +310,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
// Launch the date picker
|
// Launch the date picker
|
||||||
MaterialDatePicker.Builder.datePicker().build().apply {
|
MaterialDatePicker.Builder.datePicker().build().apply {
|
||||||
addOnPositiveButtonClickListener {
|
addOnPositiveButtonClickListener {
|
||||||
mEntryEditViewModel.selectDate(it)
|
mEntryEditViewModel.selectDate(datePickerToDataDate(it))
|
||||||
}
|
}
|
||||||
show(supportFragmentManager, "DatePickerFragment")
|
show(supportFragmentManager, "DatePickerFragment")
|
||||||
}
|
}
|
||||||
@@ -502,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()
|
||||||
|
|||||||
@@ -68,11 +68,10 @@ import com.kunzisoft.keepass.tasks.ActionRunnable
|
|||||||
import com.kunzisoft.keepass.utils.DexUtil
|
import com.kunzisoft.keepass.utils.DexUtil
|
||||||
import com.kunzisoft.keepass.utils.MagikeyboardUtil
|
import com.kunzisoft.keepass.utils.MagikeyboardUtil
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
|
||||||
import com.kunzisoft.keepass.utils.parseUri
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
import com.kunzisoft.keepass.utils.allowCreateDocumentByStorageAccessFramework
|
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.view.showActionErrorIfNeeded
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
|
||||||
@@ -180,17 +179,9 @@ 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)
|
|
||||||
|
|
||||||
databasePath?.parseUri()?.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
|
||||||
@@ -325,6 +316,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
||||||
launchPasswordActivity(databaseUri, null, 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)
|
||||||
}
|
}
|
||||||
@@ -366,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)
|
||||||
}
|
}
|
||||||
@@ -442,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"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ 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.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
@@ -96,6 +97,7 @@ 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.KeyboardUtil.showKeyboard
|
import com.kunzisoft.keepass.utils.KeyboardUtil.showKeyboard
|
||||||
|
import com.kunzisoft.keepass.utils.TimeUtil.datePickerToDataDate
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
@@ -111,9 +113,10 @@ import com.kunzisoft.keepass.view.applyWindowInsets
|
|||||||
import com.kunzisoft.keepass.view.hideByFading
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
||||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
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.LocalDateTime
|
||||||
|
|
||||||
|
|
||||||
class GroupActivity : DatabaseLockActivity(),
|
class GroupActivity : DatabaseLockActivity(),
|
||||||
@@ -292,10 +295,9 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
loadingView = findViewById(R.id.loading)
|
loadingView = findViewById(R.id.loading)
|
||||||
|
|
||||||
// To apply fit window with transparency
|
// To apply fit window with transparency
|
||||||
setTransparentNavigationBar {
|
setTransparentNavigationBar(applyToStatusBar = true) {
|
||||||
header?.applyWindowInsets(WindowInsetPosition.TOP)
|
drawerLayout?.applyWindowInsets(WindowInsetPosition.TOP_BOTTOM_IME)
|
||||||
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.LEGIT_TOP)
|
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM_IME)
|
||||||
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lockView?.setOnClickListener {
|
lockView?.setOnClickListener {
|
||||||
@@ -340,8 +342,9 @@ 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()
|
||||||
@@ -359,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)
|
||||||
|
|
||||||
@@ -476,7 +442,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
// Launch the time picker
|
// Launch the time picker
|
||||||
MaterialTimePicker.Builder().build().apply {
|
MaterialTimePicker.Builder().build().apply {
|
||||||
addOnPositiveButtonClickListener {
|
addOnPositiveButtonClickListener {
|
||||||
mGroupEditViewModel.selectTime(this.hour, this.minute)
|
mGroupEditViewModel.selectTime(DataTime(this.hour, this.minute))
|
||||||
}
|
}
|
||||||
show(supportFragmentManager, "TimePickerFragment")
|
show(supportFragmentManager, "TimePickerFragment")
|
||||||
}
|
}
|
||||||
@@ -484,7 +450,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
// Launch the date picker
|
// Launch the date picker
|
||||||
MaterialDatePicker.Builder.datePicker().build().apply {
|
MaterialDatePicker.Builder.datePicker().build().apply {
|
||||||
addOnPositiveButtonClickListener {
|
addOnPositiveButtonClickListener {
|
||||||
mGroupEditViewModel.selectDate(it)
|
mGroupEditViewModel.selectDate(datePickerToDataDate(it))
|
||||||
}
|
}
|
||||||
show(supportFragmentManager, "DatePickerFragment")
|
show(supportFragmentManager, "DatePickerFragment")
|
||||||
}
|
}
|
||||||
@@ -617,6 +583,43 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
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
|
||||||
@@ -1127,7 +1130,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
View.GONE
|
View.GONE
|
||||||
}
|
}
|
||||||
// Padding if lock button visible
|
// Padding if lock button visible
|
||||||
toolbarAction?.updateLockPaddingLeft()
|
toolbarAction?.updateLockPaddingStart()
|
||||||
|
|
||||||
loadGroup()
|
loadGroup()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
|||||||
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
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.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
@@ -212,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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,25 +42,33 @@ 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.biometric.deviceUnlockError
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
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.hardware.HardwareKey
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
import com.kunzisoft.keepass.model.*
|
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
|
||||||
@@ -79,12 +86,14 @@ 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.view.showActionErrorIfNeeded
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
|
|
||||||
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
|
||||||
@@ -95,10 +104,10 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
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)
|
||||||
|
|
||||||
@@ -166,21 +175,14 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
}
|
)
|
||||||
mainCredentialView?.onKeyFileChecked =
|
// TODO Async by ViewModel
|
||||||
CompoundButton.OnCheckedChangeListener { _, _ ->
|
enableConfirmationButton()
|
||||||
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
|
}
|
||||||
enableConfirmationButton()
|
|
||||||
}
|
|
||||||
mainCredentialView?.onHardwareKeyChecked =
|
|
||||||
CompoundButton.OnCheckedChangeListener { _, _ ->
|
|
||||||
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
|
|
||||||
enableConfirmationButton()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Observe if default database
|
// Observe if default database
|
||||||
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
||||||
@@ -228,17 +230,50 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
|
|
||||||
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri, hardwareKey)
|
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
|
// Init Biometric elements only if allowed
|
||||||
if (PreferencesUtil.isBiometricUnlockEnable(this)) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||||
advancedUnlockFragment = supportFragmentManager
|
&& PreferencesUtil.isAdvancedUnlockEnable(this)) {
|
||||||
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? AdvancedUnlockFragment?
|
deviceUnlockFragment = supportFragmentManager
|
||||||
if (advancedUnlockFragment == null) {
|
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? DeviceUnlockFragment?
|
||||||
advancedUnlockFragment = AdvancedUnlockFragment().also {
|
if (deviceUnlockFragment == null) {
|
||||||
|
deviceUnlockFragment = DeviceUnlockFragment().also {
|
||||||
supportFragmentManager.commit {
|
supportFragmentManager.commit {
|
||||||
replace(
|
replace(
|
||||||
R.id.fragment_advanced_unlock_container_view,
|
R.id.fragment_advanced_unlock_container_view,
|
||||||
@@ -258,11 +293,6 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
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)
|
||||||
}
|
}
|
||||||
@@ -296,9 +326,6 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
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 {
|
||||||
@@ -389,6 +416,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
||||||
// Check if database really loaded
|
// Check if database really loaded
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
|
mDeviceUnlockViewModel.allowAutoOpenBiometricPrompt = true
|
||||||
clearCredentialsViews(clearKeyFile = true, clearHardwareKey = true)
|
clearCredentialsViews(clearKeyFile = true, clearHardwareKey = true)
|
||||||
GroupActivity.launch(this,
|
GroupActivity.launch(this,
|
||||||
database,
|
database,
|
||||||
@@ -400,23 +428,6 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
@@ -433,7 +444,20 @@ 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()
|
||||||
@@ -485,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()
|
||||||
@@ -515,8 +539,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
|||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -645,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) {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class DatabaseChangedDialogFragment : DatabaseDialogFragment() {
|
|||||||
|
|
||||||
val oldSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelableCompat(OLD_FILE_DATABASE_INFO)
|
val oldSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelableCompat(OLD_FILE_DATABASE_INFO)
|
||||||
val newSnapFileDatabaseInfo: SnapFileDatabaseInfo? = arguments?.getParcelableCompat(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
|
||||||
@@ -54,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))
|
||||||
}
|
}
|
||||||
@@ -77,15 +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.ContextualDatabase
|
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
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ 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() {
|
||||||
|
|
||||||
@@ -90,27 +89,21 @@ class GroupEditDialogFragment : DatabaseDialogFragment() {
|
|||||||
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
mGroupEditViewModel.onDateSelected.observe(this) { dateMilliseconds ->
|
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)
|
|
||||||
.withMillis(dateMilliseconds)
|
|
||||||
.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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,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()
|
||||||
|
|||||||
@@ -43,8 +43,13 @@ import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
|||||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
import com.kunzisoft.keepass.view.HardwareKeySelectionView
|
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() {
|
||||||
|
|
||||||
@@ -55,11 +60,12 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
private lateinit var rootView: View
|
private lateinit var rootView: View
|
||||||
|
|
||||||
private lateinit var passwordCheckBox: CompoundButton
|
private lateinit var passwordCheckBox: CompoundButton
|
||||||
private lateinit var passwordView: PassKeyView
|
private lateinit var passwordEditView: PasswordEditView
|
||||||
private lateinit var passwordRepeatTextInputLayout: TextInputLayout
|
private lateinit var passwordRepeatTextInputLayout: TextInputLayout
|
||||||
private lateinit var passwordRepeatView: TextView
|
private lateinit var passwordRepeatView: TextView
|
||||||
|
|
||||||
private lateinit var keyFileCheckBox: CompoundButton
|
private lateinit var keyFileCheckBox: CompoundButton
|
||||||
|
private lateinit var keyFileGenerateButton: View
|
||||||
private lateinit var keyFileSelectionView: KeyFileSelectionView
|
private lateinit var keyFileSelectionView: KeyFileSelectionView
|
||||||
|
|
||||||
private lateinit var hardwareKeyCheckBox: CompoundButton
|
private lateinit var hardwareKeyCheckBox: CompoundButton
|
||||||
@@ -141,30 +147,40 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
passwordCheckBox = rootView.findViewById(R.id.password_checkbox)
|
passwordCheckBox = rootView.findViewById(R.id.password_checkbox)
|
||||||
passwordView = 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_checkbox)
|
keyFileCheckBox = rootView.findViewById(R.id.keyfile_checkbox)
|
||||||
|
keyFileGenerateButton = rootView.findViewById(R.id.keyfile_generate)
|
||||||
keyFileSelectionView = rootView.findViewById(R.id.keyfile_selection)
|
keyFileSelectionView = rootView.findViewById(R.id.keyfile_selection)
|
||||||
|
|
||||||
hardwareKeyCheckBox = rootView.findViewById(R.id.hardware_key_checkbox)
|
hardwareKeyCheckBox = rootView.findViewById(R.id.hardware_key_checkbox)
|
||||||
hardwareKeySelectionView = rootView.findViewById(R.id.hardware_key_selection)
|
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 ->
|
||||||
pathUri.getDocumentFile(requireContext())?.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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
keyFileGenerateButton.setOnClickListener {
|
||||||
|
mExternalFileHelper?.createDocument(DEFAULT_KEYFILE_NAME)
|
||||||
|
}
|
||||||
keyFileSelectionView.setOpenDocumentClickListener(mExternalFileHelper)
|
keyFileSelectionView.setOpenDocumentClickListener(mExternalFileHelper)
|
||||||
|
|
||||||
hardwareKeySelectionView.selectionListener = { hardwareKey ->
|
hardwareKeySelectionView.selectionListener = { hardwareKey ->
|
||||||
@@ -202,6 +218,16 @@ 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() {
|
private fun approveMainCredential() {
|
||||||
val errorPassword = verifyPassword()
|
val errorPassword = verifyPassword()
|
||||||
val errorKeyFile = verifyKeyFile()
|
val errorKeyFile = verifyKeyFile()
|
||||||
@@ -250,7 +276,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
var error = false
|
var error = false
|
||||||
passwordRepeatTextInputLayout.error = null
|
passwordRepeatTextInputLayout.error = null
|
||||||
if (passwordCheckBox.isChecked) {
|
if (passwordCheckBox.isChecked) {
|
||||||
mMasterPassword = passwordView.passwordString
|
mMasterPassword = passwordEditView.passwordString
|
||||||
val confPassword = passwordRepeatView.text.toString()
|
val confPassword = passwordRepeatView.text.toString()
|
||||||
|
|
||||||
// Verify that passwords match
|
// Verify that passwords match
|
||||||
@@ -302,13 +328,13 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
// To check checkboxes if a text is present
|
// To check checkboxes if a text is present
|
||||||
passwordView.addTextChangedListener(passwordTextWatcher)
|
passwordEditView.addTextChangedListener(passwordTextWatcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
passwordView.removeTextChangedListener(passwordTextWatcher)
|
passwordEditView.removeTextChangedListener(passwordTextWatcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showEmptyPasswordConfirmationDialog() {
|
private fun showEmptyPasswordConfirmationDialog() {
|
||||||
@@ -339,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()
|
||||||
}
|
}
|
||||||
@@ -362,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()
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ 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
|
||||||
@@ -224,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()
|
||||||
@@ -310,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ 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)
|
||||||
@@ -80,7 +80,7 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
|
|||||||
passphraseCopyView?.setOnClickListener {
|
passphraseCopyView?.setOnClickListener {
|
||||||
clipboardHelper.timeoutCopyToClipboard(
|
clipboardHelper.timeoutCopyToClipboard(
|
||||||
getString(R.string.passphrase),
|
getString(R.string.passphrase),
|
||||||
passKeyView.passwordString,
|
passwordEditView.passwordString,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -146,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) {
|
||||||
@@ -219,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ 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)
|
||||||
@@ -101,7 +101,7 @@ class PasswordGeneratorFragment : DatabaseFragment() {
|
|||||||
passwordCopyView?.setOnClickListener {
|
passwordCopyView?.setOnClickListener {
|
||||||
clipboardHelper.timeoutCopyToClipboard(
|
clipboardHelper.timeoutCopyToClipboard(
|
||||||
getString(R.string.password),
|
getString(R.string.password),
|
||||||
passKeyView.passwordString,
|
passwordEditView.passwordString,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -195,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) {
|
||||||
@@ -310,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() {
|
||||||
|
|||||||
@@ -47,10 +47,14 @@ 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 {
|
||||||
@@ -184,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()
|
||||||
@@ -414,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) {
|
||||||
@@ -429,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()
|
||||||
|
|
||||||
@@ -481,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ 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.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
|
||||||
@@ -77,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?) {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -112,12 +115,10 @@ class BreadcrumbAdapter(val context: Context)
|
|||||||
|
|
||||||
holder.groupNumbersView?.apply {
|
holder.groupNumbersView?.apply {
|
||||||
if (mShowNumberEntries) {
|
if (mShowNumberEntries) {
|
||||||
group.refreshNumberOfChildEntries(
|
text = group.getNumberOfChildEntries(
|
||||||
Group.ChildFilter.getDefaults(
|
mNodeFilter.recursiveNumberOfEntries,
|
||||||
PreferencesUtil.showExpiredEntries(context)
|
mNodeFilter.filter
|
||||||
)
|
).toString()
|
||||||
)
|
|
||||||
text = group.recursiveNumberOfChildEntries.toString()
|
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
visibility = View.GONE
|
visibility = View.GONE
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.adapters
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -66,6 +67,7 @@ class NodesAdapter (
|
|||||||
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
|
||||||
@@ -82,7 +84,7 @@ class NodesAdapter (
|
|||||||
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
|
||||||
|
|
||||||
@@ -161,9 +163,7 @@ class NodesAdapter (
|
|||||||
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(
|
this.mNodeFilters = NodeFilter(context, database)
|
||||||
PreferencesUtil.showExpiredEntries(context)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Reinit textSize for all view type
|
// Reinit textSize for all view type
|
||||||
mCalculateViewTypeTextSize.forEachIndexed { index, _ -> mCalculateViewTypeTextSize[index] = true }
|
mCalculateViewTypeTextSize.forEachIndexed { index, _ -> mCalculateViewTypeTextSize[index] = true }
|
||||||
@@ -176,7 +176,7 @@ class NodesAdapter (
|
|||||||
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) {
|
||||||
@@ -205,6 +205,11 @@ class NodesAdapter (
|
|||||||
&& 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,7 +478,10 @@ class NodesAdapter (
|
|||||||
if (mShowNumberEntries) {
|
if (mShowNumberEntries) {
|
||||||
holder.numberChildren?.apply {
|
holder.numberChildren?.apply {
|
||||||
text = (subNode as Group)
|
text = (subNode as Group)
|
||||||
.recursiveNumberOfChildEntries
|
.getNumberOfChildEntries(
|
||||||
|
mNodeFilter.recursiveNumberOfEntries,
|
||||||
|
mNodeFilter.filter
|
||||||
|
)
|
||||||
.toString()
|
.toString()
|
||||||
setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier)
|
setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier)
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
@@ -522,6 +530,9 @@ class NodesAdapter (
|
|||||||
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 ->
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ 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.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
|
||||||
@@ -69,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?) {
|
||||||
@@ -97,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)
|
||||||
@@ -174,10 +177,22 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ 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 com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
@@ -175,8 +176,8 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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')
|
||||||
@@ -191,7 +192,7 @@ object AutofillHelper {
|
|||||||
} else {
|
} else {
|
||||||
datasetBuilder.addValueToDatasetBuilder(
|
datasetBuilder.addValueToDatasetBuilder(
|
||||||
it,
|
it,
|
||||||
AutofillValue.forDate(entryInfo.expiryTime.date.time)
|
AutofillValue.forDate(entryInfo.expiryTime.toMilliseconds())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -321,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
|
||||||
|
|||||||
@@ -21,12 +21,21 @@ 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.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
@@ -35,6 +44,7 @@ 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.autofill.StructureParser.Companion.APPLICATION_ID_POPUP_WINDOW
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
@@ -99,8 +109,12 @@ class KeeAutofillService : AutofillService() {
|
|||||||
StructureParser(latestStructure).parse()?.let { parseResult ->
|
StructureParser(latestStructure).parse()?.let { parseResult ->
|
||||||
|
|
||||||
// Build search info only if applicationId or webDomain are not blocked
|
// Build search info only if applicationId or webDomain are not blocked
|
||||||
if (autofillAllowedFor(parseResult.applicationId, applicationIdBlocklist)
|
if (autofillAllowedFor(
|
||||||
&& autofillAllowedFor(parseResult.webDomain, webDomainBlocklist)) {
|
applicationId = parseResult.applicationId,
|
||||||
|
applicationIdBlocklist = applicationIdBlocklist,
|
||||||
|
webDomain = parseResult.webDomain,
|
||||||
|
webDomainBlocklist = webDomainBlocklist)
|
||||||
|
) {
|
||||||
val searchInfo = SearchInfo().apply {
|
val searchInfo = SearchInfo().apply {
|
||||||
applicationId = parseResult.applicationId
|
applicationId = parseResult.applicationId
|
||||||
webDomain = parseResult.webDomain
|
webDomain = parseResult.webDomain
|
||||||
@@ -258,7 +272,7 @@ class KeeAutofillService : AutofillService() {
|
|||||||
val inlinePresentationSpecs =
|
val inlinePresentationSpecs =
|
||||||
inlineSuggestionsRequest.inlinePresentationSpecs
|
inlineSuggestionsRequest.inlinePresentationSpecs
|
||||||
if (inlineSuggestionsRequest.maxSuggestionCount > 0
|
if (inlineSuggestionsRequest.maxSuggestionCount > 0
|
||||||
&& inlinePresentationSpecs.size > 0
|
&& inlinePresentationSpecs.isNotEmpty()
|
||||||
) {
|
) {
|
||||||
val inlinePresentationSpec = inlinePresentationSpecs[0]
|
val inlinePresentationSpec = inlinePresentationSpecs[0]
|
||||||
|
|
||||||
@@ -274,11 +288,7 @@ class KeeAutofillService : AutofillService() {
|
|||||||
this,
|
this,
|
||||||
0,
|
0,
|
||||||
Intent(this, AutofillSettingsActivity::class.java),
|
Intent(this, AutofillSettingsActivity::class.java),
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
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))
|
||||||
@@ -352,8 +362,12 @@ class KeeAutofillService : AutofillService() {
|
|||||||
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
|
||||||
@@ -414,6 +428,28 @@ class KeeAutofillService : AutofillService() {
|
|||||||
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
|
||||||
}
|
}
|
||||||
@@ -134,19 +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) -> {
|
||||||
// Priority to username or add if null
|
// Replace username until we have a password
|
||||||
if (result?.usernameId == null || it.contains(View.AUTOFILL_HINT_USERNAME, true)) {
|
if (result?.passwordId == null) {
|
||||||
result?.usernameId = autofillId
|
result?.usernameId = autofillId
|
||||||
result?.usernameValue = node.autofillValue
|
result?.usernameValue = node.autofillValue
|
||||||
Log.d(TAG, "Autofill username hint")
|
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")
|
||||||
@@ -282,9 +298,11 @@ 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" -> {
|
||||||
// Assume username is before password
|
// Assume username is before password
|
||||||
@@ -321,89 +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) -> {
|
|
||||||
// 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)}")
|
|
||||||
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
|
||||||
@@ -432,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>()
|
||||||
@@ -510,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,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,677 +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.core.view.MenuProvider
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.kunzisoft.keepass.R
|
|
||||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
|
||||||
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
|
|
||||||
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: Fragment(), 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
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
private val menuProvider: MenuProvider = object: MenuProvider {
|
|
||||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
// 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 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
deleteEncryptedDatabaseKey()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 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)
|
|
||||||
|
|
||||||
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.inflate(R.layout.fragment_advanced_unlock, container, false)
|
|
||||||
|
|
||||||
mAdvancedUnlockInfoView = rootView.findViewById(R.id.advanced_unlock_view)
|
|
||||||
|
|
||||||
return rootView
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
activity?.addMenuProvider(menuProvider, viewLifecycleOwner)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
context?.let {
|
|
||||||
mAdvancedUnlockEnabled = PreferencesUtil.isAdvancedUnlockEnable(it)
|
|
||||||
mAutoOpenPromptEnabled = PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(it)
|
|
||||||
}
|
|
||||||
keepConnection = false
|
|
||||||
}
|
|
||||||
|
|
||||||
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)) {
|
|
||||||
// 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)) {
|
|
||||||
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(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
private fun openBiometricSetting() {
|
|
||||||
mAdvancedUnlockInfoView?.setIconViewClickListener {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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.unavailable)
|
|
||||||
setAdvancedUnlockedMessageView("")
|
|
||||||
|
|
||||||
context?.let { context ->
|
|
||||||
mAdvancedUnlockInfoView?.setIconViewClickListener {
|
|
||||||
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.unlock_and_link_biometric)
|
|
||||||
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.unlock)
|
|
||||||
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 (mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt
|
|
||||||
&& mAutoOpenPromptEnabled) {
|
|
||||||
mAdvancedUnlockViewModel.allowAutoOpenBiometricPrompt = false
|
|
||||||
openAdvancedUnlockPrompt(cryptoPrompt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: deleteEncryptedDatabaseKey()
|
|
||||||
}
|
|
||||||
} ?: throw UnknownDatabaseLocationException()
|
|
||||||
} ?: 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?.setMessage(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,506 +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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(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
|
||||||
|
}
|
||||||
@@ -40,6 +40,7 @@ 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.app.ActivityCompat.shouldShowRequestPermissionRationale
|
||||||
import androidx.core.content.ContextCompat
|
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
|
||||||
@@ -182,8 +183,11 @@ class DatabaseTaskProvider(
|
|||||||
|
|
||||||
private var databaseInfoListener = object:
|
private var databaseInfoListener = object:
|
||||||
DatabaseTaskNotificationService.DatabaseInfoListener {
|
DatabaseTaskNotificationService.DatabaseInfoListener {
|
||||||
override fun onDatabaseInfoChanged(previousDatabaseInfo: SnapFileDatabaseInfo,
|
override fun onDatabaseInfoChanged(
|
||||||
newDatabaseInfo: SnapFileDatabaseInfo) {
|
previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
newDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
readOnlyDatabase: Boolean
|
||||||
|
) {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
activity.lifecycleScope.launch {
|
activity.lifecycleScope.launch {
|
||||||
if (databaseChangedDialogFragment == null) {
|
if (databaseChangedDialogFragment == null) {
|
||||||
@@ -195,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
|
||||||
@@ -326,11 +331,11 @@ class DatabaseTaskProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
||||||
|
|||||||
@@ -51,8 +51,10 @@ open class SaveDatabaseRunnable(
|
|||||||
// Build temp database file to avoid file corruption if error
|
// Build temp database file to avoid file corruption if error
|
||||||
database.saveData(
|
database.saveData(
|
||||||
cacheFile = File(context.cacheDir, databaseCopyUri.hashCode().toString()),
|
cacheFile = File(context.cacheDir, databaseCopyUri.hashCode().toString()),
|
||||||
databaseOutputStream = contentResolver
|
databaseOutputStream = {
|
||||||
.getUriOutputStream(databaseCopyUri ?: database.fileUri),
|
contentResolver
|
||||||
|
.getUriOutputStream(databaseCopyUri ?: database.fileUri)
|
||||||
|
},
|
||||||
isNewLocation = databaseCopyUri == null,
|
isNewLocation = databaseCopyUri == null,
|
||||||
mainCredential?.toMasterCredential(contentResolver),
|
mainCredential?.toMasterCredential(contentResolver),
|
||||||
challengeResponseRetriever)
|
challengeResponseRetriever)
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ object SearchHelper {
|
|||||||
// If search provide results
|
// If search provide results
|
||||||
database.createVirtualGroupFromSearchInfo(
|
database.createVirtualGroupFromSearchInfo(
|
||||||
searchInfo.toString(),
|
searchInfo.toString(),
|
||||||
|
searchInfo.isASearchByDomain(),
|
||||||
MAX_SEARCH_ENTRY
|
MAX_SEARCH_ENTRY
|
||||||
)?.let { searchGroup ->
|
)?.let { searchGroup ->
|
||||||
if (searchGroup.numberOfChildEntries > 0) {
|
if (searchGroup.numberOfChildEntries > 0) {
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package com.kunzisoft.keepass.model
|
||||||
|
|
||||||
|
data class DataDate(val year: Int, val month: Int, val day: Int)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package com.kunzisoft.keepass.model
|
||||||
|
|
||||||
|
data class DataTime(val hour: Int, val minute: Int)
|
||||||
@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.password
|
|||||||
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.text.Editable
|
||||||
import android.text.Spannable
|
import android.text.Spannable
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
@@ -253,51 +254,62 @@ class PasswordGenerator(private val resources: Resources) {
|
|||||||
return charSet.toString()
|
return charSet.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun colorizedPassword(editable: Editable?) {
|
||||||
|
editable.toString().forEachIndexed { index, char ->
|
||||||
|
colorFromChar(char)?.let { color ->
|
||||||
|
editable?.setSpan(
|
||||||
|
ForegroundColorSpan(color),
|
||||||
|
index,
|
||||||
|
index + 1,
|
||||||
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getColorizedPassword(password: String): Spannable {
|
fun getColorizedPassword(password: String): Spannable {
|
||||||
val spannableString = SpannableStringBuilder()
|
val spannableString = SpannableStringBuilder()
|
||||||
if (password.isNotEmpty()) {
|
if (password.isNotEmpty()) {
|
||||||
password.forEach {
|
password.forEach { char ->
|
||||||
when {
|
colorFromChar(char)?.let { color ->
|
||||||
UPPERCASE_CHARS.contains(it)||
|
val spannableColorChar = SpannableString(char.toString())
|
||||||
LOWERCASE_CHARS.contains(it) -> {
|
spannableColorChar.setSpan(
|
||||||
spannableString.append(it)
|
ForegroundColorSpan(color),
|
||||||
}
|
0,
|
||||||
DIGIT_CHARS.contains(it) -> {
|
1,
|
||||||
// RED
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
spannableString.append(colorizeChar(it, Color.rgb(246, 79, 62)))
|
)
|
||||||
}
|
spannableString.append(spannableColorChar)
|
||||||
SPECIAL_CHARS.contains(it) -> {
|
} ?: spannableString.append(char)
|
||||||
// Blue
|
|
||||||
spannableString.append(colorizeChar(it, Color.rgb(39, 166, 228)))
|
|
||||||
}
|
|
||||||
MINUS_CHAR.contains(it)||
|
|
||||||
UNDERLINE_CHAR.contains(it)||
|
|
||||||
BRACKET_CHARS.contains(it) -> {
|
|
||||||
// Purple
|
|
||||||
spannableString.append(colorizeChar(it, Color.rgb(185, 38, 209)))
|
|
||||||
}
|
|
||||||
extendedChars().contains(it) -> {
|
|
||||||
// Green
|
|
||||||
spannableString.append(colorizeChar(it, Color.rgb(44, 181, 50)))
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
spannableString.append(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return spannableString
|
return spannableString
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun colorizeChar(char: Char, color: Int): Spannable {
|
private fun colorFromChar(char: Char): Int? {
|
||||||
val spannableColorChar = SpannableString(char.toString())
|
return when {
|
||||||
spannableColorChar.setSpan(
|
DIGIT_CHARS.contains(char) -> {
|
||||||
ForegroundColorSpan(color),
|
// RED
|
||||||
0,
|
Color.rgb(246, 79, 62)
|
||||||
1,
|
}
|
||||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
SPECIAL_CHARS.contains(char) -> {
|
||||||
)
|
// Blue
|
||||||
return spannableColorChar
|
Color.rgb(39, 166, 228)
|
||||||
|
}
|
||||||
|
MINUS_CHAR.contains(char)||
|
||||||
|
UNDERLINE_CHAR.contains(char)||
|
||||||
|
BRACKET_CHARS.contains(char) -> {
|
||||||
|
// Purple
|
||||||
|
Color.rgb(185, 38, 209)
|
||||||
|
}
|
||||||
|
extendedChars().contains(char) -> {
|
||||||
|
// Green
|
||||||
|
Color.rgb(44, 181, 50)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
|||||||
return mTempCipherDao.firstOrNull { it.databaseUri == databaseUri.toString()}
|
return mTempCipherDao.firstOrNull { it.databaseUri == databaseUri.toString()}
|
||||||
}
|
}
|
||||||
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity) {
|
fun addOrUpdateCipherDatabase(cipherDatabaseEntity: CipherDatabaseEntity) {
|
||||||
val cipherDatabaseRetrieve = mTempCipherDao.firstOrNull { it.databaseUri == cipherDatabaseEntity.databaseUri }
|
val cipherDatabaseRetrieve = mTempCipherDao.firstOrNull {
|
||||||
|
it.databaseUri == cipherDatabaseEntity.databaseUri
|
||||||
|
}
|
||||||
cipherDatabaseRetrieve?.replaceContent(cipherDatabaseEntity)
|
cipherDatabaseRetrieve?.replaceContent(cipherDatabaseEntity)
|
||||||
?: mTempCipherDao.add(cipherDatabaseEntity)
|
?: mTempCipherDao.add(cipherDatabaseEntity)
|
||||||
}
|
}
|
||||||
@@ -35,6 +37,9 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
|||||||
mTempCipherDao.remove(it)
|
mTempCipherDao.remove(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun resetTimer() {
|
||||||
|
resetTimeJob()
|
||||||
|
}
|
||||||
fun deleteAll() {
|
fun deleteAll() {
|
||||||
mTempCipherDao.clear()
|
mTempCipherDao.clear()
|
||||||
}
|
}
|
||||||
@@ -86,11 +91,19 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
|||||||
val notificationTimeoutMilliSecs = PreferencesUtil.getAdvancedUnlockTimeout(this)
|
val notificationTimeoutMilliSecs = PreferencesUtil.getAdvancedUnlockTimeout(this)
|
||||||
// Not necessarily a foreground service
|
// Not necessarily a foreground service
|
||||||
if (mTimerJob == null && notificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
|
if (mTimerJob == null && notificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
|
||||||
defineTimerJob(notificationBuilder, notificationTimeoutMilliSecs) {
|
defineTimerJob(
|
||||||
|
notificationBuilder,
|
||||||
|
NotificationServiceType.ADVANCED_UNLOCK,
|
||||||
|
notificationTimeoutMilliSecs
|
||||||
|
) {
|
||||||
sendBroadcast(Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION))
|
sendBroadcast(Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
startForeground(notificationId, notificationBuilder.build())
|
startForegroundCompat(
|
||||||
|
notificationId,
|
||||||
|
notificationBuilder,
|
||||||
|
NotificationServiceType.ADVANCED_UNLOCK
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return mActionTaskBinder
|
return mActionTaskBinder
|
||||||
|
|||||||
@@ -36,13 +36,13 @@ 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.tasks.BinaryDatabaseManager
|
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.*
|
import java.util.LinkedList
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
||||||
|
|
||||||
@@ -282,15 +282,21 @@ class AttachmentFileNotificationService: LockNotificationService() {
|
|||||||
AttachmentState.ERROR -> {
|
AttachmentState.ERROR -> {
|
||||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH)
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH)
|
||||||
try {
|
try {
|
||||||
notificationManager?.notify(
|
checkNotificationsPermission(this) {
|
||||||
attachmentNotification.notificationId,
|
notificationManager?.notify(
|
||||||
builder.build()
|
attachmentNotification.notificationId,
|
||||||
)
|
builder.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
} catch (e: SecurityException) {
|
} catch (e: SecurityException) {
|
||||||
Log.e(TAG, "Unable to notify the attachment state", e)
|
Log.e(TAG, "Unable to notify the attachment state", e)
|
||||||
}
|
}
|
||||||
} else -> {
|
} else -> {
|
||||||
startForeground(attachmentNotification.notificationId, builder.build())
|
startForegroundCompat(
|
||||||
|
attachmentNotification.notificationId,
|
||||||
|
builder,
|
||||||
|
NotificationServiceType.ATTACHMENT
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,16 +19,12 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.services
|
package com.kunzisoft.keepass.services
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
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.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
||||||
@@ -196,23 +192,29 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
//Get settings
|
//Get settings
|
||||||
val notificationTimeoutMilliSecs = PreferencesUtil.getClipboardTimeout(this)
|
val notificationTimeoutMilliSecs = PreferencesUtil.getClipboardTimeout(this)
|
||||||
if (notificationTimeoutMilliSecs != NEVER) {
|
if (notificationTimeoutMilliSecs != NEVER) {
|
||||||
defineTimerJob(builder, notificationTimeoutMilliSecs, {
|
defineTimerJob(
|
||||||
val newGeneratedValue = fieldToCopy.getGeneratedValue(mEntryInfo)
|
builder,
|
||||||
// New auto generated value
|
NotificationServiceType.CLIPBOARD,
|
||||||
if (generatedValue != newGeneratedValue) {
|
notificationTimeoutMilliSecs,
|
||||||
generatedValue = newGeneratedValue
|
{
|
||||||
clipboardHelper?.copyToClipboard(
|
val newGeneratedValue = fieldToCopy.getGeneratedValue(mEntryInfo)
|
||||||
fieldToCopy.label,
|
// New auto generated value
|
||||||
generatedValue,
|
if (generatedValue != newGeneratedValue) {
|
||||||
fieldToCopy.isSensitive
|
generatedValue = newGeneratedValue
|
||||||
)
|
clipboardHelper?.copyToClipboard(
|
||||||
|
fieldToCopy.label,
|
||||||
|
generatedValue,
|
||||||
|
fieldToCopy.isSensitive
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stopNotificationAndSendLockIfNeeded()
|
||||||
|
// Clean password only if no next field
|
||||||
|
if (nextFields.size <= 0)
|
||||||
|
clipboardHelper?.cleanClipboard()
|
||||||
}
|
}
|
||||||
}) {
|
)
|
||||||
stopNotificationAndSendLockIfNeeded()
|
|
||||||
// Clean password only if no next field
|
|
||||||
if (nextFields.size <= 0)
|
|
||||||
clipboardHelper?.cleanClipboard()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// No timer
|
// No timer
|
||||||
checkNotificationsPermission {
|
checkNotificationsPermission {
|
||||||
@@ -226,12 +228,11 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun checkNotificationsPermission(action: () -> Unit) {
|
private fun checkNotificationsPermission(action: () -> Unit) {
|
||||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
checkNotificationsPermission(
|
||||||
== PackageManager.PERMISSION_GRANTED) {
|
this,
|
||||||
action.invoke()
|
PreferencesUtil.isClipboardNotificationsEnable(this),
|
||||||
} else {
|
action
|
||||||
showPermissionErrorIfNeeded(this)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
@@ -255,26 +256,14 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
const val EXTRA_CLIPBOARD_FIELDS = "EXTRA_CLIPBOARD_FIELDS"
|
const val EXTRA_CLIPBOARD_FIELDS = "EXTRA_CLIPBOARD_FIELDS"
|
||||||
const val ACTION_CLEAN_CLIPBOARD = "ACTION_CLEAN_CLIPBOARD"
|
const val ACTION_CLEAN_CLIPBOARD = "ACTION_CLEAN_CLIPBOARD"
|
||||||
|
|
||||||
private fun showPermissionErrorIfNeeded(context: Context) {
|
|
||||||
if (PreferencesUtil.isClipboardNotificationsEnable(context)) {
|
|
||||||
Toast.makeText(context, R.string.warning_copy_permission, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkAndLaunchNotification(
|
fun checkAndLaunchNotification(
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
entry: EntryInfo
|
entry: EntryInfo
|
||||||
) {
|
) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
checkNotificationsPermission(
|
||||||
if (ContextCompat.checkSelfPermission(
|
activity,
|
||||||
activity,
|
PreferencesUtil.isClipboardNotificationsEnable(activity)
|
||||||
Manifest.permission.POST_NOTIFICATIONS
|
) {
|
||||||
) == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
launchNotificationIfAllowed(activity, entry)
|
|
||||||
} else {
|
|
||||||
showPermissionErrorIfNeeded(activity)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
launchNotificationIfAllowed(activity, entry)
|
launchNotificationIfAllowed(activity, entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,10 +36,24 @@ import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
|||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.database.ProgressMessage
|
import com.kunzisoft.keepass.database.ProgressMessage
|
||||||
import com.kunzisoft.keepass.database.action.*
|
import com.kunzisoft.keepass.database.action.CreateDatabaseRunnable
|
||||||
|
import com.kunzisoft.keepass.database.action.LoadDatabaseRunnable
|
||||||
|
import com.kunzisoft.keepass.database.action.MergeDatabaseRunnable
|
||||||
|
import com.kunzisoft.keepass.database.action.ReloadDatabaseRunnable
|
||||||
|
import com.kunzisoft.keepass.database.action.RemoveUnlinkedDataDatabaseRunnable
|
||||||
|
import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable
|
||||||
|
import com.kunzisoft.keepass.database.action.UpdateCompressionBinariesDatabaseRunnable
|
||||||
import com.kunzisoft.keepass.database.action.history.DeleteEntryHistoryDatabaseRunnable
|
import com.kunzisoft.keepass.database.action.history.DeleteEntryHistoryDatabaseRunnable
|
||||||
import com.kunzisoft.keepass.database.action.history.RestoreEntryHistoryDatabaseRunnable
|
import com.kunzisoft.keepass.database.action.history.RestoreEntryHistoryDatabaseRunnable
|
||||||
import com.kunzisoft.keepass.database.action.node.*
|
import com.kunzisoft.keepass.database.action.node.ActionNodesValues
|
||||||
|
import com.kunzisoft.keepass.database.action.node.AddEntryRunnable
|
||||||
|
import com.kunzisoft.keepass.database.action.node.AddGroupRunnable
|
||||||
|
import com.kunzisoft.keepass.database.action.node.AfterActionNodesFinish
|
||||||
|
import com.kunzisoft.keepass.database.action.node.CopyNodesRunnable
|
||||||
|
import com.kunzisoft.keepass.database.action.node.DeleteNodesRunnable
|
||||||
|
import com.kunzisoft.keepass.database.action.node.MoveNodesRunnable
|
||||||
|
import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable
|
||||||
|
import com.kunzisoft.keepass.database.action.node.UpdateGroupRunnable
|
||||||
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
|
||||||
@@ -62,9 +76,17 @@ import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
|||||||
import com.kunzisoft.keepass.utils.getParcelableList
|
import com.kunzisoft.keepass.utils.getParcelableList
|
||||||
import com.kunzisoft.keepass.utils.putParcelableList
|
import com.kunzisoft.keepass.utils.putParcelableList
|
||||||
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
|
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import java.util.*
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
open class DatabaseTaskNotificationService : LockNotificationService(), ProgressTaskUpdater {
|
open class DatabaseTaskNotificationService : LockNotificationService(), ProgressTaskUpdater {
|
||||||
|
|
||||||
@@ -139,6 +161,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
fun onDatabaseInfoChanged(
|
fun onDatabaseInfoChanged(
|
||||||
previousDatabaseInfo: SnapFileDatabaseInfo,
|
previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
newDatabaseInfo: SnapFileDatabaseInfo,
|
newDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
|
readOnlyDatabase: Boolean
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,8 +220,11 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
// Call listener to indicate a change in database info
|
// Call listener to indicate a change in database info
|
||||||
if (!mSaveState && previousDatabaseInfo != null) {
|
if (!mSaveState && previousDatabaseInfo != null) {
|
||||||
mDatabaseInfoListeners.forEach { listener ->
|
mDatabaseInfoListeners.forEach { listener ->
|
||||||
listener.onDatabaseInfoChanged(previousDatabaseInfo,
|
listener.onDatabaseInfoChanged(
|
||||||
lastFileDatabaseInfo)
|
previousDatabaseInfo,
|
||||||
|
lastFileDatabaseInfo,
|
||||||
|
mDatabase?.isReadOnly ?: true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mSnapFileDatabaseInfo = lastFileDatabaseInfo
|
mSnapFileDatabaseInfo = lastFileDatabaseInfo
|
||||||
@@ -565,7 +591,11 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create the notification
|
// Create the notification
|
||||||
startForeground(notificationId, notificationBuilder.build())
|
startForegroundCompat(
|
||||||
|
notificationId,
|
||||||
|
notificationBuilder,
|
||||||
|
NotificationServiceType.DATABASE_TASK
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeIntentData(intent: Intent?) {
|
private fun removeIntentData(intent: Intent?) {
|
||||||
@@ -831,6 +861,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
if (intent.hasExtra(MAIN_CREDENTIAL_KEY)) {
|
if (intent.hasExtra(MAIN_CREDENTIAL_KEY)) {
|
||||||
databaseToMergeMainCredential = intent.getParcelableExtraCompat(MAIN_CREDENTIAL_KEY)
|
databaseToMergeMainCredential = intent.getParcelableExtraCompat(MAIN_CREDENTIAL_KEY)
|
||||||
}
|
}
|
||||||
|
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||||
return MergeDatabaseRunnable(
|
return MergeDatabaseRunnable(
|
||||||
this,
|
this,
|
||||||
databaseToMergeUri,
|
databaseToMergeUri,
|
||||||
@@ -839,7 +870,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||||
},
|
},
|
||||||
database,
|
database,
|
||||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
!database.isReadOnly && saveDatabase,
|
||||||
{ hardwareKey, seed ->
|
{ hardwareKey, seed ->
|
||||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||||
},
|
},
|
||||||
@@ -932,12 +963,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
val parentId: NodeId<*>? = intent.getParcelableExtraCompat(PARENT_ID_KEY)
|
val parentId: NodeId<*>? = intent.getParcelableExtraCompat(PARENT_ID_KEY)
|
||||||
val newGroup: Group? = intent.getParcelableExtraCompat(GROUP_KEY)
|
val newGroup: Group? = intent.getParcelableExtraCompat(GROUP_KEY)
|
||||||
if (parentId == null || newGroup == null) return null
|
if (parentId == null || newGroup == null) return null
|
||||||
|
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||||
database.getGroupById(parentId)?.let { parent ->
|
database.getGroupById(parentId)?.let { parent ->
|
||||||
AddGroupRunnable(this,
|
AddGroupRunnable(this,
|
||||||
database,
|
database,
|
||||||
newGroup,
|
newGroup,
|
||||||
parent,
|
parent,
|
||||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
!database.isReadOnly && saveDatabase,
|
||||||
AfterActionNodesRunnable()
|
AfterActionNodesRunnable()
|
||||||
) { hardwareKey, seed ->
|
) { hardwareKey, seed ->
|
||||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||||
@@ -959,12 +991,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
val groupId: NodeId<*>? = intent.getParcelableExtraCompat(GROUP_ID_KEY)
|
val groupId: NodeId<*>? = intent.getParcelableExtraCompat(GROUP_ID_KEY)
|
||||||
val newGroup: Group? = intent.getParcelableExtraCompat(GROUP_KEY)
|
val newGroup: Group? = intent.getParcelableExtraCompat(GROUP_KEY)
|
||||||
if (groupId == null || newGroup == null) return null
|
if (groupId == null || newGroup == null) return null
|
||||||
|
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||||
database.getGroupById(groupId)?.let { oldGroup ->
|
database.getGroupById(groupId)?.let { oldGroup ->
|
||||||
UpdateGroupRunnable(this,
|
UpdateGroupRunnable(this,
|
||||||
database,
|
database,
|
||||||
oldGroup,
|
oldGroup,
|
||||||
newGroup,
|
newGroup,
|
||||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
!database.isReadOnly && saveDatabase,
|
||||||
AfterActionNodesRunnable()
|
AfterActionNodesRunnable()
|
||||||
) { hardwareKey, seed ->
|
) { hardwareKey, seed ->
|
||||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||||
@@ -986,12 +1019,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
val parentId: NodeId<*>? = intent.getParcelableExtraCompat(PARENT_ID_KEY)
|
val parentId: NodeId<*>? = intent.getParcelableExtraCompat(PARENT_ID_KEY)
|
||||||
val newEntry: Entry? = intent.getParcelableExtraCompat(ENTRY_KEY)
|
val newEntry: Entry? = intent.getParcelableExtraCompat(ENTRY_KEY)
|
||||||
if (parentId == null || newEntry == null) return null
|
if (parentId == null || newEntry == null) return null
|
||||||
|
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||||
database.getGroupById(parentId)?.let { parent ->
|
database.getGroupById(parentId)?.let { parent ->
|
||||||
AddEntryRunnable(this,
|
AddEntryRunnable(this,
|
||||||
database,
|
database,
|
||||||
newEntry,
|
newEntry,
|
||||||
parent,
|
parent,
|
||||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
!database.isReadOnly && saveDatabase,
|
||||||
AfterActionNodesRunnable()
|
AfterActionNodesRunnable()
|
||||||
) { hardwareKey, seed ->
|
) { hardwareKey, seed ->
|
||||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||||
@@ -1013,12 +1047,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
val entryId: NodeId<UUID>? = intent.getParcelableExtraCompat(ENTRY_ID_KEY)
|
val entryId: NodeId<UUID>? = intent.getParcelableExtraCompat(ENTRY_ID_KEY)
|
||||||
val newEntry: Entry? = intent.getParcelableExtraCompat(ENTRY_KEY)
|
val newEntry: Entry? = intent.getParcelableExtraCompat(ENTRY_KEY)
|
||||||
if (entryId == null || newEntry == null) return null
|
if (entryId == null || newEntry == null) return null
|
||||||
|
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||||
database.getEntryById(entryId)?.let { oldEntry ->
|
database.getEntryById(entryId)?.let { oldEntry ->
|
||||||
UpdateEntryRunnable(this,
|
UpdateEntryRunnable(this,
|
||||||
database,
|
database,
|
||||||
oldEntry,
|
oldEntry,
|
||||||
newEntry,
|
newEntry,
|
||||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
!database.isReadOnly && saveDatabase,
|
||||||
AfterActionNodesRunnable()
|
AfterActionNodesRunnable()
|
||||||
) { hardwareKey, seed ->
|
) { hardwareKey, seed ->
|
||||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||||
@@ -1039,12 +1074,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val parentId: NodeId<*> = intent.getParcelableExtraCompat(PARENT_ID_KEY) ?: return null
|
val parentId: NodeId<*> = intent.getParcelableExtraCompat(PARENT_ID_KEY) ?: return null
|
||||||
|
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||||
database.getGroupById(parentId)?.let { newParent ->
|
database.getGroupById(parentId)?.let { newParent ->
|
||||||
CopyNodesRunnable(this,
|
CopyNodesRunnable(this,
|
||||||
database,
|
database,
|
||||||
getListNodesFromBundle(database, intent.extras!!),
|
getListNodesFromBundle(database, intent.extras!!),
|
||||||
newParent,
|
newParent,
|
||||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
!database.isReadOnly && saveDatabase,
|
||||||
AfterActionNodesRunnable()
|
AfterActionNodesRunnable()
|
||||||
) { hardwareKey, seed ->
|
) { hardwareKey, seed ->
|
||||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||||
@@ -1065,12 +1101,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val parentId: NodeId<*> = intent.getParcelableExtraCompat(PARENT_ID_KEY) ?: return null
|
val parentId: NodeId<*> = intent.getParcelableExtraCompat(PARENT_ID_KEY) ?: return null
|
||||||
|
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||||
database.getGroupById(parentId)?.let { newParent ->
|
database.getGroupById(parentId)?.let { newParent ->
|
||||||
MoveNodesRunnable(this,
|
MoveNodesRunnable(this,
|
||||||
database,
|
database,
|
||||||
getListNodesFromBundle(database, intent.extras!!),
|
getListNodesFromBundle(database, intent.extras!!),
|
||||||
newParent,
|
newParent,
|
||||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
!database.isReadOnly && saveDatabase,
|
||||||
AfterActionNodesRunnable()
|
AfterActionNodesRunnable()
|
||||||
) { hardwareKey, seed ->
|
) { hardwareKey, seed ->
|
||||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||||
@@ -1089,11 +1126,12 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
&& intent.hasExtra(ENTRIES_ID_KEY)
|
&& intent.hasExtra(ENTRIES_ID_KEY)
|
||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
|
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||||
DeleteNodesRunnable(this,
|
DeleteNodesRunnable(this,
|
||||||
database,
|
database,
|
||||||
getListNodesFromBundle(database, intent.extras!!),
|
getListNodesFromBundle(database, intent.extras!!),
|
||||||
resources.getString(R.string.recycle_bin),
|
resources.getString(R.string.recycle_bin),
|
||||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
!database.isReadOnly && saveDatabase,
|
||||||
AfterActionNodesRunnable()
|
AfterActionNodesRunnable()
|
||||||
) { hardwareKey, seed ->
|
) { hardwareKey, seed ->
|
||||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||||
@@ -1112,12 +1150,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val entryId: NodeId<UUID> = intent.getParcelableExtraCompat(ENTRY_ID_KEY) ?: return null
|
val entryId: NodeId<UUID> = intent.getParcelableExtraCompat(ENTRY_ID_KEY) ?: return null
|
||||||
|
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||||
database.getEntryById(entryId)?.let { mainEntry ->
|
database.getEntryById(entryId)?.let { mainEntry ->
|
||||||
RestoreEntryHistoryDatabaseRunnable(this,
|
RestoreEntryHistoryDatabaseRunnable(this,
|
||||||
database,
|
database,
|
||||||
mainEntry,
|
mainEntry,
|
||||||
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
|
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
|
||||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
!database.isReadOnly && saveDatabase
|
||||||
) { hardwareKey, seed ->
|
) { hardwareKey, seed ->
|
||||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||||
}
|
}
|
||||||
@@ -1136,12 +1175,13 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
&& intent.hasExtra(SAVE_DATABASE_KEY)
|
||||||
) {
|
) {
|
||||||
val entryId: NodeId<UUID> = intent.getParcelableExtraCompat(ENTRY_ID_KEY) ?: return null
|
val entryId: NodeId<UUID> = intent.getParcelableExtraCompat(ENTRY_ID_KEY) ?: return null
|
||||||
|
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||||
database.getEntryById(entryId)?.let { mainEntry ->
|
database.getEntryById(entryId)?.let { mainEntry ->
|
||||||
DeleteEntryHistoryDatabaseRunnable(this,
|
DeleteEntryHistoryDatabaseRunnable(this,
|
||||||
database,
|
database,
|
||||||
mainEntry,
|
mainEntry,
|
||||||
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
|
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
|
||||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
!database.isReadOnly && saveDatabase
|
||||||
) { hardwareKey, seed ->
|
) { hardwareKey, seed ->
|
||||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||||
}
|
}
|
||||||
@@ -1162,11 +1202,12 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
val oldElement: CompressionAlgorithm? = intent.getParcelableExtraCompat(OLD_ELEMENT_KEY)
|
val oldElement: CompressionAlgorithm? = intent.getParcelableExtraCompat(OLD_ELEMENT_KEY)
|
||||||
val newElement: CompressionAlgorithm? = intent.getParcelableExtraCompat(NEW_ELEMENT_KEY)
|
val newElement: CompressionAlgorithm? = intent.getParcelableExtraCompat(NEW_ELEMENT_KEY)
|
||||||
if (oldElement == null || newElement == null) return null
|
if (oldElement == null || newElement == null) return null
|
||||||
|
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||||
return UpdateCompressionBinariesDatabaseRunnable(this,
|
return UpdateCompressionBinariesDatabaseRunnable(this,
|
||||||
database,
|
database,
|
||||||
oldElement,
|
oldElement,
|
||||||
newElement,
|
newElement,
|
||||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
!database.isReadOnly && saveDatabase
|
||||||
) { hardwareKey, seed ->
|
) { hardwareKey, seed ->
|
||||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||||
}.apply {
|
}.apply {
|
||||||
@@ -1184,9 +1225,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
): ActionRunnable? {
|
): ActionRunnable? {
|
||||||
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
|
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
|
||||||
|
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||||
return RemoveUnlinkedDataDatabaseRunnable(this,
|
return RemoveUnlinkedDataDatabaseRunnable(this,
|
||||||
database,
|
database,
|
||||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
!database.isReadOnly && saveDatabase
|
||||||
) { hardwareKey, seed ->
|
) { hardwareKey, seed ->
|
||||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||||
}.apply {
|
}.apply {
|
||||||
@@ -1204,9 +1246,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
): ActionRunnable? {
|
): ActionRunnable? {
|
||||||
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
|
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
|
||||||
|
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||||
return SaveDatabaseRunnable(this,
|
return SaveDatabaseRunnable(this,
|
||||||
database,
|
database,
|
||||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
!database.isReadOnly && saveDatabase,
|
||||||
null,
|
null,
|
||||||
{ hardwareKey, seed ->
|
{ hardwareKey, seed ->
|
||||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||||
@@ -1229,13 +1272,14 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
database: ContextualDatabase
|
database: ContextualDatabase
|
||||||
): ActionRunnable? {
|
): ActionRunnable? {
|
||||||
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
|
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
|
||||||
|
val saveDatabase = intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||||
var databaseCopyUri: Uri? = null
|
var databaseCopyUri: Uri? = null
|
||||||
if (intent.hasExtra(DATABASE_URI_KEY)) {
|
if (intent.hasExtra(DATABASE_URI_KEY)) {
|
||||||
databaseCopyUri = intent.getParcelableExtraCompat(DATABASE_URI_KEY)
|
databaseCopyUri = intent.getParcelableExtraCompat(DATABASE_URI_KEY)
|
||||||
}
|
}
|
||||||
SaveDatabaseRunnable(this,
|
SaveDatabaseRunnable(this,
|
||||||
database,
|
database,
|
||||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
!database.isReadOnly && saveDatabase,
|
||||||
null,
|
null,
|
||||||
{ hardwareKey, seed ->
|
{ hardwareKey, seed ->
|
||||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||||
|
|||||||
@@ -111,13 +111,18 @@ class KeyboardEntryNotificationService : LockNotificationService() {
|
|||||||
.setContentIntent(null)
|
.setContentIntent(null)
|
||||||
.setDeleteIntent(pendingDeleteIntent)
|
.setDeleteIntent(pendingDeleteIntent)
|
||||||
|
|
||||||
notificationManager?.cancel(notificationId)
|
checkNotificationsPermission(this, PreferencesUtil.isKeyboardNotificationEntryEnable(this)) {
|
||||||
notificationManager?.notify(notificationId, builder.build())
|
notificationManager?.notify(notificationId, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
// Timeout only if notification clear is available
|
// Timeout only if notification clear is available
|
||||||
if (PreferencesUtil.isClearKeyboardNotificationEnable(this)) {
|
if (PreferencesUtil.isClearKeyboardNotificationEnable(this)) {
|
||||||
if (mNotificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
|
if (mNotificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
|
||||||
defineTimerJob(builder, mNotificationTimeoutMilliSecs) {
|
defineTimerJob(
|
||||||
|
builder,
|
||||||
|
NotificationServiceType.KEYBOARD,
|
||||||
|
mNotificationTimeoutMilliSecs
|
||||||
|
) {
|
||||||
stopNotificationAndSendLockIfNeeded()
|
stopNotificationAndSendLockIfNeeded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,31 @@
|
|||||||
package com.kunzisoft.keepass.services
|
package com.kunzisoft.keepass.services
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||||
|
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE
|
||||||
|
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.joda.time.Instant
|
||||||
|
|
||||||
|
|
||||||
abstract class NotificationService : Service() {
|
abstract class NotificationService : Service() {
|
||||||
@@ -20,6 +34,7 @@ abstract class NotificationService : Service() {
|
|||||||
private var colorNotificationAccent: Int = 0
|
private var colorNotificationAccent: Int = 0
|
||||||
|
|
||||||
protected var mTimerJob: Job? = null
|
protected var mTimerJob: Job? = null
|
||||||
|
private var mReset: Boolean = false
|
||||||
|
|
||||||
protected abstract val notificationId: Int
|
protected abstract val notificationId: Int
|
||||||
|
|
||||||
@@ -74,21 +89,55 @@ abstract class NotificationService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected fun startForegroundCompat(notificationId: Int,
|
||||||
|
builder: NotificationCompat.Builder,
|
||||||
|
type: NotificationServiceType
|
||||||
|
) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
val foregroundServiceTimer = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||||
|
} else {
|
||||||
|
FOREGROUND_SERVICE_TYPE_NONE
|
||||||
|
}
|
||||||
|
val foregroundType = when (type) {
|
||||||
|
NotificationServiceType.DATABASE_TASK -> FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||||
|
NotificationServiceType.ATTACHMENT -> FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||||
|
NotificationServiceType.CLIPBOARD -> foregroundServiceTimer
|
||||||
|
NotificationServiceType.KEYBOARD -> foregroundServiceTimer
|
||||||
|
NotificationServiceType.ADVANCED_UNLOCK -> foregroundServiceTimer
|
||||||
|
}
|
||||||
|
startForeground(notificationId, builder.build(), foregroundType)
|
||||||
|
} else {
|
||||||
|
startForeground(notificationId, builder.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected fun defineTimerJob(builder: NotificationCompat.Builder,
|
protected fun defineTimerJob(builder: NotificationCompat.Builder,
|
||||||
|
type: NotificationServiceType,
|
||||||
timeoutMilliseconds: Long,
|
timeoutMilliseconds: Long,
|
||||||
actionAfterASecond: (() -> Unit)? = null,
|
actionAfterASecond: (() -> Unit)? = null,
|
||||||
actionEnd: () -> Unit) {
|
actionEnd: () -> Unit) {
|
||||||
mTimerJob?.cancel()
|
mTimerJob?.cancel()
|
||||||
mTimerJob = CoroutineScope(Dispatchers.Main).launch {
|
mTimerJob = CoroutineScope(Dispatchers.Main).launch {
|
||||||
if (timeoutMilliseconds > 0) {
|
if (timeoutMilliseconds > 0) {
|
||||||
val timeoutInSeconds = timeoutMilliseconds / 1000L
|
var startInstant = Instant.now().millis
|
||||||
for (currentTime in timeoutInSeconds downTo 0) {
|
var currentTime = timeoutMilliseconds
|
||||||
|
while (currentTime >= 0) {
|
||||||
|
// Reset the timer if needed
|
||||||
|
if (mReset) {
|
||||||
|
mReset = false
|
||||||
|
startInstant = Instant.now().millis
|
||||||
|
currentTime = timeoutMilliseconds
|
||||||
|
}
|
||||||
|
// Update every second
|
||||||
actionAfterASecond?.invoke()
|
actionAfterASecond?.invoke()
|
||||||
builder.setProgress(100,
|
builder.setProgress(100,
|
||||||
(currentTime * 100 / timeoutInSeconds).toInt(),
|
(currentTime * 100 / timeoutMilliseconds).toInt(),
|
||||||
false)
|
false)
|
||||||
startForeground(notificationId, builder.build())
|
startForegroundCompat(notificationId, builder, type)
|
||||||
delay(1000)
|
delay(1000)
|
||||||
|
currentTime = timeoutMilliseconds - (Instant.now().millis - startInstant)
|
||||||
if (currentTime <= 0) {
|
if (currentTime <= 0) {
|
||||||
actionEnd()
|
actionEnd()
|
||||||
}
|
}
|
||||||
@@ -103,6 +152,10 @@ abstract class NotificationService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected fun resetTimeJob() {
|
||||||
|
mReset = true
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
mTimerJob?.cancel()
|
mTimerJob?.cancel()
|
||||||
mTimerJob = null
|
mTimerJob = null
|
||||||
@@ -114,5 +167,25 @@ abstract class NotificationService : Service() {
|
|||||||
companion object {
|
companion object {
|
||||||
private const val CHANNEL_ID = "com.kunzisoft.keepass.notification.channel"
|
private const val CHANNEL_ID = "com.kunzisoft.keepass.notification.channel"
|
||||||
private const val CHANNEL_NAME = "KeePassDX notification"
|
private const val CHANNEL_NAME = "KeePassDX notification"
|
||||||
|
|
||||||
|
fun checkNotificationsPermission(
|
||||||
|
context: Context,
|
||||||
|
showError: Boolean = true,
|
||||||
|
action: () -> Unit
|
||||||
|
) {
|
||||||
|
if (ContextCompat.checkSelfPermission(context,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
== PackageManager.PERMISSION_GRANTED) {
|
||||||
|
action.invoke()
|
||||||
|
} else {
|
||||||
|
if (showError) {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
R.string.warning_copy_permission,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.kunzisoft.keepass.services
|
||||||
|
|
||||||
|
enum class NotificationServiceType {
|
||||||
|
DATABASE_TASK,
|
||||||
|
ATTACHMENT,
|
||||||
|
CLIPBOARD,
|
||||||
|
KEYBOARD,
|
||||||
|
ADVANCED_UNLOCK
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@ import com.kunzisoft.keepass.activities.dialogs.ProFeatureDialogFragment
|
|||||||
import com.kunzisoft.keepass.activities.dialogs.UnavailableFeatureDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.UnavailableFeatureDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.stylish.Stylish
|
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
||||||
import com.kunzisoft.keepass.education.Education
|
import com.kunzisoft.keepass.education.Education
|
||||||
import com.kunzisoft.keepass.icons.IconPackChooser
|
import com.kunzisoft.keepass.icons.IconPackChooser
|
||||||
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||||
@@ -251,7 +251,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
val tempAdvancedUnlockPreference: TwoStatePreference? = findPreference(getString(R.string.temp_advanced_unlock_enable_key))
|
val tempAdvancedUnlockPreference: TwoStatePreference? = findPreference(getString(R.string.temp_advanced_unlock_enable_key))
|
||||||
|
|
||||||
val biometricUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
val biometricUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
AdvancedUnlockManager.biometricUnlockSupported(activity)
|
DeviceUnlockManager.biometricUnlockSupported(activity)
|
||||||
} else false
|
} else false
|
||||||
biometricUnlockEnablePreference?.apply {
|
biometricUnlockEnablePreference?.apply {
|
||||||
// False if under Marshmallow
|
// False if under Marshmallow
|
||||||
@@ -296,7 +296,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val deviceCredentialUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
val deviceCredentialUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
AdvancedUnlockManager.deviceCredentialUnlockSupported(activity)
|
DeviceUnlockManager.deviceCredentialUnlockSupported(activity)
|
||||||
} else false
|
} else false
|
||||||
deviceCredentialUnlockEnablePreference?.apply {
|
deviceCredentialUnlockEnablePreference?.apply {
|
||||||
// Biometric unlock already checked
|
// Biometric unlock already checked
|
||||||
@@ -395,7 +395,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
validate?.invoke()
|
validate?.invoke()
|
||||||
warningAlertDialog?.setOnDismissListener(null)
|
warningAlertDialog?.setOnDismissListener(null)
|
||||||
if (deleteKeys && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (deleteKeys && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
AdvancedUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity)
|
DeviceUnlockManager.deleteAllEntryKeysInKeystoreForBiometric(activity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(resources.getString(android.R.string.cancel)
|
.setNegativeButton(resources.getString(android.R.string.cancel)
|
||||||
@@ -483,13 +483,15 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
getString(R.string.setting_style_brightness_key),
|
getString(R.string.setting_style_brightness_key),
|
||||||
getString(R.string.setting_icon_pack_choose_key),
|
getString(R.string.setting_icon_pack_choose_key),
|
||||||
getString(R.string.show_entry_colors_key),
|
getString(R.string.show_entry_colors_key),
|
||||||
|
getString(R.string.hide_expired_entries_key),
|
||||||
|
getString(R.string.hide_templates_key),
|
||||||
getString(R.string.list_entries_show_username_key),
|
getString(R.string.list_entries_show_username_key),
|
||||||
getString(R.string.list_groups_show_number_entries_key),
|
getString(R.string.list_groups_show_number_entries_key),
|
||||||
|
getString(R.string.recursive_number_entries_key),
|
||||||
getString(R.string.show_otp_token_key),
|
getString(R.string.show_otp_token_key),
|
||||||
getString(R.string.show_uuid_key),
|
getString(R.string.show_uuid_key),
|
||||||
getString(R.string.list_size_key),
|
getString(R.string.list_size_key),
|
||||||
getString(R.string.monospace_font_fields_enable_key),
|
getString(R.string.monospace_font_fields_enable_key),
|
||||||
getString(R.string.hide_expired_entries_key),
|
|
||||||
getString(R.string.enable_education_screens_key),
|
getString(R.string.enable_education_screens_key),
|
||||||
getString(R.string.reset_education_screens_key) -> {
|
getString(R.string.reset_education_screens_key) -> {
|
||||||
DATABASE_PREFERENCE_CHANGED = true
|
DATABASE_PREFERENCE_CHANGED = true
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import androidx.preference.PreferenceManager
|
|||||||
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.Stylish
|
import com.kunzisoft.keepass.activities.stylish.Stylish
|
||||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
||||||
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
import com.kunzisoft.keepass.database.element.SortNodeEnum
|
||||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||||
import com.kunzisoft.keepass.education.Education
|
import com.kunzisoft.keepass.education.Education
|
||||||
@@ -120,6 +120,18 @@ object PreferencesUtil {
|
|||||||
context.resources.getBoolean(R.bool.show_entry_colors_default))
|
context.resources.getBoolean(R.bool.show_entry_colors_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showExpiredEntries(context: Context): Boolean {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
return ! prefs.getBoolean(context.getString(R.string.hide_expired_entries_key),
|
||||||
|
context.resources.getBoolean(R.bool.hide_expired_entries_default))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showTemplates(context: Context): Boolean {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
return ! prefs.getBoolean(context.getString(R.string.hide_templates_key),
|
||||||
|
context.resources.getBoolean(R.bool.hide_templates_default))
|
||||||
|
}
|
||||||
|
|
||||||
fun hideProtectedValue(context: Context): Boolean {
|
fun hideProtectedValue(context: Context): Boolean {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getBoolean(context.getString(R.string.hide_password_key),
|
return prefs.getBoolean(context.getString(R.string.hide_password_key),
|
||||||
@@ -144,6 +156,12 @@ object PreferencesUtil {
|
|||||||
context.resources.getBoolean(R.bool.list_groups_show_number_entries_default))
|
context.resources.getBoolean(R.bool.list_groups_show_number_entries_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun recursiveNumberEntries(context: Context): Boolean {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
return prefs.getBoolean(context.getString(R.string.recursive_number_entries_key),
|
||||||
|
context.resources.getBoolean(R.bool.recursive_number_entries_default))
|
||||||
|
}
|
||||||
|
|
||||||
fun showOTPToken(context: Context): Boolean {
|
fun showOTPToken(context: Context): Boolean {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getBoolean(context.getString(R.string.show_otp_token_key),
|
return prefs.getBoolean(context.getString(R.string.show_otp_token_key),
|
||||||
@@ -156,12 +174,6 @@ object PreferencesUtil {
|
|||||||
context.resources.getBoolean(R.bool.show_uuid_default))
|
context.resources.getBoolean(R.bool.show_uuid_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showExpiredEntries(context: Context): Boolean {
|
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
return ! prefs.getBoolean(context.getString(R.string.hide_expired_entries_key),
|
|
||||||
context.resources.getBoolean(R.bool.hide_expired_entries_default))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getStyle(context: Context): String {
|
fun getStyle(context: Context): String {
|
||||||
val defaultStyleString = Stylish.defaultStyle(context)
|
val defaultStyleString = Stylish.defaultStyle(context)
|
||||||
val styleString = PreferenceManager.getDefaultSharedPreferences(context)
|
val styleString = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
@@ -500,7 +512,7 @@ object PreferencesUtil {
|
|||||||
return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key),
|
return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key),
|
||||||
context.resources.getBoolean(R.bool.biometric_unlock_enable_default))
|
context.resources.getBoolean(R.bool.biometric_unlock_enable_default))
|
||||||
&& (if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
&& (if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
||||||
AdvancedUnlockManager.biometricUnlockSupported(context)
|
DeviceUnlockManager.biometricUnlockSupported(context)
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
})
|
})
|
||||||
@@ -841,15 +853,17 @@ object PreferencesUtil {
|
|||||||
context.getString(R.string.setting_style_brightness_key) -> editor.putString(name, value)
|
context.getString(R.string.setting_style_brightness_key) -> editor.putString(name, value)
|
||||||
context.getString(R.string.setting_icon_pack_choose_key) -> editor.putString(name, value)
|
context.getString(R.string.setting_icon_pack_choose_key) -> editor.putString(name, value)
|
||||||
context.getString(R.string.show_entry_colors_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.show_entry_colors_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.hide_expired_entries_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.hide_templates_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.hide_password_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.hide_password_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.colorize_password_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.colorize_password_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.list_entries_show_username_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.list_entries_show_username_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.list_groups_show_number_entries_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.list_groups_show_number_entries_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
context.getString(R.string.recursive_number_entries_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.show_otp_token_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.show_otp_token_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.show_uuid_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.show_uuid_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.list_size_key) -> editor.putString(name, value)
|
context.getString(R.string.list_size_key) -> editor.putString(name, value)
|
||||||
context.getString(R.string.monospace_font_fields_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.monospace_font_fields_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.hide_expired_entries_key) -> editor.putBoolean(name, value.toBoolean())
|
|
||||||
context.getString(R.string.enable_education_screens_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.enable_education_screens_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
|
|
||||||
context.getString(R.string.password_generator_length_key) -> editor.putInt(name, value.toInt())
|
context.getString(R.string.password_generator_length_key) -> editor.putInt(name, value.toInt())
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import com.kunzisoft.keepass.model.SearchInfo
|
|||||||
class AutofillBlocklistWebDomainPreferenceDialogFragmentCompat
|
class AutofillBlocklistWebDomainPreferenceDialogFragmentCompat
|
||||||
: AutofillBlocklistPreferenceDialogFragmentCompat() {
|
: AutofillBlocklistPreferenceDialogFragmentCompat() {
|
||||||
|
|
||||||
override fun buildSearchInfoFromString(searchInfoString: String): SearchInfo? {
|
override fun buildSearchInfoFromString(searchInfoString: String): SearchInfo {
|
||||||
val newSearchInfo = searchInfoString
|
val newSearchInfo = searchInfoString
|
||||||
// remove prefix https://
|
// remove prefix https://
|
||||||
.replace(Regex("^.*://"), "")
|
.replace(Regex("^.*://"), "")
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import android.content.Intent
|
|||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||||
@@ -45,9 +46,9 @@ const val LOCK_ACTION = "com.kunzisoft.keepass.LOCK"
|
|||||||
const val REMOVE_ENTRY_MAGIKEYBOARD_ACTION = "com.kunzisoft.keepass.REMOVE_ENTRY_MAGIKEYBOARD"
|
const val REMOVE_ENTRY_MAGIKEYBOARD_ACTION = "com.kunzisoft.keepass.REMOVE_ENTRY_MAGIKEYBOARD"
|
||||||
const val BACK_PREVIOUS_KEYBOARD_ACTION = "com.kunzisoft.keepass.BACK_PREVIOUS_KEYBOARD"
|
const val BACK_PREVIOUS_KEYBOARD_ACTION = "com.kunzisoft.keepass.BACK_PREVIOUS_KEYBOARD"
|
||||||
|
|
||||||
class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
|
class LockReceiver(private var lockAction: () -> Unit) : BroadcastReceiver() {
|
||||||
|
|
||||||
var mLockPendingIntent: PendingIntent? = null
|
private var mLockPendingIntent: PendingIntent? = null
|
||||||
var backToPreviousKeyboardAction: (() -> Unit)? = null
|
var backToPreviousKeyboardAction: (() -> Unit)? = null
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
@@ -60,7 +61,7 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
Intent.ACTION_SCREEN_OFF -> {
|
Intent.ACTION_SCREEN_OFF -> {
|
||||||
if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(context)) {
|
if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(context)) {
|
||||||
mLockPendingIntent = PendingIntent.getBroadcast(context,
|
val lockPendingIntent = PendingIntent.getBroadcast(context,
|
||||||
4575,
|
4575,
|
||||||
Intent(intent).apply {
|
Intent(intent).apply {
|
||||||
action = LOCK_ACTION
|
action = LOCK_ACTION
|
||||||
@@ -71,6 +72,7 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
|
|||||||
0
|
0
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
this.mLockPendingIntent = lockPendingIntent
|
||||||
// Launch the effective action after a small time
|
// Launch the effective action after a small time
|
||||||
val first: Long = System.currentTimeMillis() + context.getString(R.string.timeout_screen_off).toLong()
|
val first: Long = System.currentTimeMillis() + context.getString(R.string.timeout_screen_off).toLong()
|
||||||
(context.getSystemService(ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
|
(context.getSystemService(ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
|
||||||
@@ -80,20 +82,20 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
|
|||||||
alarmManager.set(
|
alarmManager.set(
|
||||||
AlarmManager.RTC_WAKEUP,
|
AlarmManager.RTC_WAKEUP,
|
||||||
first,
|
first,
|
||||||
mLockPendingIntent
|
lockPendingIntent
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
alarmManager.setExact(
|
alarmManager.setExact(
|
||||||
AlarmManager.RTC_WAKEUP,
|
AlarmManager.RTC_WAKEUP,
|
||||||
first,
|
first,
|
||||||
mLockPendingIntent
|
lockPendingIntent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
alarmManager.set(
|
alarmManager.set(
|
||||||
AlarmManager.RTC_WAKEUP,
|
AlarmManager.RTC_WAKEUP,
|
||||||
first,
|
first,
|
||||||
mLockPendingIntent
|
lockPendingIntent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,9 +122,9 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelLockPendingIntent(context: Context) {
|
private fun cancelLockPendingIntent(context: Context) {
|
||||||
mLockPendingIntent?.let {
|
mLockPendingIntent?.let { lockPendingIntent ->
|
||||||
val alarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager?
|
val alarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager?
|
||||||
alarmManager?.cancel(mLockPendingIntent)
|
alarmManager?.cancel(lockPendingIntent)
|
||||||
mLockPendingIntent = null
|
mLockPendingIntent = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,7 +133,7 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
|
|||||||
fun Context.registerLockReceiver(lockReceiver: LockReceiver?,
|
fun Context.registerLockReceiver(lockReceiver: LockReceiver?,
|
||||||
registerKeyboardAction: Boolean = false) {
|
registerKeyboardAction: Boolean = false) {
|
||||||
lockReceiver?.let {
|
lockReceiver?.let {
|
||||||
registerReceiver(it, IntentFilter().apply {
|
ContextCompat.registerReceiver(this, it, IntentFilter().apply {
|
||||||
addAction(Intent.ACTION_SCREEN_OFF)
|
addAction(Intent.ACTION_SCREEN_OFF)
|
||||||
addAction(Intent.ACTION_SCREEN_ON)
|
addAction(Intent.ACTION_SCREEN_ON)
|
||||||
addAction(LOCK_ACTION)
|
addAction(LOCK_ACTION)
|
||||||
@@ -139,7 +141,7 @@ fun Context.registerLockReceiver(lockReceiver: LockReceiver?,
|
|||||||
addAction(REMOVE_ENTRY_MAGIKEYBOARD_ACTION)
|
addAction(REMOVE_ENTRY_MAGIKEYBOARD_ACTION)
|
||||||
addAction(BACK_PREVIOUS_KEYBOARD_ACTION)
|
addAction(BACK_PREVIOUS_KEYBOARD_ACTION)
|
||||||
}
|
}
|
||||||
})
|
}, ContextCompat.RECEIVER_EXPORTED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,17 @@ package com.kunzisoft.keepass.utils
|
|||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import androidx.core.os.ConfigurationCompat
|
import androidx.core.os.ConfigurationCompat
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
|
import com.kunzisoft.keepass.model.DataDate
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
|
import java.util.Calendar
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
object TimeUtil {
|
object TimeUtil {
|
||||||
|
|
||||||
fun DateInstant.getDateTimeString(resources: Resources): String {
|
fun DateInstant.getDateTimeString(resources: Resources): String {
|
||||||
val locale = ConfigurationCompat.getLocales(resources.configuration)[0] ?: Locale.ROOT
|
val locale = ConfigurationCompat.getLocales(resources.configuration)[0] ?: Locale.ROOT
|
||||||
|
val date = instant.toDate()
|
||||||
return when (type) {
|
return when (type) {
|
||||||
DateInstant.Type.DATE -> DateFormat.getDateInstance(
|
DateInstant.Type.DATE -> DateFormat.getDateInstance(
|
||||||
DateFormat.MEDIUM,
|
DateFormat.MEDIUM,
|
||||||
@@ -26,4 +30,22 @@ object TimeUtil {
|
|||||||
.format(date)
|
.format(date)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://github.com/material-components/material-components-android/issues/882#issuecomment-1111374962
|
||||||
|
// To fix UTC time in date picker
|
||||||
|
fun datePickerToDataDate(millis: Long): DataDate {
|
||||||
|
val selectedUtc = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
|
||||||
|
selectedUtc.timeInMillis = millis
|
||||||
|
val selectedLocal = Calendar.getInstance()
|
||||||
|
selectedLocal.clear()
|
||||||
|
selectedLocal.set(
|
||||||
|
selectedUtc.get(Calendar.YEAR),
|
||||||
|
selectedUtc.get(Calendar.MONTH),
|
||||||
|
selectedUtc.get(Calendar.DAY_OF_MONTH))
|
||||||
|
return DataDate(
|
||||||
|
selectedLocal.get(Calendar.YEAR),
|
||||||
|
selectedLocal.get(Calendar.MONTH) + 1,
|
||||||
|
selectedLocal.get(Calendar.DAY_OF_MONTH),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -68,6 +68,7 @@ class AddNodeButtonView @JvmOverloads constructor(context: Context,
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
inflate(context)
|
inflate(context)
|
||||||
|
hideButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun inflate(context: Context) {
|
private fun inflate(context: Context) {
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.view
|
|
||||||
|
|
||||||
data class DataTime(val hours: Int, val minutes: Int)
|
|
||||||
@@ -111,7 +111,7 @@ class DateTimeEditFieldView @JvmOverloads constructor(context: Context,
|
|||||||
mDefault
|
mDefault
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
mDateTime = DateInstant(value.date, mDateTime.type)
|
mDateTime = DateInstant(value.instant, mDateTime.type)
|
||||||
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
|
entryExpiresTextView.text = if (entryExpiresCheckBox.isChecked) {
|
||||||
mDateTime.getDateTimeString(resources)
|
mDateTime.getDateTimeString(resources)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -57,8 +57,6 @@ class DateTimeFieldView @JvmOverloads constructor(context: Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun assignExpiresDateText() {
|
private fun assignExpiresDateText() {
|
||||||
val isExpires = mDateTime.isCurrentlyExpire()
|
|
||||||
|
|
||||||
// Show or not the warning icon
|
// Show or not the warning icon
|
||||||
expiresImage.isVisible = if (mActivated) {
|
expiresImage.isVisible = if (mActivated) {
|
||||||
isExpires
|
isExpires
|
||||||
@@ -100,6 +98,13 @@ class DateTimeFieldView @JvmOverloads constructor(context: Context,
|
|||||||
mDateTime.type = value
|
mDateTime.type = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isExpirable: Boolean = false
|
||||||
|
|
||||||
|
val isExpires: Boolean
|
||||||
|
get() {
|
||||||
|
return isExpirable && mDateTime.isCurrentlyExpire()
|
||||||
|
}
|
||||||
|
|
||||||
override var activation: Boolean
|
override var activation: Boolean
|
||||||
get() {
|
get() {
|
||||||
return mActivated
|
return mActivated
|
||||||
@@ -128,7 +133,7 @@ class DateTimeFieldView @JvmOverloads constructor(context: Context,
|
|||||||
mDefault
|
mDefault
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
mDateTime = DateInstant(value.date, mDateTime.type)
|
mDateTime = DateInstant(value.instant, mDateTime.type)
|
||||||
assignExpiresDateText()
|
assignExpiresDateText()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,15 +25,14 @@ import android.util.AttributeSet
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
|
class DeviceUnlockView @JvmOverloads constructor(context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyle: Int = 0)
|
defStyle: Int = 0)
|
||||||
: LinearLayout(context, attrs, defStyle) {
|
: LinearLayout(context, attrs, defStyle) {
|
||||||
|
|
||||||
private var biometricButtonView: Button? = null
|
private var biometricButtonView: Button? = null
|
||||||
@@ -45,7 +44,7 @@ class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
|
|||||||
biometricButtonView = findViewById(R.id.biometric_button)
|
biometricButtonView = findViewById(R.id.biometric_button)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setIconViewClickListener(listener: OnClickListener?) {
|
fun setDeviceUnlockButtonViewClickListener(listener: OnClickListener?) {
|
||||||
biometricButtonView?.setOnClickListener(listener)
|
biometricButtonView?.setOnClickListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,14 +59,4 @@ class AdvancedUnlockInfoView @JvmOverloads constructor(context: Context,
|
|||||||
fun setTitle(@StringRes textId: Int) {
|
fun setTitle(@StringRes textId: Int) {
|
||||||
title = context.getString(textId)
|
title = context.getString(textId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setMessage(text: CharSequence) {
|
|
||||||
if (text.isNotEmpty())
|
|
||||||
Toast.makeText(context, text, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setMessage(@StringRes textId: Int) {
|
|
||||||
Toast.makeText(context, textId, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -53,9 +53,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
|||||||
private var checkboxHardwareView: CompoundButton
|
private var checkboxHardwareView: CompoundButton
|
||||||
private var hardwareKeySelectionView: HardwareKeySelectionView
|
private var hardwareKeySelectionView: HardwareKeySelectionView
|
||||||
|
|
||||||
var onPasswordChecked: (CompoundButton.OnCheckedChangeListener)? = null
|
var onConditionToStoreCredentialChanged: ((CredentialStorage, verified: Boolean) -> Unit)? = null
|
||||||
var onKeyFileChecked: (CompoundButton.OnCheckedChangeListener)? = null
|
|
||||||
var onHardwareKeyChecked: (CompoundButton.OnCheckedChangeListener)? = null
|
|
||||||
var onValidateListener: (() -> Unit)? = null
|
var onValidateListener: (() -> Unit)? = null
|
||||||
|
|
||||||
private var mCredentialStorage: CredentialStorage = CredentialStorage.PASSWORD
|
private var mCredentialStorage: CredentialStorage = CredentialStorage.PASSWORD
|
||||||
@@ -103,24 +101,33 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
|||||||
handled
|
handled
|
||||||
}
|
}
|
||||||
|
|
||||||
checkboxPasswordView.setOnCheckedChangeListener { view, checked ->
|
checkboxPasswordView.setOnCheckedChangeListener { _, _ ->
|
||||||
onPasswordChecked?.onCheckedChanged(view, checked)
|
onConditionToStoreCredentialChanged?.invoke(
|
||||||
|
mCredentialStorage,
|
||||||
|
conditionToStoreCredential()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
checkboxKeyFileView.setOnCheckedChangeListener { view, checked ->
|
checkboxKeyFileView.setOnCheckedChangeListener { _, checked ->
|
||||||
if (checked) {
|
if (checked) {
|
||||||
if (keyFileSelectionView.uri == null) {
|
if (keyFileSelectionView.uri == null) {
|
||||||
checkboxKeyFileView.isChecked = false
|
checkboxKeyFileView.isChecked = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onKeyFileChecked?.onCheckedChanged(view, checked)
|
onConditionToStoreCredentialChanged?.invoke(
|
||||||
|
mCredentialStorage,
|
||||||
|
conditionToStoreCredential()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
checkboxHardwareView.setOnCheckedChangeListener { view, checked ->
|
checkboxHardwareView.setOnCheckedChangeListener { _, checked ->
|
||||||
if (checked) {
|
if (checked) {
|
||||||
if (hardwareKeySelectionView.hardwareKey == null) {
|
if (hardwareKeySelectionView.hardwareKey == null) {
|
||||||
checkboxHardwareView.isChecked = false
|
checkboxHardwareView.isChecked = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onHardwareKeyChecked?.onCheckedChanged(view, checked)
|
onConditionToStoreCredentialChanged?.invoke(
|
||||||
|
mCredentialStorage,
|
||||||
|
conditionToStoreCredential()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
hardwareKeySelectionView.selectionListener = { _ ->
|
hardwareKeySelectionView.selectionListener = { _ ->
|
||||||
|
|||||||
@@ -22,28 +22,30 @@ package com.kunzisoft.keepass.view
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
|
import android.text.Spannable
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.widget.EditText
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
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.password.PasswordGenerator
|
|
||||||
import com.kunzisoft.keepass.password.PasswordEntropy
|
import com.kunzisoft.keepass.password.PasswordEntropy
|
||||||
|
import com.kunzisoft.keepass.password.PasswordGenerator
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|
||||||
class PassKeyView @JvmOverloads constructor(context: Context,
|
class PasswordEditView @JvmOverloads constructor(context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyle: Int = 0)
|
defStyle: Int = 0)
|
||||||
: FrameLayout(context, attrs, defStyle) {
|
: FrameLayout(context, attrs, defStyle) {
|
||||||
|
|
||||||
private var mPasswordEntropyCalculator: PasswordEntropy? = null
|
private var mPasswordEntropyCalculator: PasswordEntropy? = null
|
||||||
|
|
||||||
private val passwordInputLayout: TextInputLayout
|
private val passwordInputLayout: TextInputLayout
|
||||||
private val passwordText: TextView
|
private val passwordText: EditText
|
||||||
private val passwordStrengthProgress: LinearProgressIndicator
|
private val passwordStrengthProgress: LinearProgressIndicator
|
||||||
private val passwordEntropy: TextView
|
private val passwordEntropy: TextView
|
||||||
|
|
||||||
@@ -51,38 +53,19 @@ class PassKeyView @JvmOverloads constructor(context: Context,
|
|||||||
private var mMaxLines: Int = 3
|
private var mMaxLines: Int = 3
|
||||||
private var mShowPassword: Boolean = false
|
private var mShowPassword: Boolean = false
|
||||||
|
|
||||||
private var mPasswordTextWatcher: MutableList<TextWatcher> = mutableListOf()
|
private var mPasswordTextWatchers: MutableList<TextWatcher> = mutableListOf()
|
||||||
private val passwordTextWatcher = object : TextWatcher {
|
private var mPasswordTextWatcher: TextWatcher? = null
|
||||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
|
|
||||||
mPasswordTextWatcher.forEach {
|
|
||||||
it.beforeTextChanged(charSequence, i, i1, i2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
|
|
||||||
mPasswordTextWatcher.forEach {
|
|
||||||
it.onTextChanged(charSequence, i, i1, i2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun afterTextChanged(editable: Editable) {
|
|
||||||
mPasswordTextWatcher.forEach {
|
|
||||||
it.afterTextChanged(editable)
|
|
||||||
}
|
|
||||||
getEntropyStrength(editable.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
context.theme.obtainStyledAttributes(
|
context.theme.obtainStyledAttributes(
|
||||||
attrs,
|
attrs,
|
||||||
R.styleable.PassKeyView,
|
R.styleable.PasswordView,
|
||||||
0, 0).apply {
|
0, 0).apply {
|
||||||
try {
|
try {
|
||||||
mViewHint = getString(R.styleable.PassKeyView_passKeyHint)
|
mViewHint = getString(R.styleable.PasswordView_passwordHint)
|
||||||
?: context.getString(R.string.password)
|
?: context.getString(R.string.password)
|
||||||
mMaxLines = getInteger(R.styleable.PassKeyView_passKeyMaxLines, mMaxLines)
|
mMaxLines = getInteger(R.styleable.PasswordView_passwordMaxLines, mMaxLines)
|
||||||
mShowPassword = getBoolean(R.styleable.PassKeyView_passKeyVisible,
|
mShowPassword = getBoolean(R.styleable.PasswordView_passwordVisible,
|
||||||
!PreferencesUtil.hideProtectedValue(context))
|
!PreferencesUtil.hideProtectedValue(context))
|
||||||
} finally {
|
} finally {
|
||||||
recycle()
|
recycle()
|
||||||
@@ -90,31 +73,53 @@ class PassKeyView @JvmOverloads constructor(context: Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||||
inflater?.inflate(R.layout.view_passkey, this)
|
inflater?.inflate(R.layout.view_password_edit, this)
|
||||||
|
|
||||||
passwordInputLayout = findViewById(R.id.password_input_layout)
|
passwordInputLayout = findViewById(R.id.password_edit_input_layout)
|
||||||
passwordInputLayout?.hint = mViewHint
|
passwordInputLayout?.hint = mViewHint
|
||||||
passwordText = findViewById(R.id.password_text)
|
passwordText = findViewById(R.id.password_edit_text)
|
||||||
if (mShowPassword) {
|
if (mShowPassword) {
|
||||||
passwordText?.inputType = passwordText.inputType or
|
passwordText?.inputType = passwordText.inputType or
|
||||||
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
||||||
}
|
}
|
||||||
passwordText?.maxLines = mMaxLines
|
passwordText?.maxLines = mMaxLines
|
||||||
passwordText?.applyFontVisibility()
|
passwordText?.applyFontVisibility()
|
||||||
passwordText.addTextChangedListener(passwordTextWatcher)
|
passwordStrengthProgress = findViewById(R.id.password_edit_strength_progress)
|
||||||
passwordStrengthProgress = findViewById(R.id.password_strength_progress)
|
|
||||||
passwordStrengthProgress?.apply {
|
passwordStrengthProgress?.apply {
|
||||||
setIndicatorColor(PasswordEntropy.Strength.RISKY.color)
|
setIndicatorColor(PasswordEntropy.Strength.RISKY.color)
|
||||||
progress = 0
|
progress = 0
|
||||||
max = 100
|
max = 100
|
||||||
}
|
}
|
||||||
passwordEntropy = findViewById(R.id.password_entropy)
|
passwordEntropy = findViewById(R.id.password_edit_entropy)
|
||||||
|
|
||||||
mPasswordEntropyCalculator = PasswordEntropy {
|
mPasswordEntropyCalculator = PasswordEntropy {
|
||||||
passwordText?.text?.toString()?.let { firstPassword ->
|
passwordText?.text?.toString()?.let { firstPassword ->
|
||||||
getEntropyStrength(firstPassword)
|
getEntropyStrength(firstPassword)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mPasswordTextWatcher = object : TextWatcher {
|
||||||
|
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
|
||||||
|
mPasswordTextWatchers.forEach {
|
||||||
|
it.beforeTextChanged(charSequence, i, i1, i2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
|
||||||
|
mPasswordTextWatchers.forEach {
|
||||||
|
it.onTextChanged(charSequence, i, i1, i2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun afterTextChanged(editable: Editable) {
|
||||||
|
mPasswordTextWatchers.forEach {
|
||||||
|
it.afterTextChanged(editable)
|
||||||
|
}
|
||||||
|
getEntropyStrength(editable.toString())
|
||||||
|
PasswordGenerator.colorizedPassword(editable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
passwordText?.addTextChangedListener(mPasswordTextWatcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getEntropyStrength(passwordText: String) {
|
private fun getEntropyStrength(passwordText: String) {
|
||||||
@@ -134,11 +139,18 @@ class PassKeyView @JvmOverloads constructor(context: Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addTextChangedListener(textWatcher: TextWatcher) {
|
fun addTextChangedListener(textWatcher: TextWatcher) {
|
||||||
mPasswordTextWatcher.add(textWatcher)
|
mPasswordTextWatchers.add(textWatcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeTextChangedListener(textWatcher: TextWatcher) {
|
fun removeTextChangedListener(textWatcher: TextWatcher) {
|
||||||
mPasswordTextWatcher.remove(textWatcher)
|
mPasswordTextWatchers.remove(textWatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun spannableValue(value: String): Spannable {
|
||||||
|
return if (PreferencesUtil.colorizePassword(context))
|
||||||
|
PasswordGenerator.getColorizedPassword(value)
|
||||||
|
else
|
||||||
|
SpannableString(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var passwordString: String
|
var passwordString: String
|
||||||
@@ -146,11 +158,6 @@ class PassKeyView @JvmOverloads constructor(context: Context,
|
|||||||
return passwordText.text.toString()
|
return passwordText.text.toString()
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
val spannableString =
|
passwordText.setText(spannableValue(value))
|
||||||
if (PreferencesUtil.colorizePassword(context))
|
|
||||||
PasswordGenerator.getColorizedPassword(value)
|
|
||||||
else
|
|
||||||
SpannableString(value)
|
|
||||||
passwordText.text = spannableString
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 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.view
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.text.Spannable
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.setPadding
|
||||||
|
import androidx.core.widget.TextViewCompat
|
||||||
|
import androidx.core.widget.doAfterTextChanged
|
||||||
|
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.password.PasswordEntropy
|
||||||
|
import com.kunzisoft.keepass.password.PasswordGenerator
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordTextEditFieldView @JvmOverloads constructor(context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyle: Int = 0)
|
||||||
|
: TextEditFieldView(context, attrs, defStyle) {
|
||||||
|
|
||||||
|
private var mPasswordEntropyCalculator: PasswordEntropy = PasswordEntropy {
|
||||||
|
valueView.text?.toString()?.let { firstPassword ->
|
||||||
|
getEntropyStrength(firstPassword)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var isColorizedPasswordActivated = PreferencesUtil.colorizePassword(context)
|
||||||
|
|
||||||
|
private var passwordProgressViewId = ViewCompat.generateViewId()
|
||||||
|
private var passwordEntropyViewId = ViewCompat.generateViewId()
|
||||||
|
|
||||||
|
private var mPasswordProgress = LinearProgressIndicator(context).apply {
|
||||||
|
layoutParams = LayoutParams(
|
||||||
|
LayoutParams.MATCH_PARENT,
|
||||||
|
LayoutParams.WRAP_CONTENT
|
||||||
|
).apply {
|
||||||
|
addRule(ALIGN_PARENT_BOTTOM)
|
||||||
|
}
|
||||||
|
setPadding(
|
||||||
|
TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
1f,
|
||||||
|
context.resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
)
|
||||||
|
setIndicatorColor(PasswordEntropy.Strength.RISKY.color)
|
||||||
|
progress = 0
|
||||||
|
max = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
private val mPasswordEntropyView = TextView(context).apply {
|
||||||
|
layoutParams = LayoutParams(
|
||||||
|
LayoutParams.WRAP_CONTENT,
|
||||||
|
LayoutParams.WRAP_CONTENT
|
||||||
|
).apply {
|
||||||
|
addRule(ALIGN_PARENT_BOTTOM)
|
||||||
|
}
|
||||||
|
setPadding(
|
||||||
|
TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
4f,
|
||||||
|
context.resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
)
|
||||||
|
TextViewCompat.setTextAppearance(this, R.style.KeepassDXStyle_Text_Indicator)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
buildViews()
|
||||||
|
|
||||||
|
valueView.doAfterTextChanged { editable ->
|
||||||
|
getEntropyStrength(editable.toString())
|
||||||
|
PasswordGenerator.colorizedPassword(editable)
|
||||||
|
}
|
||||||
|
|
||||||
|
addView(mPasswordProgress)
|
||||||
|
addView(mPasswordEntropyView)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildViews() {
|
||||||
|
mPasswordProgress.apply {
|
||||||
|
id = passwordProgressViewId
|
||||||
|
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||||
|
it.addRule(LEFT_OF, actionImageButtonId)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
|
it.addRule(START_OF, actionImageButtonId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mPasswordEntropyView.apply {
|
||||||
|
id = passwordEntropyViewId
|
||||||
|
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||||
|
it.addRule(ALIGN_RIGHT, passwordProgressViewId)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
|
it.addRule(ALIGN_END, passwordProgressViewId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getEntropyStrength(passwordText: String) {
|
||||||
|
mPasswordEntropyCalculator.getEntropyStrength(passwordText) { entropyStrength ->
|
||||||
|
mPasswordProgress.apply {
|
||||||
|
post {
|
||||||
|
setIndicatorColor(entropyStrength.strength.color)
|
||||||
|
setProgressCompat(entropyStrength.estimationPercent, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mPasswordEntropyView.apply {
|
||||||
|
post {
|
||||||
|
text = PasswordEntropy.getStringEntropy(resources, entropyStrength.entropy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun spannableValue(value: String?): Spannable? {
|
||||||
|
if (value == null)
|
||||||
|
return null
|
||||||
|
return if (isColorizedPasswordActivated)
|
||||||
|
PasswordGenerator.getColorizedPassword(value)
|
||||||
|
else
|
||||||
|
super.spannableValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override var label: String
|
||||||
|
get() {
|
||||||
|
return super.label
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
super.label = value
|
||||||
|
// Define views Ids with label value
|
||||||
|
passwordProgressViewId = "passwordProgressViewId $value".hashCode()
|
||||||
|
passwordEntropyViewId = "passwordEntropyViewId $value".hashCode()
|
||||||
|
buildViews()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 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.view
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.text.SpannableString
|
||||||
|
import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
import android.text.style.ImageSpan
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.password.PasswordEntropy
|
||||||
|
import com.kunzisoft.keepass.password.PasswordGenerator
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordTextFieldView @JvmOverloads constructor(context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyle: Int = 0)
|
||||||
|
: TextFieldView(context, attrs, defStyle) {
|
||||||
|
|
||||||
|
private var mPasswordEntropyCalculator: PasswordEntropy = PasswordEntropy {
|
||||||
|
valueView.text?.toString()?.let { firstPassword ->
|
||||||
|
getEntropyStrength(firstPassword)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var indicatorDrawable = ContextCompat.getDrawable(
|
||||||
|
context,
|
||||||
|
R.drawable.ic_shield_white_24dp
|
||||||
|
)?.apply {
|
||||||
|
val lineHeight = labelView.lineHeight
|
||||||
|
setBounds(0,0,lineHeight, lineHeight)
|
||||||
|
DrawableCompat.setTint(this, Color.TRANSPARENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
override var label: String
|
||||||
|
get() {
|
||||||
|
return labelView.text.toString().removeSuffix(ICON_STRING_SPACES)
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
indicatorDrawable?.let { drawable ->
|
||||||
|
val spannableString = SpannableString("$value$ICON_STRING_SPACES")
|
||||||
|
val startPosition = spannableString.split(ICON_STRING)[0].length
|
||||||
|
val endPosition = startPosition + ICON_STRING.length
|
||||||
|
spannableString
|
||||||
|
.setSpan(
|
||||||
|
ImageSpan(drawable),
|
||||||
|
startPosition,
|
||||||
|
endPosition,
|
||||||
|
SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
)
|
||||||
|
labelView.text = spannableString
|
||||||
|
} ?: kotlin.run {
|
||||||
|
labelView.text = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setLabel(@StringRes labelId: Int) {
|
||||||
|
label = resources.getString(labelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override var value: String
|
||||||
|
get() {
|
||||||
|
return valueView.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
val spannableString =
|
||||||
|
if (PreferencesUtil.colorizePassword(context))
|
||||||
|
PasswordGenerator.getColorizedPassword(value)
|
||||||
|
else
|
||||||
|
SpannableString(value)
|
||||||
|
valueView.text = spannableString
|
||||||
|
changeProtectedValueParameters()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setValue(@StringRes valueId: Int) {
|
||||||
|
value = resources.getString(valueId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getEntropyStrength(passwordText: String) {
|
||||||
|
mPasswordEntropyCalculator.getEntropyStrength(passwordText) { entropyStrength ->
|
||||||
|
labelView.apply {
|
||||||
|
post {
|
||||||
|
val strengthColor = entropyStrength.strength.color
|
||||||
|
indicatorDrawable?.let { drawable ->
|
||||||
|
DrawableCompat.setTint(drawable, strengthColor)
|
||||||
|
}
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ICON_STRING = "[icon]"
|
||||||
|
private const val ICON_STRING_SPACES = " $ICON_STRING"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
private var searchUsername: CompoundButton
|
private var searchUsername: CompoundButton
|
||||||
private var searchPassword: CompoundButton
|
private var searchPassword: CompoundButton
|
||||||
private var searchURL: CompoundButton
|
private var searchURL: CompoundButton
|
||||||
|
private var searchByURLDomain: Boolean = false
|
||||||
private var searchExpired: CompoundButton
|
private var searchExpired: CompoundButton
|
||||||
private var searchNotes: CompoundButton
|
private var searchNotes: CompoundButton
|
||||||
private var searchOther: CompoundButton
|
private var searchOther: CompoundButton
|
||||||
@@ -50,6 +51,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
this.searchInUsernames = searchUsername.isChecked
|
this.searchInUsernames = searchUsername.isChecked
|
||||||
this.searchInPasswords = searchPassword.isChecked
|
this.searchInPasswords = searchPassword.isChecked
|
||||||
this.searchInUrls = searchURL.isChecked
|
this.searchInUrls = searchURL.isChecked
|
||||||
|
this.searchByDomain = searchByURLDomain
|
||||||
this.searchInExpired = searchExpired.isChecked
|
this.searchInExpired = searchExpired.isChecked
|
||||||
this.searchInNotes = searchNotes.isChecked
|
this.searchInNotes = searchNotes.isChecked
|
||||||
this.searchInOther = searchOther.isChecked
|
this.searchInOther = searchOther.isChecked
|
||||||
@@ -70,6 +72,7 @@ class SearchFiltersView @JvmOverloads constructor(context: Context,
|
|||||||
searchUsername.isChecked = value.searchInUsernames
|
searchUsername.isChecked = value.searchInUsernames
|
||||||
searchPassword.isChecked = value.searchInPasswords
|
searchPassword.isChecked = value.searchInPasswords
|
||||||
searchURL.isChecked = value.searchInUrls
|
searchURL.isChecked = value.searchInUrls
|
||||||
|
searchByURLDomain = value.searchByDomain
|
||||||
searchExpired.isChecked = value.searchInExpired
|
searchExpired.isChecked = value.searchInExpired
|
||||||
searchNotes.isChecked = value.searchInNotes
|
searchNotes.isChecked = value.searchInNotes
|
||||||
searchOther.isChecked = value.searchInOther
|
searchOther.isChecked = value.searchInOther
|
||||||
|
|||||||
@@ -18,8 +18,14 @@ import com.kunzisoft.keepass.database.element.DateInstant
|
|||||||
import com.kunzisoft.keepass.database.element.Field
|
import com.kunzisoft.keepass.database.element.Field
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.database.element.template.*
|
import com.kunzisoft.keepass.database.element.template.Template
|
||||||
|
import com.kunzisoft.keepass.database.element.template.TemplateAttribute
|
||||||
|
import com.kunzisoft.keepass.database.element.template.TemplateAttributeAction
|
||||||
|
import com.kunzisoft.keepass.database.element.template.TemplateAttributeOption
|
||||||
|
import com.kunzisoft.keepass.database.element.template.TemplateAttributeType
|
||||||
|
import com.kunzisoft.keepass.database.element.template.TemplateEngine
|
||||||
import com.kunzisoft.keepass.database.element.template.TemplateEngine.Companion.addTemplateDecorator
|
import com.kunzisoft.keepass.database.element.template.TemplateEngine.Companion.addTemplateDecorator
|
||||||
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
@@ -608,9 +614,8 @@ abstract class TemplateAbstractView<
|
|||||||
getViewFieldByName(oldField.name)?.view?.let { viewToReplace ->
|
getViewFieldByName(oldField.name)?.view?.let { viewToReplace ->
|
||||||
val oldValue = getCustomField(oldField.name).protectedValue.toString()
|
val oldValue = getCustomField(oldField.name).protectedValue.toString()
|
||||||
|
|
||||||
val parentGroup = viewToReplace.parent as ViewGroup
|
val parentGroup = viewToReplace.parent as? ViewGroup?
|
||||||
val indexInParent = parentGroup.indexOfChild(viewToReplace)
|
parentGroup?.removeView(viewToReplace)
|
||||||
parentGroup.removeView(viewToReplace)
|
|
||||||
|
|
||||||
val newCustomFieldWithValue = if (keepOldValue)
|
val newCustomFieldWithValue = if (keepOldValue)
|
||||||
Field(newField.name,
|
Field(newField.name,
|
||||||
@@ -624,7 +629,9 @@ abstract class TemplateAbstractView<
|
|||||||
|
|
||||||
val newCustomView = buildViewForCustomField(newCustomFieldWithValue)
|
val newCustomView = buildViewForCustomField(newCustomFieldWithValue)
|
||||||
newCustomView?.let {
|
newCustomView?.let {
|
||||||
parentGroup.addView(newCustomView, indexInParent)
|
parentGroup?.indexOfChild(viewToReplace)?.let { indexInParent ->
|
||||||
|
parentGroup.addView(newCustomView, indexInParent)
|
||||||
|
}
|
||||||
mViewFields.add(
|
mViewFields.add(
|
||||||
oldPosition,
|
oldPosition,
|
||||||
ViewField(
|
ViewField(
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ import com.kunzisoft.keepass.database.element.template.TemplateAttribute
|
|||||||
import com.kunzisoft.keepass.database.element.template.TemplateAttributeAction
|
import com.kunzisoft.keepass.database.element.template.TemplateAttributeAction
|
||||||
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.database.helper.getLocalizedName
|
||||||
|
import com.kunzisoft.keepass.database.helper.isStandardPasswordName
|
||||||
|
import com.kunzisoft.keepass.model.DataDate
|
||||||
|
import com.kunzisoft.keepass.model.DataTime
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import org.joda.time.DateTime
|
|
||||||
|
|
||||||
|
|
||||||
class TemplateEditView @JvmOverloads constructor(context: Context,
|
class TemplateEditView @JvmOverloads constructor(context: Context,
|
||||||
@@ -112,15 +114,22 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
|
|||||||
override fun buildLinearTextView(templateAttribute: TemplateAttribute,
|
override fun buildLinearTextView(templateAttribute: TemplateAttribute,
|
||||||
field: Field): TextEditFieldView? {
|
field: Field): TextEditFieldView? {
|
||||||
return context?.let {
|
return context?.let {
|
||||||
TextEditFieldView(it).apply {
|
(if (TemplateField.isStandardPasswordName(context, templateAttribute.label))
|
||||||
|
PasswordTextEditFieldView(it)
|
||||||
|
else TextEditFieldView(it)).apply {
|
||||||
// hiddenProtectedValue (mHideProtectedValue) don't work with TextInputLayout
|
// hiddenProtectedValue (mHideProtectedValue) don't work with TextInputLayout
|
||||||
setProtection(field.protectedValue.isProtected)
|
setProtection(field.protectedValue.isProtected)
|
||||||
default = templateAttribute.default
|
default = templateAttribute.default
|
||||||
setMaxChars(templateAttribute.options.getNumberChars())
|
setMaxChars(templateAttribute.options.getNumberChars())
|
||||||
setMaxLines(templateAttribute.options.getNumberLines())
|
setMaxLines(templateAttribute.options.getNumberLines())
|
||||||
setActionClick(templateAttribute, field, this)
|
setActionClick(templateAttribute, field, this)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO
|
if (field.protectedValue.isProtected) {
|
||||||
|
textDirection = TEXT_DIRECTION_LTR
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,35 +220,31 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
|
|||||||
val dateTimeView = getFieldViewById(viewId)
|
val dateTimeView = getFieldViewById(viewId)
|
||||||
if (dateTimeView is DateTimeEditFieldView) {
|
if (dateTimeView is DateTimeEditFieldView) {
|
||||||
dateTimeView.dateTime = DateInstant(
|
dateTimeView.dateTime = DateInstant(
|
||||||
action.invoke(dateTimeView.dateTime).date,
|
action.invoke(dateTimeView.dateTime).instant,
|
||||||
dateTimeView.dateTime.type)
|
dateTimeView.dateTime.type
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setCurrentDateTimeValue(dateMilliseconds: Long) {
|
fun setCurrentDateTimeValue(date: DataDate) {
|
||||||
// Save the date
|
// Save the date
|
||||||
setCurrentDateTimeSelection { instant ->
|
setCurrentDateTimeSelection { dateInstant ->
|
||||||
val newDateInstant = DateInstant(
|
dateInstant.setDate(date.year, date.month, date.day)
|
||||||
DateTime(instant.date)
|
if (dateInstant.type == DateInstant.Type.DATE_TIME) {
|
||||||
.withMillis(dateMilliseconds)
|
|
||||||
.toDate(), instant.type)
|
|
||||||
if (instant.type == DateInstant.Type.DATE_TIME) {
|
|
||||||
val instantTime = DateInstant(instant.date, DateInstant.Type.TIME)
|
|
||||||
// Trick to recall selection with time
|
// Trick to recall selection with time
|
||||||
mOnDateInstantClickListener?.invoke(instantTime)
|
mOnDateInstantClickListener?.invoke(
|
||||||
|
DateInstant(dateInstant.instant, DateInstant.Type.TIME)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
newDateInstant
|
dateInstant
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setCurrentTimeValue(time: DataTime) {
|
fun setCurrentTimeValue(time: DataTime) {
|
||||||
setCurrentDateTimeSelection { instant ->
|
setCurrentDateTimeSelection { dateInstant ->
|
||||||
DateInstant(
|
dateInstant.setTime(time.hour, time.minute)
|
||||||
DateTime(instant.date)
|
dateInstant
|
||||||
.withHourOfDay(time.hours)
|
|
||||||
.withMinuteOfHour(time.minutes)
|
|
||||||
.toDate(), instant.type)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.kunzisoft.keepass.view
|
package com.kunzisoft.keepass.view
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
@@ -10,6 +11,7 @@ import com.kunzisoft.keepass.database.element.security.ProtectedString
|
|||||||
import com.kunzisoft.keepass.database.element.template.TemplateAttribute
|
import com.kunzisoft.keepass.database.element.template.TemplateAttribute
|
||||||
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.database.helper.getLocalizedName
|
||||||
|
import com.kunzisoft.keepass.database.helper.isStandardPasswordName
|
||||||
import com.kunzisoft.keepass.model.OtpModel
|
import com.kunzisoft.keepass.model.OtpModel
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
||||||
@@ -48,7 +50,9 @@ class TemplateView @JvmOverloads constructor(context: Context,
|
|||||||
field: Field): TextFieldView? {
|
field: Field): TextFieldView? {
|
||||||
// Add an action icon if needed
|
// Add an action icon if needed
|
||||||
return context?.let {
|
return context?.let {
|
||||||
TextFieldView(it).apply {
|
(if (TemplateField.isStandardPasswordName(context, templateAttribute.label))
|
||||||
|
PasswordTextFieldView(it)
|
||||||
|
else TextFieldView(it)).apply {
|
||||||
applyFontVisibility(mFontInVisibility)
|
applyFontVisibility(mFontInVisibility)
|
||||||
setProtection(field.protectedValue.isProtected, mHideProtectedValue)
|
setProtection(field.protectedValue.isProtected, mHideProtectedValue)
|
||||||
label = templateAttribute.alias
|
label = templateAttribute.alias
|
||||||
@@ -59,6 +63,9 @@ class TemplateView @JvmOverloads constructor(context: Context,
|
|||||||
// Here the value is often empty
|
// Here the value is often empty
|
||||||
|
|
||||||
if (field.protectedValue.isProtected) {
|
if (field.protectedValue.isProtected) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
|
textDirection = TEXT_DIRECTION_LTR
|
||||||
|
}
|
||||||
if (mFirstTimeAskAllowCopyProtectedFields) {
|
if (mFirstTimeAskAllowCopyProtectedFields) {
|
||||||
setCopyButtonState(TextFieldView.ButtonState.DEACTIVATE)
|
setCopyButtonState(TextFieldView.ButtonState.DEACTIVATE)
|
||||||
setCopyButtonClickListener { _, _ ->
|
setCopyButtonClickListener { _, _ ->
|
||||||
@@ -100,13 +107,12 @@ class TemplateView @JvmOverloads constructor(context: Context,
|
|||||||
return context?.let {
|
return context?.let {
|
||||||
DateTimeFieldView(it).apply {
|
DateTimeFieldView(it).apply {
|
||||||
label = TemplateField.getLocalizedName(context, field.name)
|
label = TemplateField.getLocalizedName(context, field.name)
|
||||||
val dateInstantType = templateAttribute.options.getDateFormat()
|
type = templateAttribute.options.getDateFormat()
|
||||||
|
isExpirable = templateAttribute.options.getExpirable()
|
||||||
try {
|
try {
|
||||||
val value = field.protectedValue.toString().trim()
|
val value = field.protectedValue.toString().trim()
|
||||||
type = dateInstantType
|
|
||||||
activation = value.isNotEmpty()
|
activation = value.isNotEmpty()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
type = dateInstantType
|
|
||||||
activation = false
|
activation = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,6 +179,9 @@ class TemplateView @JvmOverloads constructor(context: Context,
|
|||||||
otpElement.type.name,
|
otpElement.type.name,
|
||||||
ProtectedString(false, otpElement.token)))
|
ProtectedString(false, otpElement.token)))
|
||||||
}
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
|
textDirection = TEXT_DIRECTION_LTR
|
||||||
|
}
|
||||||
mLastOtpTokenView = this
|
mLastOtpTokenView = this
|
||||||
mOtpRunnable = Runnable {
|
mOtpRunnable = Runnable {
|
||||||
if (otpElement.shouldRefreshToken()) {
|
if (otpElement.shouldRefreshToken()) {
|
||||||
|
|||||||
@@ -8,45 +8,40 @@ import android.text.Spannable
|
|||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.ContextThemeWrapper
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
import androidx.appcompat.widget.AppCompatImageButton
|
import androidx.appcompat.widget.AppCompatImageButton
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.widget.doAfterTextChanged
|
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
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.template.TemplateField
|
|
||||||
import com.kunzisoft.keepass.database.helper.isStandardPasswordName
|
|
||||||
import com.kunzisoft.keepass.password.PasswordGenerator
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|
||||||
class TextEditFieldView @JvmOverloads constructor(context: Context,
|
open class TextEditFieldView @JvmOverloads constructor(context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyle: Int = 0)
|
defStyle: Int = 0)
|
||||||
: RelativeLayout(context, attrs, defStyle), GenericTextFieldView {
|
: RelativeLayout(context, attrs, defStyle), GenericTextFieldView {
|
||||||
|
|
||||||
private var labelViewId = ViewCompat.generateViewId()
|
private var labelViewId = ViewCompat.generateViewId()
|
||||||
private var valueViewId = ViewCompat.generateViewId()
|
private var valueViewId = ViewCompat.generateViewId()
|
||||||
private var actionImageButtonId = ViewCompat.generateViewId()
|
protected var actionImageButtonId = ViewCompat.generateViewId()
|
||||||
|
|
||||||
private var textModified = false
|
|
||||||
private var isColorizedPasswordActivated = PreferencesUtil.colorizePassword(context)
|
|
||||||
|
|
||||||
private val labelView = TextInputLayout(context).apply {
|
private val labelView = TextInputLayout(context).apply {
|
||||||
layoutParams = LayoutParams(
|
layoutParams = LayoutParams(
|
||||||
LayoutParams.MATCH_PARENT,
|
LayoutParams.MATCH_PARENT,
|
||||||
LayoutParams.WRAP_CONTENT)
|
LayoutParams.WRAP_CONTENT)
|
||||||
}
|
}
|
||||||
private val valueView = TextInputEditText(
|
protected val valueView = TextInputEditText(
|
||||||
ContextThemeWrapper(getContext(),
|
ContextThemeWrapper(
|
||||||
R.style.KeepassDXStyle_TextInputLayout)
|
getContext(),
|
||||||
|
R.style.KeepassDXStyle_TextInputLayout
|
||||||
|
)
|
||||||
).apply {
|
).apply {
|
||||||
layoutParams = LinearLayout.LayoutParams(
|
layoutParams = LinearLayout.LayoutParams(
|
||||||
LayoutParams.MATCH_PARENT,
|
LayoutParams.MATCH_PARENT,
|
||||||
@@ -62,7 +57,10 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
|
|||||||
maxLines = 1
|
maxLines = 1
|
||||||
}
|
}
|
||||||
private var actionImageButton = AppCompatImageButton(
|
private var actionImageButton = AppCompatImageButton(
|
||||||
ContextThemeWrapper(context, R.style.KeepassDXStyle_ImageButton_Simple), null, 0).apply {
|
ContextThemeWrapper(
|
||||||
|
context,
|
||||||
|
R.style.KeepassDXStyle_ImageButton_Simple
|
||||||
|
), null, 0).apply {
|
||||||
layoutParams = LayoutParams(
|
layoutParams = LayoutParams(
|
||||||
LayoutParams.WRAP_CONTENT,
|
LayoutParams.WRAP_CONTENT,
|
||||||
LayoutParams.WRAP_CONTENT).also {
|
LayoutParams.WRAP_CONTENT).also {
|
||||||
@@ -83,20 +81,6 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
|
|||||||
init {
|
init {
|
||||||
// Manually write view to avoid view id bugs
|
// Manually write view to avoid view id bugs
|
||||||
buildViews()
|
buildViews()
|
||||||
// To change the password color dynamically
|
|
||||||
valueView.doAfterTextChanged { editable ->
|
|
||||||
editable?.let { text ->
|
|
||||||
if (textModified) {
|
|
||||||
textModified = false
|
|
||||||
} else {
|
|
||||||
textModified = true
|
|
||||||
val selectionStart = valueView.selectionStart
|
|
||||||
val selectionEnd = valueView.selectionEnd
|
|
||||||
value = spannableValue(text.toString()).toString()
|
|
||||||
valueView.setSelection(selectionStart, selectionEnd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
labelView.addView(valueView)
|
labelView.addView(valueView)
|
||||||
addView(labelView)
|
addView(labelView)
|
||||||
addView(actionImageButton)
|
addView(actionImageButton)
|
||||||
@@ -105,10 +89,10 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
|
|||||||
private fun buildViews() {
|
private fun buildViews() {
|
||||||
labelView.apply {
|
labelView.apply {
|
||||||
id = labelViewId
|
id = labelViewId
|
||||||
layoutParams = (layoutParams as LayoutParams?).also {
|
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||||
it?.addRule(LEFT_OF, actionImageButtonId)
|
it.addRule(LEFT_OF, actionImageButtonId)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
it?.addRule(START_OF, actionImageButtonId)
|
it.addRule(START_OF, actionImageButtonId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,15 +113,6 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
|
|||||||
return actionImageButton
|
return actionImageButton
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun spannableValue(value: String?): Spannable? {
|
|
||||||
if (value == null)
|
|
||||||
return null
|
|
||||||
return if (isColorizedPasswordActivated && TemplateField.isStandardPasswordName(context, label))
|
|
||||||
PasswordGenerator.getColorizedPassword(value)
|
|
||||||
else
|
|
||||||
SpannableString(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
override var label: String
|
override var label: String
|
||||||
get() {
|
get() {
|
||||||
return labelView.hint?.toString() ?: ""
|
return labelView.hint?.toString() ?: ""
|
||||||
@@ -151,6 +126,10 @@ class TextEditFieldView @JvmOverloads constructor(context: Context,
|
|||||||
buildViews()
|
buildViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected open fun spannableValue(value: String?): Spannable? {
|
||||||
|
return SpannableString(value)
|
||||||
|
}
|
||||||
|
|
||||||
override var value: String
|
override var value: String
|
||||||
get() {
|
get() {
|
||||||
return valueView.text?.toString() ?: ""
|
return valueView.text?.toString() ?: ""
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.view
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.text.InputFilter
|
import android.text.InputFilter
|
||||||
import android.text.SpannableString
|
|
||||||
import android.text.util.Linkify
|
import android.text.util.Linkify
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
@@ -38,15 +37,11 @@ import androidx.core.text.util.LinkifyCompat
|
|||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
|
||||||
import com.kunzisoft.keepass.database.helper.isStandardPasswordName
|
|
||||||
import com.kunzisoft.keepass.model.EntryInfo.Companion.APPLICATION_ID_FIELD_NAME
|
import com.kunzisoft.keepass.model.EntryInfo.Companion.APPLICATION_ID_FIELD_NAME
|
||||||
import com.kunzisoft.keepass.password.PasswordGenerator
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.openExternalApp
|
import com.kunzisoft.keepass.utils.UriUtil.openExternalApp
|
||||||
|
|
||||||
|
|
||||||
class TextFieldView @JvmOverloads constructor(context: Context,
|
open class TextFieldView @JvmOverloads constructor(context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyle: Int = 0)
|
defStyle: Int = 0)
|
||||||
: RelativeLayout(context, attrs, defStyle), GenericTextFieldView {
|
: RelativeLayout(context, attrs, defStyle), GenericTextFieldView {
|
||||||
@@ -56,7 +51,7 @@ class TextFieldView @JvmOverloads constructor(context: Context,
|
|||||||
private var showButtonId = ViewCompat.generateViewId()
|
private var showButtonId = ViewCompat.generateViewId()
|
||||||
private var copyButtonId = ViewCompat.generateViewId()
|
private var copyButtonId = ViewCompat.generateViewId()
|
||||||
|
|
||||||
private val labelView = AppCompatTextView(context).apply {
|
protected val labelView = AppCompatTextView(context).apply {
|
||||||
setTextAppearance(context,
|
setTextAppearance(context,
|
||||||
R.style.KeepassDXStyle_TextAppearance_LabelTextStyle)
|
R.style.KeepassDXStyle_TextAppearance_LabelTextStyle)
|
||||||
layoutParams = LayoutParams(
|
layoutParams = LayoutParams(
|
||||||
@@ -77,7 +72,7 @@ class TextFieldView @JvmOverloads constructor(context: Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val valueView = AppCompatTextView(context).apply {
|
protected val valueView = AppCompatTextView(context).apply {
|
||||||
setTextAppearance(context,
|
setTextAppearance(context,
|
||||||
R.style.KeepassDXStyle_TextAppearance_TextNode)
|
R.style.KeepassDXStyle_TextAppearance_TextNode)
|
||||||
layoutParams = LayoutParams(
|
layoutParams = LayoutParams(
|
||||||
@@ -131,46 +126,46 @@ class TextFieldView @JvmOverloads constructor(context: Context,
|
|||||||
private fun buildViews() {
|
private fun buildViews() {
|
||||||
copyButton.apply {
|
copyButton.apply {
|
||||||
id = copyButtonId
|
id = copyButtonId
|
||||||
layoutParams = (layoutParams as LayoutParams?).also {
|
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||||
it?.addRule(ALIGN_PARENT_RIGHT)
|
it.addRule(ALIGN_PARENT_RIGHT)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
it?.addRule(ALIGN_PARENT_END)
|
it.addRule(ALIGN_PARENT_END)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
showButton.apply {
|
showButton.apply {
|
||||||
id = showButtonId
|
id = showButtonId
|
||||||
layoutParams = (layoutParams as LayoutParams?).also {
|
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||||
if (copyButton.isVisible) {
|
if (copyButton.isVisible) {
|
||||||
it?.addRule(LEFT_OF, copyButtonId)
|
it.addRule(LEFT_OF, copyButtonId)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
it?.addRule(START_OF, copyButtonId)
|
it.addRule(START_OF, copyButtonId)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
it?.addRule(ALIGN_PARENT_RIGHT)
|
it.addRule(ALIGN_PARENT_RIGHT)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
it?.addRule(ALIGN_PARENT_END)
|
it.addRule(ALIGN_PARENT_END)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
labelView.apply {
|
labelView.apply {
|
||||||
id = labelViewId
|
id = labelViewId
|
||||||
layoutParams = (layoutParams as LayoutParams?).also {
|
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||||
it?.addRule(LEFT_OF, showButtonId)
|
it.addRule(LEFT_OF, showButtonId)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
it?.addRule(START_OF, showButtonId)
|
it.addRule(START_OF, showButtonId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
valueView.apply {
|
valueView.apply {
|
||||||
id = valueViewId
|
id = valueViewId
|
||||||
layoutParams = (layoutParams as LayoutParams?).also {
|
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||||
it?.addRule(LEFT_OF, showButtonId)
|
it.addRule(LEFT_OF, showButtonId)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
it?.addRule(START_OF, showButtonId)
|
it.addRule(START_OF, showButtonId)
|
||||||
}
|
}
|
||||||
it?.addRule(BELOW, labelViewId)
|
it.addRule(BELOW, labelViewId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,7 +183,7 @@ class TextFieldView @JvmOverloads constructor(context: Context,
|
|||||||
labelView.text = value
|
labelView.text = value
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setLabel(@StringRes labelId: Int) {
|
open fun setLabel(@StringRes labelId: Int) {
|
||||||
labelView.setText(labelId)
|
labelView.setText(labelId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,17 +192,11 @@ class TextFieldView @JvmOverloads constructor(context: Context,
|
|||||||
return valueView.text.toString()
|
return valueView.text.toString()
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
val spannableString =
|
valueView.text = value
|
||||||
if (PreferencesUtil.colorizePassword(context)
|
|
||||||
&& TemplateField.isStandardPasswordName(context, label))
|
|
||||||
PasswordGenerator.getColorizedPassword(value)
|
|
||||||
else
|
|
||||||
SpannableString(value)
|
|
||||||
valueView.text = spannableString
|
|
||||||
changeProtectedValueParameters()
|
changeProtectedValueParameters()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setValue(@StringRes valueId: Int) {
|
open fun setValue(@StringRes valueId: Int) {
|
||||||
value = resources.getString(valueId)
|
value = resources.getString(valueId)
|
||||||
changeProtectedValueParameters()
|
changeProtectedValueParameters()
|
||||||
}
|
}
|
||||||
@@ -237,7 +226,7 @@ class TextFieldView @JvmOverloads constructor(context: Context,
|
|||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun changeProtectedValueParameters() {
|
protected fun changeProtectedValueParameters() {
|
||||||
valueView.apply {
|
valueView.apply {
|
||||||
if (showButton.isVisible) {
|
if (showButton.isVisible) {
|
||||||
applyHiddenStyle(showButton.isSelected)
|
applyHiddenStyle(showButton.isSelected)
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import androidx.core.view.forEach
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.core.view.updatePaddingRelative
|
||||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
@@ -225,14 +226,20 @@ fun View.showByFading() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun View.updateLockPaddingLeft() {
|
fun View.updateLockPaddingStart() {
|
||||||
updatePadding(resources.getDimensionPixelSize(
|
resources.getDimensionPixelSize(
|
||||||
if (PreferencesUtil.showLockDatabaseButton(context)) {
|
if (PreferencesUtil.showLockDatabaseButton(context)) {
|
||||||
R.dimen.lock_button_size
|
R.dimen.lock_button_size
|
||||||
} else {
|
} else {
|
||||||
R.dimen.hidden_lock_button_size
|
R.dimen.hidden_lock_button_size
|
||||||
}
|
}
|
||||||
))
|
).let { lockPadding ->
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
|
updatePaddingRelative(lockPadding)
|
||||||
|
} else {
|
||||||
|
updatePadding(lockPadding)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.showActionErrorIfNeeded(result: ActionRunnable.Result) {
|
fun Context.showActionErrorIfNeeded(result: ActionRunnable.Result) {
|
||||||
@@ -306,9 +313,12 @@ fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, appl
|
|||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1
|
||||||
&& resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
&& resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
this.window.navigationBarColor = ContextCompat.getColor(this, R.color.surface_selector)
|
window.navigationBarColor = ContextCompat.getColor(this, R.color.surface_selector)
|
||||||
if (applyToStatusBar) {
|
if (applyToStatusBar) {
|
||||||
this.window.statusBarColor = ContextCompat.getColor(this, R.color.surface_selector)
|
obtainStyledAttributes(intArrayOf(R.attr.colorSurface)).apply {
|
||||||
|
window.statusBarColor = getColor(0, Color.GRAY)
|
||||||
|
recycle()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
applyWindowInsets.invoke()
|
applyWindowInsets.invoke()
|
||||||
}
|
}
|
||||||
@@ -356,6 +366,23 @@ fun View.applyWindowInsets(position: WindowInsetPosition = WindowInsetPosition.B
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
WindowInsetPosition.BOTTOM_IME -> {
|
||||||
|
val imeHeight = windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
||||||
|
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
|
||||||
|
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
|
bottomMargin = if (imeHeight > 1) 0 else insets.bottom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowInsetPosition.TOP_BOTTOM_IME -> {
|
||||||
|
val imeHeight = windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
||||||
|
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
|
||||||
|
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
|
topMargin = insets.top
|
||||||
|
bottomMargin = if (imeHeight > 1) imeHeight else 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// If any of the children consumed the insets, return an appropriate value
|
// If any of the children consumed the insets, return an appropriate value
|
||||||
if (consumed) WindowInsetsCompat.CONSUMED else windowInsets
|
if (consumed) WindowInsetsCompat.CONSUMED else windowInsets
|
||||||
@@ -363,5 +390,5 @@ fun View.applyWindowInsets(position: WindowInsetPosition = WindowInsetPosition.B
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum class WindowInsetPosition {
|
enum class WindowInsetPosition {
|
||||||
TOP, BOTTOM, LEGIT_TOP
|
TOP, BOTTOM, LEGIT_TOP, BOTTOM_IME, TOP_BOTTOM_IME
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.viewmodels
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
|
|
||||||
class AdvancedUnlockViewModel : ViewModel() {
|
|
||||||
|
|
||||||
var allowAutoOpenBiometricPrompt : Boolean = true
|
|
||||||
var deviceCredentialAuthSucceeded: Boolean? = null
|
|
||||||
|
|
||||||
val onInitAdvancedUnlockModeRequested : LiveData<Void?> get() = _onInitAdvancedUnlockModeRequested
|
|
||||||
private val _onInitAdvancedUnlockModeRequested = SingleLiveEvent<Void?>()
|
|
||||||
|
|
||||||
val onUnlockAvailabilityCheckRequested : LiveData<Void?> get() = _onUnlockAvailabilityCheckRequested
|
|
||||||
private val _onUnlockAvailabilityCheckRequested = SingleLiveEvent<Void?>()
|
|
||||||
|
|
||||||
val onDatabaseFileLoaded : LiveData<Uri?> get() = _onDatabaseFileLoaded
|
|
||||||
private val _onDatabaseFileLoaded = SingleLiveEvent<Uri?>()
|
|
||||||
|
|
||||||
fun initAdvancedUnlockMode() {
|
|
||||||
_onInitAdvancedUnlockModeRequested.call()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkUnlockAvailability() {
|
|
||||||
_onUnlockAvailabilityCheckRequested.call()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun databaseFileLoaded(databaseUri: Uri?) {
|
|
||||||
_onDatabaseFileLoaded.value = databaseUri
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -51,7 +51,9 @@ class DatabaseFileViewModel(application: Application) : AndroidViewModel(applica
|
|||||||
|
|
||||||
fun loadDatabaseFile(databaseUri: Uri) {
|
fun loadDatabaseFile(databaseUri: Uri) {
|
||||||
mFileDatabaseHistoryAction?.getDatabaseFile(databaseUri) { databaseFileRetrieved ->
|
mFileDatabaseHistoryAction?.getDatabaseFile(databaseUri) { databaseFileRetrieved ->
|
||||||
mDatabaseFileLoaded.value = databaseFileRetrieved
|
databaseFileRetrieved?.let {
|
||||||
|
mDatabaseFileLoaded.value = it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.kunzisoft.keepass.viewmodels
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.kunzisoft.keepass.app.App
|
import com.kunzisoft.keepass.app.App
|
||||||
@@ -10,8 +11,8 @@ 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.IOActionTask
|
||||||
import com.kunzisoft.keepass.utils.parseUri
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.releaseUriPermission
|
import com.kunzisoft.keepass.utils.UriUtil.releaseUriPermission
|
||||||
|
import com.kunzisoft.keepass.utils.parseUri
|
||||||
|
|
||||||
class DatabaseFilesViewModel(application: Application) : AndroidViewModel(application) {
|
class DatabaseFilesViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
@@ -25,11 +26,29 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
|
|||||||
MutableLiveData<DatabaseFileData>()
|
MutableLiveData<DatabaseFileData>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var mDefaultDatabaseAlreadyChecked : Boolean = false
|
||||||
|
|
||||||
val defaultDatabase: MutableLiveData<Uri?> by lazy {
|
val defaultDatabase: MutableLiveData<Uri?> by lazy {
|
||||||
MutableLiveData<Uri?>()
|
MutableLiveData<Uri?>()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkDefaultDatabase() {
|
fun doForDefaultDatabase(action: (defaultDatabaseUri: Uri) -> Unit) {
|
||||||
|
if (!mDefaultDatabaseAlreadyChecked) {
|
||||||
|
mDefaultDatabaseAlreadyChecked = true
|
||||||
|
val context = getApplication<App>().applicationContext
|
||||||
|
PreferencesUtil.getDefaultDatabasePath(context)?.parseUri()?.let { databaseFileUri ->
|
||||||
|
if (FileDatabaseInfo(context, databaseFileUri).exists) {
|
||||||
|
action.invoke(databaseFileUri)
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Unable to automatically load a non-accessible file")
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
Log.i(TAG, "No default database to prepare")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkDefaultDatabase() {
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
{
|
{
|
||||||
PreferencesUtil.getDefaultDatabasePath(getApplication<App>().applicationContext)
|
PreferencesUtil.getDefaultDatabasePath(getApplication<App>().applicationContext)
|
||||||
@@ -149,4 +168,8 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
|
|||||||
enum class DatabaseFileAction {
|
enum class DatabaseFileAction {
|
||||||
NONE, ADD, UPDATE, DELETE
|
NONE, ADD, UPDATE, DELETE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = DatabaseFilesViewModel::class.java.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,435 @@
|
|||||||
|
package com.kunzisoft.keepass.viewmodels
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.biometric.BiometricPrompt
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||||
|
import com.kunzisoft.keepass.biometric.DeviceUnlockCryptoPrompt
|
||||||
|
import com.kunzisoft.keepass.biometric.DeviceUnlockCryptoPromptType
|
||||||
|
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
||||||
|
import com.kunzisoft.keepass.biometric.DeviceUnlockMode
|
||||||
|
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
|
||||||
|
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 kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
|
||||||
|
class DeviceUnlockViewModel(application: Application): AndroidViewModel(application) {
|
||||||
|
|
||||||
|
var allowAutoOpenBiometricPrompt : Boolean = true
|
||||||
|
|
||||||
|
private var cipherDatabaseListener: CipherDatabaseAction.CipherDatabaseListener? = null
|
||||||
|
|
||||||
|
private var isConditionToStoreCredentialVerified: Boolean = false
|
||||||
|
|
||||||
|
private var deviceUnlockManager: DeviceUnlockManager? = null
|
||||||
|
private var databaseUri: Uri? = null
|
||||||
|
|
||||||
|
private var deviceUnlockMode = DeviceUnlockMode.BIOMETRIC_UNAVAILABLE
|
||||||
|
var cryptoPrompt: DeviceUnlockCryptoPrompt? = null
|
||||||
|
|
||||||
|
// TODO Retrieve credential storage from app database
|
||||||
|
var credentialDatabaseStorage: CredentialStorage = CredentialStorage.DEFAULT
|
||||||
|
|
||||||
|
val cipherDatabaseAction = CipherDatabaseAction.getInstance(getApplication())
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(DeviceUnlockState())
|
||||||
|
val uiState: StateFlow<DeviceUnlockState> = _uiState
|
||||||
|
|
||||||
|
fun checkConditionToStoreCredential(condition: Boolean, databaseFileUri: Uri?) {
|
||||||
|
isConditionToStoreCredentialVerified = condition
|
||||||
|
checkUnlockAvailability(databaseFileUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check unlock availability by verifying device settings and database mode
|
||||||
|
*/
|
||||||
|
fun checkUnlockAvailability() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipherDatabase ->
|
||||||
|
if (PreferencesUtil.isBiometricUnlockEnable(getApplication())) {
|
||||||
|
// biometric not supported (by API level or hardware) so keep option hidden
|
||||||
|
// or manually disable
|
||||||
|
val biometricCanAuthenticate = DeviceUnlockManager.canAuthenticate(getApplication())
|
||||||
|
if (!PreferencesUtil.isAdvancedUnlockEnable(getApplication())
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
|
||||||
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) {
|
||||||
|
changeMode(DeviceUnlockMode.BIOMETRIC_UNAVAILABLE)
|
||||||
|
} else if (biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED) {
|
||||||
|
changeMode(DeviceUnlockMode.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) {
|
||||||
|
changeMode(DeviceUnlockMode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
|
||||||
|
} else {
|
||||||
|
selectMode(containsCipherDatabase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (PreferencesUtil.isDeviceCredentialUnlockEnable(getApplication())) {
|
||||||
|
if (DeviceUnlockManager.isDeviceSecure(getApplication())) {
|
||||||
|
selectMode(containsCipherDatabase)
|
||||||
|
} else {
|
||||||
|
changeMode(DeviceUnlockMode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check unlock availability and change the current mode depending of device's state
|
||||||
|
*/
|
||||||
|
fun checkUnlockAvailability(databaseFileUri: Uri?) {
|
||||||
|
databaseUri = databaseFileUri
|
||||||
|
checkUnlockAvailability()
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun selectMode(containsCipherDatabase: Boolean) {
|
||||||
|
try {
|
||||||
|
if (isConditionToStoreCredentialVerified) {
|
||||||
|
deviceUnlockManager = DeviceUnlockManager(getApplication())
|
||||||
|
// listen for encryption
|
||||||
|
changeMode(DeviceUnlockMode.STORE_CREDENTIAL)
|
||||||
|
initEncryptData()
|
||||||
|
} else if (containsCipherDatabase) {
|
||||||
|
deviceUnlockManager = DeviceUnlockManager(getApplication())
|
||||||
|
// biometric available but no stored password found yet for this DB
|
||||||
|
// listen for decryption
|
||||||
|
changeMode(DeviceUnlockMode.EXTRACT_CREDENTIAL)
|
||||||
|
initDecryptData()
|
||||||
|
} else {
|
||||||
|
// wait for typing
|
||||||
|
changeMode(DeviceUnlockMode.WAIT_CREDENTIAL)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
changeMode(DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE)
|
||||||
|
setException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun connect(databaseUri: Uri) {
|
||||||
|
this.databaseUri = databaseUri
|
||||||
|
cipherDatabaseListener = object: CipherDatabaseAction.CipherDatabaseListener {
|
||||||
|
override fun onCipherDatabaseCleared() {
|
||||||
|
closeBiometricPrompt()
|
||||||
|
checkUnlockAvailability(databaseUri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cipherDatabaseAction.apply {
|
||||||
|
reloadPreferences()
|
||||||
|
cipherDatabaseListener?.let {
|
||||||
|
registerDatabaseListener(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkUnlockAvailability(databaseUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disconnect() {
|
||||||
|
this.databaseUri = null
|
||||||
|
cipherDatabaseListener?.let {
|
||||||
|
cipherDatabaseAction.unregisterDatabaseListener(it)
|
||||||
|
}
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun databaseFileLoaded(databaseUri: Uri?) {
|
||||||
|
// To get device credential unlock result, only if same database uri
|
||||||
|
if (databaseUri != null
|
||||||
|
&& PreferencesUtil.isAdvancedUnlockEnable(getApplication())) {
|
||||||
|
if (databaseUri != this.databaseUri) {
|
||||||
|
connect(databaseUri)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun onAuthenticationSucceeded(
|
||||||
|
activityResult: ActivityResult
|
||||||
|
) {
|
||||||
|
cryptoPrompt?.let { prompt ->
|
||||||
|
when (prompt.type) {
|
||||||
|
DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION ->
|
||||||
|
retrieveCredentialForEncryption( prompt.cipher)
|
||||||
|
DeviceUnlockCryptoPromptType.CREDENTIAL_DECRYPTION ->
|
||||||
|
decryptCredential( prompt.cipher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun onAuthenticationSucceeded(
|
||||||
|
result: BiometricPrompt.AuthenticationResult
|
||||||
|
) {
|
||||||
|
cryptoPrompt?.type?.let { type ->
|
||||||
|
when (type) {
|
||||||
|
DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION ->
|
||||||
|
retrieveCredentialForEncryption(result.cryptoObject?.cipher)
|
||||||
|
DeviceUnlockCryptoPromptType.CREDENTIAL_DECRYPTION ->
|
||||||
|
decryptCredential(result.cryptoObject?.cipher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun retrieveCredentialForEncryption(cipher: Cipher?) {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
credentialRequiredCipher = cipher
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun encryptCredential(
|
||||||
|
credential: ByteArray,
|
||||||
|
cipher: Cipher?
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
deviceUnlockManager?.encryptData(
|
||||||
|
value = credential,
|
||||||
|
cipher = cipher,
|
||||||
|
handleEncryptedResult = { encryptedValue, ivSpec ->
|
||||||
|
databaseUri?.let { databaseUri ->
|
||||||
|
onCredentialEncrypted(
|
||||||
|
CipherEncryptDatabase().apply {
|
||||||
|
this.databaseUri = databaseUri
|
||||||
|
this.credentialStorage = credentialDatabaseStorage
|
||||||
|
this.encryptedValue = encryptedValue
|
||||||
|
this.specParameters = ivSpec
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
setException(e)
|
||||||
|
} finally {
|
||||||
|
// Reinit credential storage request
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
credentialRequiredCipher = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun decryptCredential(cipher: Cipher?) {
|
||||||
|
// retrieve the encrypted value from preferences
|
||||||
|
databaseUri?.let { databaseUri ->
|
||||||
|
cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase ->
|
||||||
|
cipherDatabase?.encryptedValue?.let { encryptedCredential ->
|
||||||
|
try {
|
||||||
|
deviceUnlockManager?.decryptData(
|
||||||
|
encryptedValue = encryptedCredential,
|
||||||
|
cipher = cipher,
|
||||||
|
handleDecryptedResult = { decryptedValue ->
|
||||||
|
// Load database directly with password retrieve
|
||||||
|
onCredentialDecrypted(
|
||||||
|
CipherDecryptDatabase().apply {
|
||||||
|
this.databaseUri = databaseUri
|
||||||
|
this.credentialStorage = credentialDatabaseStorage
|
||||||
|
this.decryptedValue = decryptedValue
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cipherDatabaseAction.resetCipherParameters(databaseUri)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
setException(e)
|
||||||
|
}
|
||||||
|
} ?: deleteEncryptedDatabaseKey()
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
setException(UnknownDatabaseLocationException())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCredentialEncrypted(cipherEncryptDatabase: CipherEncryptDatabase) {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
cipherEncryptDatabase = cipherEncryptDatabase
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consumeCredentialEncrypted() {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
cipherEncryptDatabase = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCredentialDecrypted(cipherDecryptDatabase: CipherDecryptDatabase) {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
cipherDecryptDatabase = cipherDecryptDatabase
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun consumeCredentialDecrypted() {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
cipherDecryptDatabase = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPromptRequested(
|
||||||
|
cryptoPrompt: DeviceUnlockCryptoPrompt,
|
||||||
|
autoOpen: Boolean = false
|
||||||
|
) {
|
||||||
|
this@DeviceUnlockViewModel.cryptoPrompt = cryptoPrompt
|
||||||
|
if (autoOpen && PreferencesUtil.isAdvancedUnlockPromptAutoOpenEnable(getApplication()))
|
||||||
|
showPrompt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showPrompt() {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
cryptoPromptState = DeviceUnlockPromptMode.SHOW
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun promptShown() {
|
||||||
|
allowAutoOpenBiometricPrompt = false
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
cryptoPromptState = DeviceUnlockPromptMode.IDLE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setException(value: Exception?) {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
exception = value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exceptionShown() {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
exception = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
private fun initEncryptData() {
|
||||||
|
try {
|
||||||
|
deviceUnlockManager?.initEncryptData { cryptoPrompt ->
|
||||||
|
onPromptRequested(cryptoPrompt)
|
||||||
|
} ?: setException(Exception("AdvancedUnlockManager not initialized"))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
setException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
private fun initDecryptData() {
|
||||||
|
databaseUri?.let { databaseUri ->
|
||||||
|
cipherDatabaseAction.getCipherDatabase(databaseUri) { cipherDatabase ->
|
||||||
|
cipherDatabase?.let {
|
||||||
|
try {
|
||||||
|
deviceUnlockManager?.initDecryptData(cipherDatabase.specParameters) { cryptoPrompt ->
|
||||||
|
onPromptRequested(cryptoPrompt, autoOpen = allowAutoOpenBiometricPrompt)
|
||||||
|
} ?: setException(Exception("AdvancedUnlockManager not initialized"))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
setException(e)
|
||||||
|
}
|
||||||
|
} ?: deleteEncryptedDatabaseKey()
|
||||||
|
}
|
||||||
|
} ?: setException(UnknownDatabaseLocationException())
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
private fun changeMode(deviceUnlockMode: DeviceUnlockMode) {
|
||||||
|
this.deviceUnlockMode = deviceUnlockMode
|
||||||
|
cipherDatabaseAction.containsCipherDatabase(databaseUri) { containsCipher ->
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
newDeviceUnlockMode = deviceUnlockMode,
|
||||||
|
allowAdvancedUnlockMenu = containsCipher
|
||||||
|
&& deviceUnlockMode != DeviceUnlockMode.BIOMETRIC_UNAVAILABLE
|
||||||
|
&& deviceUnlockMode != DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteEncryptedDatabaseKey() {
|
||||||
|
closeBiometricPrompt()
|
||||||
|
databaseUri?.let { databaseUri ->
|
||||||
|
cipherDatabaseAction.deleteByDatabaseUri(databaseUri) {
|
||||||
|
checkUnlockAvailability(databaseUri)
|
||||||
|
}
|
||||||
|
} ?: checkUnlockAvailability(null)
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
allowAdvancedUnlockMenu = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeBiometricPrompt() {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
cryptoPromptState = DeviceUnlockPromptMode.CLOSE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun biometricPromptClosed() {
|
||||||
|
cryptoPrompt = null
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
cryptoPromptState = DeviceUnlockPromptMode.IDLE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
changeMode(DeviceUnlockMode.BIOMETRIC_UNAVAILABLE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
deviceUnlockManager = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class DeviceUnlockPromptMode {
|
||||||
|
IDLE, SHOW, CLOSE
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DeviceUnlockState(
|
||||||
|
val newDeviceUnlockMode: DeviceUnlockMode = DeviceUnlockMode.BIOMETRIC_UNAVAILABLE,
|
||||||
|
val allowAdvancedUnlockMenu: Boolean = false,
|
||||||
|
val credentialRequiredCipher: Cipher? = null,
|
||||||
|
val cipherEncryptDatabase: CipherEncryptDatabase? = null,
|
||||||
|
val cipherDecryptDatabase: CipherDecryptDatabase? = null,
|
||||||
|
val cryptoPromptState: DeviceUnlockPromptMode = DeviceUnlockPromptMode.IDLE,
|
||||||
|
val autoOpenPrompt: Boolean = false,
|
||||||
|
val exception: Exception? = null
|
||||||
|
)
|
||||||
@@ -43,8 +43,8 @@ class KeyGeneratorViewModel: ViewModel() {
|
|||||||
val requirePassphraseGeneration : LiveData<Void?> get() = _requirePassphraseGeneration
|
val requirePassphraseGeneration : LiveData<Void?> get() = _requirePassphraseGeneration
|
||||||
private val _requirePassphraseGeneration = SingleLiveEvent<Void?>()
|
private val _requirePassphraseGeneration = SingleLiveEvent<Void?>()
|
||||||
|
|
||||||
fun setKeyGenerated(passKey: String) {
|
fun setKeyGenerated(value: String) {
|
||||||
_keyGenerated.value = passKey
|
_keyGenerated.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validateKeyGenerated() {
|
fun validateKeyGenerated() {
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import androidx.lifecycle.LiveData
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
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.view.DataTime
|
import com.kunzisoft.keepass.model.DataDate
|
||||||
|
import com.kunzisoft.keepass.model.DataTime
|
||||||
|
|
||||||
abstract class NodeEditViewModel : ViewModel() {
|
abstract class NodeEditViewModel : ViewModel() {
|
||||||
|
|
||||||
@@ -23,8 +24,8 @@ abstract class NodeEditViewModel : ViewModel() {
|
|||||||
|
|
||||||
val requestDateTimeSelection : LiveData<DateInstant> get() = _requestDateTimeSelection
|
val requestDateTimeSelection : LiveData<DateInstant> get() = _requestDateTimeSelection
|
||||||
private val _requestDateTimeSelection = SingleLiveEvent<DateInstant>()
|
private val _requestDateTimeSelection = SingleLiveEvent<DateInstant>()
|
||||||
val onDateSelected : LiveData<Long> get() = _onDateSelected
|
val onDateSelected : LiveData<DataDate> get() = _onDateSelected
|
||||||
private val _onDateSelected = SingleLiveEvent<Long>()
|
private val _onDateSelected = SingleLiveEvent<DataDate>()
|
||||||
val onTimeSelected : LiveData<DataTime> get() = _onTimeSelected
|
val onTimeSelected : LiveData<DataTime> get() = _onTimeSelected
|
||||||
private val _onTimeSelected = SingleLiveEvent<DataTime>()
|
private val _onTimeSelected = SingleLiveEvent<DataTime>()
|
||||||
|
|
||||||
@@ -57,12 +58,12 @@ abstract class NodeEditViewModel : ViewModel() {
|
|||||||
_requestDateTimeSelection.value = dateInstant
|
_requestDateTimeSelection.value = dateInstant
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectDate(dateMilliseconds: Long) {
|
fun selectDate(date: DataDate) {
|
||||||
_onDateSelected.value = dateMilliseconds
|
_onDateSelected.value = date
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectTime(hours: Int, minutes: Int) {
|
fun selectTime(dataTime: DataTime) {
|
||||||
_onTimeSelected.value = DataTime(hours, minutes)
|
_onTimeSelected.value = dataTime
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum class ColorRequest {
|
private enum class ColorRequest {
|
||||||
|
|||||||
13
app/src/main/res/drawable-ldrtl/ic_arrow_back_white_24dp.xml
Normal file
13
app/src/main/res/drawable-ldrtl/ic_arrow_back_white_24dp.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?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="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<group>
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeWidth="1.78885484"
|
||||||
|
android:pathData="M10,19Q7.5,19 5.5,17.5Q4,16 4,13.5Q4,11 5.5,9.5Q7.5,8 10,8L16,8L13.5,5.5L15,4L20,9L15,14L13.5,12.5L16,10L10,10Q8.5,10 7,11Q6,12 6,13.5Q6,15 7,16Q8.5,17 10,17L17,17L17,19L10,19Z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M8.59,16.59L13.17,12 8.59,7.41 10,6l6,6 -6,6 -1.41,-1.41z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:viewportWidth="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/green"
|
||||||
|
android:pathData="M14,7l-5,5 5,5V7z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:drawable="@drawable/ic_arrow_left_white_24dp"
|
||||||
|
android:fromDegrees="180"
|
||||||
|
android:toDegrees="180"
|
||||||
|
android:visible="true" />
|
||||||
9
app/src/main/res/drawable/ic_file_key_white_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_file_key_white_24dp.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:pathData="M11 16C11 16.6 10.6 17 10 17S9 16.6 9 16C9 15.4 9.4 15 10 15S11 15.4 11 16M20 8V20C20 21.1 19.1 22 18 22H6C4.9 22 4 21.1 4 20V4C4 2.9 4.9 2 6 2H14M18 15H12.8C12.2 13.4 10.5 12.6 9 13.2C7.4 13.8 6.6 15.5 7.2 17S9.5 19.4 11 18.8C11.9 18.5 12.5 17.8 12.8 17H14V19H16V17H18M18.5 9L13 3.5V9H18.5Z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M18,22l-0.01,-6L14,12l3.99,-4.01L18,2H6v6l4,4l-4,3.99V22H18zM8,7.5V4h8v3.5l-4,4L8,7.5z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/ic_shield_white_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_shield_white_24dp.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12V5l-9,-4z"/>
|
||||||
|
</vector>
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportHeight="24"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:width="24dp">
|
|
||||||
<path android:fillColor="#ffffff" android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<com.kunzisoft.keepass.view.AdvancedUnlockInfoView
|
<com.kunzisoft.keepass.view.DeviceUnlockView
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@+id/advanced_unlock_view"
|
android:id="@+id/advanced_unlock_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -22,59 +22,55 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:importantForAutofill="noExcludeDescendants"
|
android:importantForAutofill="noExcludeDescendants"
|
||||||
|
android:id="@+id/activity_entry_edit_container"
|
||||||
tools:targetApi="o"
|
tools:targetApi="o"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:filterTouchesWhenObscured="true">
|
android:filterTouchesWhenObscured="true">
|
||||||
|
|
||||||
|
<com.kunzisoft.keepass.view.ToolbarSpecial
|
||||||
|
android:id="@+id/special_mode_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
android:id="@+id/entry_edit_coordinator_layout"
|
android:id="@+id/entry_edit_coordinator_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/special_mode_view"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.core.widget.NestedScrollView
|
||||||
android:id="@+id/activity_entry_edit_container"
|
android:id="@+id/entry_edit_scroll"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
<com.kunzisoft.keepass.view.ToolbarSpecial
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
android:id="@+id/special_mode_view"
|
android:scrollbarStyle="insideOverlay"
|
||||||
|
android:scrollbars="none"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
android:paddingTop="@dimen/card_view_margin_vertical"
|
||||||
|
android:paddingBottom="128dp">
|
||||||
<androidx.core.widget.NestedScrollView
|
<androidx.appcompat.widget.AppCompatSpinner
|
||||||
android:id="@+id/entry_edit_scroll"
|
android:id="@+id/entry_edit_template_selector"
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/special_mode_view"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
android:scrollbarStyle="insideOverlay"
|
|
||||||
android:scrollbars="none"
|
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:paddingTop="@dimen/card_view_margin_vertical"
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
android:paddingBottom="128dp">
|
<androidx.fragment.app.FragmentContainerView
|
||||||
<androidx.appcompat.widget.AppCompatSpinner
|
android:id="@+id/entry_edit_content"
|
||||||
android:id="@+id/entry_edit_template_selector"
|
android:name="com.kunzisoft.keepass.activities.fragments.EntryEditFragment"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintWidth_percent="@dimen/content_percent"
|
||||||
<androidx.fragment.app.FragmentContainerView
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
android:id="@+id/entry_edit_content"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
android:name="com.kunzisoft.keepass.activities.fragments.EntryEditFragment"
|
app:layout_constraintTop_toBottomOf="@+id/entry_edit_template_selector"/>
|
||||||
android:layout_width="0dp"
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
android:layout_height="wrap_content"
|
</androidx.core.widget.NestedScrollView>
|
||||||
app:layout_constraintWidth_percent="@dimen/content_percent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/entry_edit_template_selector"/>
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
</androidx.core.widget.NestedScrollView>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
||||||
@@ -84,35 +80,47 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintBottom_toBottomOf="parent">
|
app:layout_constraintBottom_toBottomOf="parent">
|
||||||
|
|
||||||
<com.kunzisoft.keepass.view.ToolbarAction
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/entry_edit_bottom_bar"
|
android:id="@+id/bottom_toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="bottom"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" />
|
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner">
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<com.kunzisoft.keepass.view.ToolbarAction
|
||||||
android:id="@+id/entry_edit_validate"
|
android:id="@+id/entry_edit_bottom_bar"
|
||||||
style="@style/KeepassDXStyle.Fab"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_gravity="bottom"
|
||||||
android:contentDescription="@string/validate"
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
android:src="@drawable/ic_check_white_24dp"
|
|
||||||
app:fabCustomSize="@dimen/button_small_size"
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
app:layout_constraintTop_toTopOf="@+id/entry_edit_bottom_bar"
|
android:id="@+id/entry_edit_validate"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/entry_edit_bottom_bar"
|
style="@style/KeepassDXStyle.Fab"
|
||||||
android:layout_marginBottom="6dp"
|
android:layout_width="wrap_content"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
android:contentDescription="@string/validate"
|
||||||
|
android:src="@drawable/ic_check_white_24dp"
|
||||||
|
app:fabCustomSize="@dimen/button_small_size"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/entry_edit_bottom_bar"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/entry_edit_bottom_bar"
|
||||||
|
android:layout_marginBottom="6dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<include
|
||||||
|
layout="@layout/view_button_lock"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<include
|
<include
|
||||||
layout="@layout/view_button_lock"
|
app:layout_constraintTop_toBottomOf="@+id/bottom_toolbar"
|
||||||
android:layout_width="wrap_content"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
android:layout_height="wrap_content"
|
layout="@layout/view_screenshot_mode_banner" />
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"/>
|
|
||||||
|
|
||||||
<include layout="@layout/view_screenshot_mode_banner" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_collapseMode="parallax"
|
app:layout_collapseMode="parallax"
|
||||||
|
android:layoutDirection="ltr"
|
||||||
android:layout_gravity="center_horizontal|bottom"
|
android:layout_gravity="center_horizontal|bottom"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
|
|||||||
@@ -46,14 +46,15 @@
|
|||||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
||||||
|
|
||||||
<include
|
<include
|
||||||
layout="@layout/view_button_lock"
|
layout="@layout/view_button_lock"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
android:layout_gravity="bottom|start"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" />
|
app:layout_anchorGravity="bottom|start"
|
||||||
|
app:layout_dodgeInsetEdges="bottom" />
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
||||||
<include layout="@layout/view_screenshot_mode_banner"
|
<include layout="@layout/view_screenshot_mode_banner"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||||
|
|||||||
@@ -50,14 +50,14 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="@dimen/card_view_padding">
|
android:layout_margin="@dimen/card_view_padding">
|
||||||
|
|
||||||
<com.kunzisoft.keepass.view.PassKeyView
|
<com.kunzisoft.keepass.view.PasswordEditView
|
||||||
android:id="@+id/passphrase_view"
|
android:id="@+id/passphrase_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_toStartOf="@+id/passphrase_copy_button"
|
android:layout_toStartOf="@+id/passphrase_copy_button"
|
||||||
android:layout_toLeftOf="@+id/passphrase_copy_button"
|
android:layout_toLeftOf="@+id/passphrase_copy_button"
|
||||||
app:passKeyHint="@string/passphrase"
|
app:passwordHint="@string/passphrase"
|
||||||
app:passKeyMaxLines="7"/>
|
app:passwordMaxLines="7"/>
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
android:id="@+id/passphrase_copy_button"
|
android:id="@+id/passphrase_copy_button"
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="@dimen/card_view_padding">
|
android:layout_margin="@dimen/card_view_padding">
|
||||||
|
|
||||||
<com.kunzisoft.keepass.view.PassKeyView
|
<com.kunzisoft.keepass.view.PasswordEditView
|
||||||
android:id="@+id/password_view"
|
android:id="@+id/password_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|||||||
@@ -72,11 +72,11 @@
|
|||||||
android:text="@string/password"/>
|
android:text="@string/password"/>
|
||||||
|
|
||||||
<!-- Password Input -->
|
<!-- Password Input -->
|
||||||
<com.kunzisoft.keepass.view.PassKeyView
|
<com.kunzisoft.keepass.view.PasswordEditView
|
||||||
android:id="@+id/password_view"
|
android:id="@+id/password_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:passKeyVisible="false"/>
|
app:passwordVisible="false"/>
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:id="@+id/password_repeat_input_layout"
|
android:id="@+id/password_repeat_input_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -94,6 +94,7 @@
|
|||||||
android:importantForAccessibility="no"
|
android:importantForAccessibility="no"
|
||||||
android:importantForAutofill="no"
|
android:importantForAutofill="no"
|
||||||
android:inputType="textPassword|textMultiLine"
|
android:inputType="textPassword|textMultiLine"
|
||||||
|
android:textDirection="ltr"
|
||||||
android:maxLines="3"
|
android:maxLines="3"
|
||||||
android:hint="@string/hint_conf_pass"/>
|
android:hint="@string/hint_conf_pass"/>
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
@@ -121,6 +122,15 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/entry_keyfile"/>
|
android:text="@string/entry_keyfile"/>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
style="@style/KeepassDXStyle.Button.Secondary"
|
||||||
|
android:id="@+id/keyfile_generate"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
app:icon="@drawable/ic_file_key_white_24dp"
|
||||||
|
android:text="@string/generate_keyfile" />
|
||||||
|
|
||||||
<com.kunzisoft.keepass.view.KeyFileSelectionView
|
<com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||||
android:id="@+id/keyfile_selection"
|
android:id="@+id/keyfile_selection"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -125,8 +125,7 @@
|
|||||||
android:paddingStart="8dp"
|
android:paddingStart="8dp"
|
||||||
android:paddingLeft="8dp"
|
android:paddingLeft="8dp"
|
||||||
android:paddingEnd="4dp"
|
android:paddingEnd="4dp"
|
||||||
android:paddingRight="4dp"
|
android:paddingRight="4dp">
|
||||||
android:paddingVertical="4dp">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/node_otp_token"
|
android:id="@+id/node_otp_token"
|
||||||
@@ -179,4 +178,4 @@
|
|||||||
tools:text="Database / Group A / Group B" />
|
tools:text="Database / Group A / Group B" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
@@ -61,11 +61,11 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignTop="@+id/node_icon"
|
android:layout_alignTop="@+id/node_icon"
|
||||||
android:layout_marginEnd="-32dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginRight="-32dp"
|
android:layout_marginLeft="8dp"
|
||||||
android:layout_toStartOf="@+id/node_icon"
|
android:layout_alignParentLeft="true"
|
||||||
android:layout_toLeftOf="@+id/node_icon"
|
android:layout_alignParentStart="true"
|
||||||
tools:text="3" />
|
tools:text="123" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -1,91 +1,90 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<FrameLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?attr/colorSurface"
|
android:background="?attr/colorSurface"
|
||||||
android:gravity="bottom"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_marginBottom="6dp"
|
|
||||||
android:paddingTop="36dp"
|
|
||||||
android:paddingLeft="@dimen/default_margin"
|
|
||||||
android:paddingRight="@dimen/default_margin"
|
|
||||||
android:paddingBottom="@dimen/default_margin"
|
|
||||||
tools:ignore="UnusedAttribute">
|
tools:ignore="UnusedAttribute">
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
<TextView
|
|
||||||
android:id="@+id/nav_database_version"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
android:paddingStart="8dp"
|
|
||||||
android:paddingLeft="8dp"
|
|
||||||
android:paddingEnd="8dp"
|
|
||||||
android:paddingRight="8dp"
|
|
||||||
style="@style/KeepassDXStyle.Text.Info.OnSurface"
|
|
||||||
android:textSize="11sp"
|
|
||||||
tools:text="version"
|
|
||||||
android:textIsSelectable="true" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/nav_database_icon"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:contentDescription="@string/content_description_nav_header"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/nav_database_version"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/nav_database_name"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
android:layout_marginStart="6dp"
|
|
||||||
android:layout_marginLeft="6dp"
|
|
||||||
android:layout_marginBottom="12dp"
|
|
||||||
app:srcCompat="@drawable/ic_database_white_36dp"
|
|
||||||
style="@style/KeepassDXStyle.Icon"
|
|
||||||
app:tint="?attr/colorSecondary" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/nav_database_modified"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:contentDescription="@string/save"
|
|
||||||
android:src="@drawable/ic_modified_white_12dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/nav_database_icon"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/nav_database_icon"
|
|
||||||
app:layout_constraintTop_toTopOf="@+id/nav_database_icon" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/nav_database_color"
|
|
||||||
android:layout_width="18dp"
|
|
||||||
android:layout_height="18dp"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:contentDescription="@string/content_description_database_color"
|
|
||||||
android:src="@drawable/background_icon"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/nav_database_name"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/nav_database_name"
|
|
||||||
style="@style/KeepassDXStyle.Title.OnSurface"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="6dp"
|
|
||||||
android:maxLines="2"
|
|
||||||
android:text="@string/database"
|
|
||||||
android:textIsSelectable="true"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/nav_database_path"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/nav_database_color"
|
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/nav_database_path"
|
|
||||||
style="@style/KeepassDXStyle.Text.Info.OnSurface"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:gravity="bottom"
|
||||||
android:textSize="11sp"
|
android:layout_margin="@dimen/default_margin">
|
||||||
android:text="@string/path"
|
|
||||||
android:textIsSelectable="true"/>
|
<TextView
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
android:id="@+id/nav_database_version"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingLeft="8dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:paddingRight="8dp"
|
||||||
|
style="@style/KeepassDXStyle.Text.Info.OnSurface"
|
||||||
|
android:textSize="11sp"
|
||||||
|
tools:text="version"
|
||||||
|
android:textIsSelectable="true" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/nav_database_icon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/content_description_nav_header"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/nav_database_version"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/nav_database_name"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
android:layout_marginStart="6dp"
|
||||||
|
android:layout_marginLeft="6dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
app:srcCompat="@drawable/ic_database_white_36dp"
|
||||||
|
style="@style/KeepassDXStyle.Icon"
|
||||||
|
app:tint="?attr/colorSecondary" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/nav_database_modified"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/save"
|
||||||
|
android:src="@drawable/ic_modified_white_12dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/nav_database_icon"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/nav_database_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/nav_database_icon" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/nav_database_color"
|
||||||
|
android:layout_width="18dp"
|
||||||
|
android:layout_height="18dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:contentDescription="@string/content_description_database_color"
|
||||||
|
android:src="@drawable/background_icon"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/nav_database_name"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/nav_database_name"
|
||||||
|
style="@style/KeepassDXStyle.Title.OnSurface"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="6dp"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:text="@string/database"
|
||||||
|
android:textIsSelectable="true"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/nav_database_path"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/nav_database_color"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/nav_database_path"
|
||||||
|
style="@style/KeepassDXStyle.Text.Info.OnSurface"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:text="@string/path"
|
||||||
|
android:textIsSelectable="true"/>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</FrameLayout>
|
||||||
@@ -76,6 +76,7 @@
|
|||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layoutDirection="ltr"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/switch_element">
|
app:layout_constraintTop_toBottomOf="@+id/switch_element">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="center_vertical"
|
android:layout_gravity="center_vertical"
|
||||||
android:src="@drawable/ic_warning_white_24dp"
|
android:src="@drawable/ic_hourglass_bottom_white_24dp"
|
||||||
android:contentDescription="@string/content_description_file_information"
|
android:contentDescription="@string/content_description_file_information"
|
||||||
app:tint="?android:attr/textColor"/>
|
app:tint="?android:attr/textColor"/>
|
||||||
<androidx.appcompat.widget.AppCompatTextView
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
android:minHeight="48dp"
|
android:minHeight="48dp"
|
||||||
android:hint="@string/password"
|
android:hint="@string/password"
|
||||||
android:inputType="textPassword"
|
android:inputType="textPassword"
|
||||||
|
android:textDirection="ltr"
|
||||||
android:importantForAutofill="no"
|
android:importantForAutofill="no"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:focusableInTouchMode="true"
|
android:focusableInTouchMode="true"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:id="@+id/password_input_layout"
|
android:id="@+id/password_edit_input_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
tools:ignore="UnusedAttribute">
|
tools:ignore="UnusedAttribute">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/password_text"
|
android:id="@+id/password_edit_text"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:ems="10"
|
android:ems="10"
|
||||||
@@ -26,28 +26,27 @@
|
|||||||
android:importantForAccessibility="no"
|
android:importantForAccessibility="no"
|
||||||
android:importantForAutofill="no"
|
android:importantForAutofill="no"
|
||||||
android:inputType="textPassword|textMultiLine"
|
android:inputType="textPassword|textMultiLine"
|
||||||
|
android:textDirection="ltr"
|
||||||
android:maxLines="3"
|
android:maxLines="3"
|
||||||
tools:ignore="TextFields" />
|
tools:ignore="TextFields" />
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
android:id="@+id/password_strength_progress"
|
android:id="@+id/password_edit_strength_progress"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:padding="1dp"
|
||||||
app:trackCornerRadius="8dp"
|
app:trackCornerRadius="8dp"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/password_input_layout"/>
|
app:layout_constraintBottom_toBottomOf="@+id/password_edit_input_layout"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/password_entropy"
|
android:id="@+id/password_edit_entropy"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
tools:text="Entropy: 72.50 bit"
|
tools:text="Entropy: 72.50 bit"
|
||||||
android:textSize="11sp"
|
style="@style/KeepassDXStyle.Text.Indicator"
|
||||||
android:layout_margin="4dp"
|
android:padding="4dp"
|
||||||
android:textColor="?attr/colorSecondary"
|
app:layout_constraintBottom_toBottomOf="@+id/password_edit_input_layout"
|
||||||
android:textStyle="bold"
|
app:layout_constraintEnd_toEndOf="@+id/password_edit_input_layout" />
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/password_input_layout"
|
|
||||||
app:layout_constraintEnd_toEndOf="@+id/password_input_layout" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -18,39 +18,39 @@
|
|||||||
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
--><resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
--><resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||||
<string name="homepage">الصفحة الرئيسة</string>
|
<string name="homepage">الصفحة الرئيسة</string>
|
||||||
<string name="accept">قبول</string>
|
<string name="accept">اقبل</string>
|
||||||
<string name="add_group">إضافة مجموعة</string>
|
<string name="add_group">أضف مجموعة</string>
|
||||||
<string name="encryption">التشفير</string>
|
<string name="encryption">التعمية</string>
|
||||||
<string name="encryption_algorithm">خوارزمية التشفير</string>
|
<string name="encryption_algorithm">خوارزمية التعمية</string>
|
||||||
<string name="application">التطبيق</string>
|
<string name="application">التطبيق</string>
|
||||||
<string name="brackets">الأقواس</string>
|
<string name="brackets">الأقواس</string>
|
||||||
<string name="extended_ASCII">تمديد ASCII</string>
|
<string name="extended_ASCII">ASCII ممتد</string>
|
||||||
<string name="allow">سماح</string>
|
<string name="allow">اسمح</string>
|
||||||
<string name="clipboard_cleared">مُسِحت الحافظة</string>
|
<string name="clipboard_cleared">مُسِحت الحافظة</string>
|
||||||
<string name="clipboard_error_title">خطأ في الحافظة</string>
|
<string name="clipboard_error_title">خطأ في الحافظة</string>
|
||||||
<string name="clipboard_error_clear">تعذَّر مسح الحافظة</string>
|
<string name="clipboard_error_clear">تعذَّر مسح الحافظة</string>
|
||||||
<string name="database">قاعدة البيانات</string>
|
<string name="database">قاعدة البيانات</string>
|
||||||
<string name="decrypting_db">يفك تشفير محتوى قاعدة البيانات…</string>
|
<string name="decrypting_db">يفك تعمية محتوى قاعدة البيانات…</string>
|
||||||
<string name="digits">أرقام</string>
|
<string name="digits">أرقام</string>
|
||||||
<string name="entry_cancel">إلغاء</string>
|
<string name="entry_cancel">ألغِ</string>
|
||||||
<string name="entry_notes">ملاحظات</string>
|
<string name="entry_notes">ملاحظات</string>
|
||||||
<string name="entry_confpassword">تأكيد كلمة السر</string>
|
<string name="entry_confpassword">أكّد كلمة السر</string>
|
||||||
<string name="entry_created">أُنشئ</string>
|
<string name="entry_created">أُنشئ</string>
|
||||||
<string name="entry_modified">معدل</string>
|
<string name="entry_modified">مُعدل</string>
|
||||||
<string name="entry_not_found">تعذر العثور على بيانات المُدخلة.</string>
|
<string name="entry_not_found">تعذر العثور على بيانات المُدخلة.</string>
|
||||||
<string name="entry_password">كلمة السر</string>
|
<string name="entry_password">كلمة السر</string>
|
||||||
<string name="save">حفظ</string>
|
<string name="save">احفظ</string>
|
||||||
<string name="entry_title">العنوان</string>
|
<string name="entry_title">العنوان</string>
|
||||||
<string name="entry_url">رابط</string>
|
<string name="entry_url">رابط</string>
|
||||||
<string name="entry_user_name">اسم المستخدم</string>
|
<string name="entry_user_name">اسم المستخدم</string>
|
||||||
<string name="error_file_not_create">تعذر إنشاء الملف</string>
|
<string name="error_file_not_create">تعذر إنشاء الملف.</string>
|
||||||
<string name="error_invalid_path">تأكد أن المسار صحيح.</string>
|
<string name="error_invalid_path">تأكد أن المسار صحيح.</string>
|
||||||
<string name="error_no_name">ادخل اسمًا.</string>
|
<string name="error_no_name">ادخل اسمًا.</string>
|
||||||
<string name="error_pass_match">كلمتا السر غير متطابقتين.</string>
|
<string name="error_pass_match">كلمتا السر غير متطابقتين.</string>
|
||||||
<string name="field_name">اسم الحقل</string>
|
<string name="field_name">اسم الحقل</string>
|
||||||
<string name="field_value">قيمة الحقل</string>
|
<string name="field_value">قيمة الحقل</string>
|
||||||
<string name="generate_password">توليد كلمة سر</string>
|
<string name="generate_password">ولّد كلمة سر</string>
|
||||||
<string name="hint_conf_pass">تأكيد كلمة السر</string>
|
<string name="hint_conf_pass">أكّد كلمة السر</string>
|
||||||
<string name="hint_group_name">اسم المجموعة</string>
|
<string name="hint_group_name">اسم المجموعة</string>
|
||||||
<string name="hint_length">الطول</string>
|
<string name="hint_length">الطول</string>
|
||||||
<string name="hint_pass">كلمة السر</string>
|
<string name="hint_pass">كلمة السر</string>
|
||||||
@@ -61,15 +61,15 @@
|
|||||||
<string name="list_size_summary">حجم النص في قائمة العناصر</string>
|
<string name="list_size_summary">حجم النص في قائمة العناصر</string>
|
||||||
<string name="loading_database">يحمل قاعدة البيانات…</string>
|
<string name="loading_database">يحمل قاعدة البيانات…</string>
|
||||||
<string name="lowercase">حروف صغيرة</string>
|
<string name="lowercase">حروف صغيرة</string>
|
||||||
<string name="hide_password_summary">إخفاء كلمات السر بشكل افتراضي</string>
|
<string name="hide_password_summary">أخفِ كلمات السر (***) افتراضيًا</string>
|
||||||
<string name="about">عن التطبيق</string>
|
<string name="about">عن التطبيق</string>
|
||||||
<string name="menu_change_key_settings">تغيير المفتاح الرئيسي</string>
|
<string name="menu_change_key_settings">تغيير المفتاح الرئيسي</string>
|
||||||
<string name="settings">الإعدادات</string>
|
<string name="settings">الإعدادات</string>
|
||||||
<string name="menu_app_settings">إعدادات التطبيق</string>
|
<string name="menu_app_settings">إعدادات التطبيق</string>
|
||||||
<string name="menu_database_settings">إعدادات قاعدة البيانات</string>
|
<string name="menu_database_settings">إعدادات قاعدة البيانات</string>
|
||||||
<string name="menu_delete">حذف</string>
|
<string name="menu_delete">احذف</string>
|
||||||
<string name="menu_donate">التبرع</string>
|
<string name="menu_donate">التبرع</string>
|
||||||
<string name="menu_edit">تعديل</string>
|
<string name="menu_edit">عدّل</string>
|
||||||
<string name="menu_lock">اقفل قاعدة البيانات</string>
|
<string name="menu_lock">اقفل قاعدة البيانات</string>
|
||||||
<string name="menu_open">فتح</string>
|
<string name="menu_open">فتح</string>
|
||||||
<string name="menu_search">البحث</string>
|
<string name="menu_search">البحث</string>
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
<string name="progress_create">إنشاء قاعدة بيانات جديدة …</string>
|
<string name="progress_create">إنشاء قاعدة بيانات جديدة …</string>
|
||||||
<string name="protection">الحماية</string>
|
<string name="protection">الحماية</string>
|
||||||
<string name="read_only">محمي من التعديل</string>
|
<string name="read_only">محمي من التعديل</string>
|
||||||
<string name="content_description_remove_from_list">إزالة</string>
|
<string name="content_description_remove_from_list">أزل</string>
|
||||||
<string name="root">الجذر</string>
|
<string name="root">الجذر</string>
|
||||||
<string name="memory_usage">استخدام الذاكرة</string>
|
<string name="memory_usage">استخدام الذاكرة</string>
|
||||||
<string name="parallelism">التَّوازِي</string>
|
<string name="parallelism">التَّوازِي</string>
|
||||||
@@ -107,20 +107,19 @@
|
|||||||
<string name="feedback">أرسل انطباعاتك</string>
|
<string name="feedback">أرسل انطباعاتك</string>
|
||||||
<string name="about_description">\"KeePassDX\" هو تطبيق أندرويد لمدير كلمات المرور كي باس \"KeePass\"</string>
|
<string name="about_description">\"KeePassDX\" هو تطبيق أندرويد لمدير كلمات المرور كي باس \"KeePass\"</string>
|
||||||
<string name="add_entry">أضف مدخل</string>
|
<string name="add_entry">أضف مدخل</string>
|
||||||
<string name="edit_entry">تحرير مدخل</string>
|
<string name="edit_entry">عدّل مدخل</string>
|
||||||
<string name="key_derivation_function">وظيفة اشتقاق المفتاح</string>
|
<string name="key_derivation_function">وظيفة اشتقاق المفتاح</string>
|
||||||
<string name="app_timeout">المهلة</string>
|
<string name="app_timeout">المهلة</string>
|
||||||
<string name="app_timeout_summary">مدة الخمول قبل قفل قاعدة البيانات</string>
|
<string name="app_timeout_summary">مدة الخمول قبل قفل قاعدة البيانات</string>
|
||||||
<string name="file_manager_install_description">مدير الملفات الذي يمكنه القيام بالإجراءين ACTION_CREATE_DOCUMENT و ACTION_OPEN_DOCUMENT ضروري لانشاء, وفتح وحفض قواعد البيانات.</string>
|
<string name="file_manager_install_description">مدير الملفات الذي يمكنه القيام بالإجراءين ACTION_CREATE_DOCUMENT و ACTION_OPEN_DOCUMENT ضروري لانشاء، وفتح وحفظ قواعد البيانات.</string>
|
||||||
<string name="clipboard_error">بعض الأجهزة لا تسمح للتطبيقات باستعمال الحافظة.</string>
|
<string name="clipboard_error">بعض الأجهزة لا تسمح للتطبيقات باستعمال الحافظة.</string>
|
||||||
<string name="clipboard_timeout">مهلة الحافظة</string>
|
<string name="clipboard_timeout">مهلة الحافظة</string>
|
||||||
<string name="clipboard_timeout_summary">مدة التخزين في الحافظة(إذا كان جهازك يدعمها)</string>
|
<string name="clipboard_timeout_summary">مدة التخزين في الحافظة (إذا كان جهازك يدعمها)</string>
|
||||||
<string name="select_to_copy">اختر لنسخ %1$s إلى الحافظة</string>
|
<string name="select_to_copy">اختر لنسخ %1$s إلى الحافظة</string>
|
||||||
<string name="retrieving_db_key">يجلب مفتاح قاعدة البيانات…</string>
|
<string name="retrieving_db_key">يجلب مفتاح قاعدة البيانات…</string>
|
||||||
<string name="default_checkbox">استخدامها كقاعدة بيانات افتراضية</string>
|
<string name="default_checkbox">استخدامها كقاعدة بيانات افتراضية</string>
|
||||||
<string name="html_about_licence">KeePassDX © %1$d كونزيسوفت <strong>مفتوح المصدر</strong> و <strong>بدون اعلانات</strong>.
|
<string name="html_about_licence">KeePassDX © %1$d كونزيسوفت <strong>مفتوح المصدر</strong> و <strong>بدون إعلانات</strong>. \n يوزع كما هو، دون ضمان، تحت ترخيص <strong>GPLv3</strong>.</string>
|
||||||
\n يوزع كما هو، بدون ضمان, تحت ترخيص <strong>GPLv3</strong>.</string>
|
<string name="entry_accessed">وُصِل إليه</string>
|
||||||
<string name="entry_accessed">نُفذ إليه</string>
|
|
||||||
<string name="entry_expires">تنتهي صلاحيته في</string>
|
<string name="entry_expires">تنتهي صلاحيته في</string>
|
||||||
<string name="entry_keyfile">ملف المفتاح</string>
|
<string name="entry_keyfile">ملف المفتاح</string>
|
||||||
<string name="error_arc4">تشفير دفق Arcfour غير مدعوم.</string>
|
<string name="error_arc4">تشفير دفق Arcfour غير مدعوم.</string>
|
||||||
@@ -129,7 +128,7 @@
|
|||||||
<string name="error_nokeyfile">اختر ملف مفتاح.</string>
|
<string name="error_nokeyfile">اختر ملف مفتاح.</string>
|
||||||
<string name="error_out_of_memory">لا ذاكرة لتحميل قاعدة البيانات كاملة.</string>
|
<string name="error_out_of_memory">لا ذاكرة لتحميل قاعدة البيانات كاملة.</string>
|
||||||
<string name="error_load_database">تعذر تحميل قاعدة البيانات.</string>
|
<string name="error_load_database">تعذر تحميل قاعدة البيانات.</string>
|
||||||
<string name="error_load_database_KDF_memory">لا يمكن تحميل المفتاح، حاول تقليل \"الذاكرة المستخدمة\" من قبل KDF.</string>
|
<string name="error_load_database_KDF_memory">تعذر تحميل المفتاح. حاول تقليل \"الذاكرة المستخدمة\" من قِبل KDF.</string>
|
||||||
<string name="error_pass_gen_type">يجب تحديد نوع واحد على الأقل لتوليد كلمة السر.</string>
|
<string name="error_pass_gen_type">يجب تحديد نوع واحد على الأقل لتوليد كلمة السر.</string>
|
||||||
<string name="error_rounds_too_large">\"جولات التحويل\" كثيرة جداً. الإعداد إلى 2147483648.</string>
|
<string name="error_rounds_too_large">\"جولات التحويل\" كثيرة جداً. الإعداد إلى 2147483648.</string>
|
||||||
<string name="error_string_key">يجب أن يكون لكل سلسلة اسم حقل.</string>
|
<string name="error_string_key">يجب أن يكون لكل سلسلة اسم حقل.</string>
|
||||||
@@ -141,17 +140,17 @@
|
|||||||
<string name="invalid_db_sig">تعذر تمييز نسق قاعدة البيانات.</string>
|
<string name="invalid_db_sig">تعذر تمييز نسق قاعدة البيانات.</string>
|
||||||
<string name="keyfile_is_empty">ملف المفتاح فارغ.</string>
|
<string name="keyfile_is_empty">ملف المفتاح فارغ.</string>
|
||||||
<string name="list_entries_show_username_title">أظهر أسماء المستخدمين</string>
|
<string name="list_entries_show_username_title">أظهر أسماء المستخدمين</string>
|
||||||
<string name="list_entries_show_username_summary">اعرض اسماء المستخدمين في قوائم المدخلات</string>
|
<string name="list_entries_show_username_summary">يعرض اسماء المستخدمين في قوائم المدخلات</string>
|
||||||
<string name="hint_generated_password">كلمة السر الموَلدة</string>
|
<string name="hint_generated_password">كلمة السر مولّدة</string>
|
||||||
<string name="hint_keyfile">الملف المفتاحي</string>
|
<string name="hint_keyfile">ملف المفتاح</string>
|
||||||
<string name="hide_password_title">اخفاء كلمات السر</string>
|
<string name="hide_password_title">أخفِ كلمات السر</string>
|
||||||
<string name="copy_field">نُسخة من %1$s</string>
|
<string name="copy_field">نُسخة من %1$s</string>
|
||||||
<string name="menu_copy">نسخ</string>
|
<string name="menu_copy">نسخ</string>
|
||||||
<string name="menu_move">نقل</string>
|
<string name="menu_move">نقل</string>
|
||||||
<string name="menu_paste">لصق</string>
|
<string name="menu_paste">لصق</string>
|
||||||
<string name="menu_cancel">الغاء</string>
|
<string name="menu_cancel">ألغِ</string>
|
||||||
<string name="menu_hide_password">اخفاء كلمة السر</string>
|
<string name="menu_hide_password">أخفِ كلمة السر</string>
|
||||||
<string name="menu_showpass">اظهار كلمة السر</string>
|
<string name="menu_showpass">أظهر كلمة السر</string>
|
||||||
<string name="menu_url">الانتقال الى الرابط</string>
|
<string name="menu_url">الانتقال الى الرابط</string>
|
||||||
<string name="menu_file_selection_read_only">محمي من التعديل</string>
|
<string name="menu_file_selection_read_only">محمي من التعديل</string>
|
||||||
<string name="menu_open_file_read_and_write">قابل للتعديل</string>
|
<string name="menu_open_file_read_and_write">قابل للتعديل</string>
|
||||||
@@ -166,7 +165,7 @@
|
|||||||
<string name="unavailable">غير متوفر</string>
|
<string name="unavailable">غير متوفر</string>
|
||||||
<string name="menu_appearance_settings">المظهر</string>
|
<string name="menu_appearance_settings">المظهر</string>
|
||||||
<string name="general">عام</string>
|
<string name="general">عام</string>
|
||||||
<string name="autofill">ملأ تلقائي</string>
|
<string name="autofill">الملء التلقائي</string>
|
||||||
<string name="autofill_sign_in_prompt">سجل باستخدام KeePassDX</string>
|
<string name="autofill_sign_in_prompt">سجل باستخدام KeePassDX</string>
|
||||||
<string name="set_autofill_service_title">تعيين خدمة الملأ التلقائي الافتراضية</string>
|
<string name="set_autofill_service_title">تعيين خدمة الملأ التلقائي الافتراضية</string>
|
||||||
<string name="password_size_title">حجم كلمة السر المولدة</string>
|
<string name="password_size_title">حجم كلمة السر المولدة</string>
|
||||||
@@ -178,7 +177,7 @@
|
|||||||
<string name="clipboard_warning">اذا فشل الحذف التلقائي من الحافظة ,احذف تأريخه يدويا.</string>
|
<string name="clipboard_warning">اذا فشل الحذف التلقائي من الحافظة ,احذف تأريخه يدويا.</string>
|
||||||
<string name="lock_database_screen_off_title">قفل الشاشة</string>
|
<string name="lock_database_screen_off_title">قفل الشاشة</string>
|
||||||
<string name="lock_database_screen_off_summary">اقفل قاعدة البيانات بعد بضع ثوانٍ بمجرد إيقاف تشغيل الشاشة</string>
|
<string name="lock_database_screen_off_summary">اقفل قاعدة البيانات بعد بضع ثوانٍ بمجرد إيقاف تشغيل الشاشة</string>
|
||||||
<string name="biometric_delete_all_key_title">حذف مفاتيح التشفير</string>
|
<string name="biometric_delete_all_key_title">احذف مفاتيح التعمية</string>
|
||||||
<string name="unavailable_feature_text">لا يمكن بدأ هذه الميزة .</string>
|
<string name="unavailable_feature_text">لا يمكن بدأ هذه الميزة .</string>
|
||||||
<string name="unavailable_feature_version">هذا الجهاز يعمل بأندرويد %1$s لكن يحتاج نسخة %2$s على الأقل.</string>
|
<string name="unavailable_feature_version">هذا الجهاز يعمل بأندرويد %1$s لكن يحتاج نسخة %2$s على الأقل.</string>
|
||||||
<string name="file_name">اسم الملف</string>
|
<string name="file_name">اسم الملف</string>
|
||||||
@@ -234,10 +233,10 @@
|
|||||||
<string name="keyboard_notification_entry_content_title">%1$s متوفر على Magikeyboard</string>
|
<string name="keyboard_notification_entry_content_title">%1$s متوفر على Magikeyboard</string>
|
||||||
<string name="keyboard_notification_entry_content_text">%1$s</string>
|
<string name="keyboard_notification_entry_content_text">%1$s</string>
|
||||||
<string name="reset_education_screens_title">إعادة تعيين التلميحات التعليمية</string>
|
<string name="reset_education_screens_title">إعادة تعيين التلميحات التعليمية</string>
|
||||||
<string name="education_search_title">البحث من خلال الإدخالات</string>
|
<string name="education_search_title">ابحث من خلال المدخلات</string>
|
||||||
<string name="content_description_open_file">افتح الملف</string>
|
<string name="content_description_open_file">افتح الملف</string>
|
||||||
<string name="content_description_add_entry">إضافة مدخلة</string>
|
<string name="content_description_add_entry">أضف مدخل</string>
|
||||||
<string name="content_description_add_group">إضافة مجموعة</string>
|
<string name="content_description_add_group">أضف مجموعة</string>
|
||||||
<string name="content_description_file_information">معلومات الملف</string>
|
<string name="content_description_file_information">معلومات الملف</string>
|
||||||
<string name="entry_password_generator">مولد كلمة السر</string>
|
<string name="entry_password_generator">مولد كلمة السر</string>
|
||||||
<string name="content_description_background">الخلفية</string>
|
<string name="content_description_background">الخلفية</string>
|
||||||
@@ -250,50 +249,50 @@
|
|||||||
<string name="do_not_kill_app">لا تقتل التطبيق…</string>
|
<string name="do_not_kill_app">لا تقتل التطبيق…</string>
|
||||||
<string name="content_description_node_children">العقد الفرعية</string>
|
<string name="content_description_node_children">العقد الفرعية</string>
|
||||||
<string name="content_description_add_node">أضف عقدة</string>
|
<string name="content_description_add_node">أضف عقدة</string>
|
||||||
<string name="content_description_entry_icon">ايقونة المدخل</string>
|
<string name="content_description_entry_icon">أيقونة المدخل</string>
|
||||||
<string name="content_description_password_length">طول كلمة السر</string>
|
<string name="content_description_password_length">طول كلمة السر</string>
|
||||||
<string name="entry_add_field">أضف حقل</string>
|
<string name="entry_add_field">أضف حقل</string>
|
||||||
<string name="content_description_remove_field">أزل حقل</string>
|
<string name="content_description_remove_field">أزل حقل</string>
|
||||||
<string name="error_move_entry_here">يتعذر نقل مدخل إلى هنا.</string>
|
<string name="error_move_entry_here">لا يمكنك نقل مدخل هنا.</string>
|
||||||
<string name="error_copy_entry_here">يتعذر نسخ مدخال إلى هنا.</string>
|
<string name="error_copy_entry_here">لا يمكنك نسخ مدخل هنا.</string>
|
||||||
<string name="list_groups_show_number_entries_title">عرض عدد المدخلات</string>
|
<string name="list_groups_show_number_entries_title">أظهر عدد المدخلات</string>
|
||||||
<string name="list_groups_show_number_entries_summary">عرض عدد المدخلات في المجموعة</string>
|
<string name="list_groups_show_number_entries_summary">يعرض عدد المدخلات في المجموعة</string>
|
||||||
<string name="content_description_update_from_list">تحديث</string>
|
<string name="content_description_update_from_list">حدِّث</string>
|
||||||
<string name="content_description_keyboard_close_fields">أغلق الحقول</string>
|
<string name="content_description_keyboard_close_fields">أغلق الحقول</string>
|
||||||
<string name="error_create_database_file">لا يمكن انشاء قاعدة بيانات بكلمة السر وملف المفتاح الحاليين.</string>
|
<string name="error_create_database_file">تعذر إنشاء قاعدة بيانات بكلمة السر وملف المفتاح الحاليين.</string>
|
||||||
<string name="menu_advanced_unlock_settings">فك قفل الجهاز</string>
|
<string name="menu_advanced_unlock_settings">فك قفل الجهاز</string>
|
||||||
<string name="entry_attachments">مرفقات</string>
|
<string name="entry_attachments">مرفقات</string>
|
||||||
<string name="entry_history">السجل</string>
|
<string name="entry_history">التاريخ</string>
|
||||||
<string name="entry_add_attachment">أضف مرفقا</string>
|
<string name="entry_add_attachment">أضف مرفقًا</string>
|
||||||
<string name="discard">إلغاء</string>
|
<string name="discard">تجاهل</string>
|
||||||
<string name="discard_changes">تجاهل التغييرات؟</string>
|
<string name="discard_changes">تجاهل التغييرات؟</string>
|
||||||
<string name="validate">تأكيد</string>
|
<string name="validate">تأكيد</string>
|
||||||
<string name="security">الأمان</string>
|
<string name="security">الأمان</string>
|
||||||
<string name="master_key">المفتاح الرئيسي</string>
|
<string name="master_key">المفتاح الرئيسي</string>
|
||||||
<string name="error_otp_period">يجب ان تكون المدة بين %1$d و%2$d ثانية.</string>
|
<string name="error_otp_period">يجب ان تكون المدة بين %1$d و%2$d ثانية.</string>
|
||||||
<string name="error_otp_secret_key">المفتاح السري يجب ان يكون بصيغة Base32.</string>
|
<string name="error_otp_secret_key">المفتاح السري يجب ان يكون بصيغة Base32.</string>
|
||||||
<string name="error_save_database">لا يمكن حفظ قاعدة البيانات.</string>
|
<string name="error_save_database">تعذر حفظ قاعدة البيانات.</string>
|
||||||
<string name="error_create_database">لا يمكن إنشاء ملف قاعدة البيانات.</string>
|
<string name="error_create_database">تعذر إنشاء ملف قاعدة البيانات.</string>
|
||||||
<string name="error_copy_group_here">لا يمكن نسخ مجموعة هنا.</string>
|
<string name="error_copy_group_here">لا يمكنك نسخ مجموعة هنا.</string>
|
||||||
<string name="error_label_exists">هذه التسمية موجودة بالفعل.</string>
|
<string name="error_label_exists">هذه التسمية موجودة بالفعل.</string>
|
||||||
<string name="otp_period">المدة (ثواني)</string>
|
<string name="otp_period">المدة (ثواني)</string>
|
||||||
<string name="otp_algorithm">الخوارزمية</string>
|
<string name="otp_algorithm">الخوارزمية</string>
|
||||||
<string name="otp_digits">أرقام</string>
|
<string name="otp_digits">أرقام</string>
|
||||||
<string name="otp_counter">العداد</string>
|
<string name="otp_counter">العداد</string>
|
||||||
<string name="entry_setup_otp">عيّن كلمة مرور لمرة واحدة</string>
|
<string name="entry_setup_otp">عيّن كلمة سر لمرة واحدة</string>
|
||||||
<string name="entry_UUID">UUID</string>
|
<string name="entry_UUID">UUID</string>
|
||||||
<string name="html_about_contribution">من أجل <strong>حماية خصوصيتا</strong>٫<strong> إصلاح العلل</strong>٫ <strong>إضافة مميزات</strong> <strong>وجعلنا نشطاء دائما</strong>٫ نحن نعتمد على <strong>مساهمتك</strong>.</string>
|
<string name="html_about_contribution">لكي <strong>نحافظ على حريتنا</strong>، و<strong>نصلح الأخطاء</strong>، و<strong>نضيف ميزات</strong>، و<strong>نبقى دائمًا نشطين</strong>، فإننا نعتمد على <strong>مساهمتكم</strong>.</string>
|
||||||
<string name="content_description_keyfile_checkbox">خانة تأشير الملف المفتاحي</string>
|
<string name="content_description_keyfile_checkbox">خانة تأشير ملف المفتاح</string>
|
||||||
<string name="content_description_password_checkbox">خانة تأشير كلمة السر</string>
|
<string name="content_description_password_checkbox">خانة تأشير كلمة السر</string>
|
||||||
<string name="content_description_add_item">أضف عنصر</string>
|
<string name="content_description_add_item">أضف عنصر</string>
|
||||||
<string name="warning_permanently_delete_nodes">حذف العقد المحددة نهائيا؟</string>
|
<string name="warning_permanently_delete_nodes">حذف العقد المحددة نهائيا؟</string>
|
||||||
<string name="filter">مرشح</string>
|
<string name="filter">مرشح</string>
|
||||||
<string name="command_execution">ينفذ الأمر…</string>
|
<string name="command_execution">ينفذ الأمر…</string>
|
||||||
<string name="hide_broken_locations_title">اِخفي روابط قواعد البيانات المعطلة</string>
|
<string name="hide_broken_locations_title">أخفِ روابط قواعد البيانات المعطوبة</string>
|
||||||
<string name="show_recent_files_summary">أظهر موقع قواعد البيانات الأخيرة</string>
|
<string name="show_recent_files_summary">أظهر موقع قواعد البيانات الأخيرة</string>
|
||||||
<string name="show_recent_files_title">أظهر الملفات الأخيرة</string>
|
<string name="show_recent_files_title">أظهر الملفات الأخيرة</string>
|
||||||
<string name="remember_keyfile_locations_summary">تعقب موقع الملفات المفتاحية لقاعدة البيانات</string>
|
<string name="remember_keyfile_locations_summary">تعقب موقع الملفات المفتاحية لقاعدة البيانات</string>
|
||||||
<string name="remember_keyfile_locations_title">تذكر موقع الملف المفتاحي</string>
|
<string name="remember_keyfile_locations_title">تذكر موقع ملف المفتاح</string>
|
||||||
<string name="remember_database_locations_summary">تعقب موقع قاعدة البيانات</string>
|
<string name="remember_database_locations_summary">تعقب موقع قاعدة البيانات</string>
|
||||||
<string name="remember_database_locations_title">تذكر موقع تخزين قاعدة البيانات</string>
|
<string name="remember_database_locations_title">تذكر موقع تخزين قاعدة البيانات</string>
|
||||||
<string name="contains_duplicate_uuid_procedure">للمتابعة هل تريد حل المشكلة بتوليد UUID للعناصر المكررة ؟</string>
|
<string name="contains_duplicate_uuid_procedure">للمتابعة هل تريد حل المشكلة بتوليد UUID للعناصر المكررة ؟</string>
|
||||||
@@ -308,42 +307,42 @@
|
|||||||
<string name="creating_database">ينشئ قاعدة البيانات…</string>
|
<string name="creating_database">ينشئ قاعدة البيانات…</string>
|
||||||
<string name="error_string_type">لا يطابق هذا النص العنصر المطلوب.</string>
|
<string name="error_string_type">لا يطابق هذا النص العنصر المطلوب.</string>
|
||||||
<string name="error_otp_counter">على العداد أن يكون ما بين %1$d و %2$d.</string>
|
<string name="error_otp_counter">على العداد أن يكون ما بين %1$d و %2$d.</string>
|
||||||
<string name="entry_otp">كلمة مرور لمرة واحدة</string>
|
<string name="entry_otp">كلمة سر لمرة واحدة</string>
|
||||||
<string name="otp_type">نوع كلمة المرور لمرة واحدة</string>
|
<string name="otp_type">نوع كلمة السر لمرة واحدة (OTP)</string>
|
||||||
<string name="error_disallow_no_credentials">عين اعتماد واحد على الأقل.</string>
|
<string name="error_disallow_no_credentials">عين اعتماد واحد على الأقل.</string>
|
||||||
<string name="contribution">ساهم</string>
|
<string name="contribution">ساهم</string>
|
||||||
<string name="contact">الإتصال بنا</string>
|
<string name="contact">التواصل</string>
|
||||||
<string name="biometric">البصمة</string>
|
<string name="biometric">البصمة</string>
|
||||||
<string name="warning_empty_keyfile_explanation">يجب ألا تغير محتوى ملف المفتاح، في أحسن الحالات يجب أن يحتوي بيانات مولدة عشوائيا.</string>
|
<string name="warning_empty_keyfile_explanation">يجب ألا تغير محتوى ملف المفتاح، في أحسن الحالات يجب أن يحتوي بيانات مولدة عشوائيا.</string>
|
||||||
<string name="warning_empty_keyfile">من غير المستحسن اضافة ملف مفتاح فارغ.</string>
|
<string name="warning_empty_keyfile">من غير المستحسن اضافة ملف مفتاح فارغ.</string>
|
||||||
<string name="warning_sure_remove_data">أزل هذه البيانات عل أي حال؟</string>
|
<string name="warning_sure_remove_data">أزل هذه البيانات عل أي حال؟</string>
|
||||||
<string name="warning_sure_add_file">أأضف الملف على أي حال؟</string>
|
<string name="warning_sure_add_file">أأضف الملف على أي حال؟</string>
|
||||||
<string name="warning_replace_file">رفعُ هذا الملف سيستبدل الموجود مسبقا.</string>
|
<string name="warning_replace_file">رفع هذا الملف سيستبدل الموجود مسبقًا.</string>
|
||||||
<string name="warning_database_link_revoked">أبطلَ مدير الملفات الوصول للملف</string>
|
<string name="warning_database_link_revoked">أبطلَ مدير الملفات الوصول للملف</string>
|
||||||
<string name="warning_database_read_only">أعط صلاحية الكتابة من أجل حفظ قاعدة البيانات</string>
|
<string name="warning_database_read_only">أعط صلاحية الكتابة من أجل حفظ قاعدة البيانات</string>
|
||||||
<string name="warning_password_encoding">تجنب استخدام المحارف غير الموجودة في ترميز قاعدة البيانات (تحوَّل المحارف غير الموجودة لنفس الحرف).</string>
|
<string name="warning_password_encoding">تجنب استخدام المحارف غير الموجودة في ترميز قاعدة البيانات (تحوَّل المحارف غير الموجودة لنفس الحرف).</string>
|
||||||
<string name="hide_broken_locations_summary">إخف الروابط المعطلة في قائمة قواعد البيانات الحديثة</string>
|
<string name="hide_broken_locations_summary">أخفِ الروابط المعطوبة في قائمة قواعد البيانات الحديثة</string>
|
||||||
<string name="auto_focus_search_summary">افتح البحث عند فتح قاعدة البيانات</string>
|
<string name="auto_focus_search_summary">افتح البحث عند فتح قاعدة البيانات</string>
|
||||||
<string name="content_description_credentials_information">معلومات بيانات الاعتماد</string>
|
<string name="content_description_credentials_information">معلومات بيانات الاعتماد</string>
|
||||||
<string name="max_history_items_title">العدد الأقصى</string>
|
<string name="max_history_items_title">العدد الأقصى</string>
|
||||||
<string name="recycle_bin_group_title">مجموعة سلة المحذوفات</string>
|
<string name="recycle_bin_group_title">مجموعة سلة المحذوفات</string>
|
||||||
<string name="recycle_bin_summary">أُنقل المجموعات والمدخلات لسلة المحذوفات قبل حذفها</string>
|
<string name="recycle_bin_summary">أُنقل المجموعات والمدخلات لسلة المحذوفات قبل حذفها</string>
|
||||||
<string name="database_data_remove_unlinked_attachments_summary">أزِل المرفقات غير المرتبطة بإدخال في قاعدة البيانات</string>
|
<string name="database_data_remove_unlinked_attachments_summary">أزل المرفقات غير المرتبطة بمدخل في قاعدة البيانات</string>
|
||||||
<string name="database_data_remove_unlinked_attachments_title">أزل البيانات غير المرتبطة</string>
|
<string name="database_data_remove_unlinked_attachments_title">أزل البيانات غير المرتبطة</string>
|
||||||
<string name="database_data_compression_summary">ضغط البيانات يقلص من حجم قاعدة البيانات</string>
|
<string name="database_data_compression_summary">ضغط البيانات يقلص من حجم قاعدة البيانات</string>
|
||||||
<string name="database_data_compression_title">ضغط البيانات</string>
|
<string name="database_data_compression_title">ضغط البيانات</string>
|
||||||
<string name="data">البيانات</string>
|
<string name="data">البيانات</string>
|
||||||
<string name="unavailable_feature_hardware">تعذر العثور على ماسح البصمة.</string>
|
<string name="unavailable_feature_hardware">تعذر العثور على ماسح البصمة.</string>
|
||||||
<string name="biometric_delete_all_key_summary">احذف كل مفاتيح التشفير المرتبطة بفتح الجهاز</string>
|
<string name="biometric_delete_all_key_summary">احذف كل مفاتيح التعمية المرتبطة بفتح الجهاز</string>
|
||||||
<string name="advanced_unlock_explanation_summary">استخدم إلغاء القفل الجهاز لفتح قاعدة البيانات بسهولة</string>
|
<string name="advanced_unlock_explanation_summary">استخدم إلغاء القفل الجهاز لفتح قاعدة البيانات بسهولة</string>
|
||||||
<string name="lock_database_show_button_summary">يعرض زر القَفل في الواجهة</string>
|
<string name="lock_database_show_button_summary">يعرض زر القَفل في الواجهة</string>
|
||||||
<string name="lock_database_show_button_title">اعرض زر القَفل</string>
|
<string name="lock_database_show_button_title">أظهر زر القفل</string>
|
||||||
<string name="lock_database_back_root_summary">قفل قاعدة البيانات عند النقر على زر الرجوع في الشاشة الرئيسية</string>
|
<string name="lock_database_back_root_summary">قفل قاعدة البيانات عند النقر على زر الرجوع في الشاشة الرئيسية</string>
|
||||||
<string name="lock_database_back_root_title">اضغط على \"رجوع\" للإقفال</string>
|
<string name="lock_database_back_root_title">اضغط على \"رجوع\" للإقفال</string>
|
||||||
<string name="clipboard_explanation_summary">انسخ حقول الإدخال باستخدام الحافظة</string>
|
<string name="clipboard_explanation_summary">انسخ حقول المدخل باستخدام الحافظة</string>
|
||||||
<string name="database_opened">قاعدة البيانات مفتوحة</string>
|
<string name="database_opened">قاعدة البيانات مفتوحة</string>
|
||||||
<string name="autofill_preference_title">إعدادات الملء التلقائي</string>
|
<string name="autofill_preference_title">إعدادات الملء التلقائي</string>
|
||||||
<string name="education_entry_edit_title">حرر المدخلة</string>
|
<string name="education_entry_edit_title">عدّل المدخل</string>
|
||||||
<string name="education_advanced_unlock_summary">لفتح قاعدة البيانات بسرعة اربط كلمة المرور بالبصمة.</string>
|
<string name="education_advanced_unlock_summary">لفتح قاعدة البيانات بسرعة اربط كلمة المرور بالبصمة.</string>
|
||||||
<string name="education_search_summary">لإيجاد كلمة المرور، أدخل العنوان أو اسم المستخدم أو محتوى أحد الحقول.</string>
|
<string name="education_search_summary">لإيجاد كلمة المرور، أدخل العنوان أو اسم المستخدم أو محتوى أحد الحقول.</string>
|
||||||
<string name="education_new_node_summary">المدخلات لإدارة معرفاتك الرقمية.
|
<string name="education_new_node_summary">المدخلات لإدارة معرفاتك الرقمية.
|
||||||
@@ -357,15 +356,15 @@
|
|||||||
<string name="autofill_web_domain_blocklist_title">قائمة النطاقات المحظورة</string>
|
<string name="autofill_web_domain_blocklist_title">قائمة النطاقات المحظورة</string>
|
||||||
<string name="autofill_application_id_blocklist_summary">منع الملء التلقائي للتطبيقات الموجودة في القائمة</string>
|
<string name="autofill_application_id_blocklist_summary">منع الملء التلقائي للتطبيقات الموجودة في القائمة</string>
|
||||||
<string name="autofill_application_id_blocklist_title">قائمة التطبيقات المحظورة</string>
|
<string name="autofill_application_id_blocklist_title">قائمة التطبيقات المحظورة</string>
|
||||||
<string name="content_description_repeat_toggle_password_visibility">بدِّل ظهور كلمة السر</string>
|
<string name="content_description_repeat_toggle_password_visibility">أعد تبديل ظهور كلمة السر</string>
|
||||||
<string name="hide_expired_entries_summary">لن تعرض المدخلات منتهية الصلاحية</string>
|
<string name="hide_expired_entries_summary">لا يتم عرض المدخلات منتهية الصلاحية</string>
|
||||||
<string name="education_read_only_summary">تغيير وضع الافتتاح للجلسة.
|
<string name="education_read_only_summary">تغيير وضع الافتتاح للجلسة.
|
||||||
\n
|
\n
|
||||||
\nيمنع \"محمي ضد الكتابة\" التغييرات غير المقصودة في قاعدة البيانات.
|
\nيمنع \"محمي ضد الكتابة\" التغييرات غير المقصودة في قاعدة البيانات.
|
||||||
\n\"قابل للتعديل\" يتيح لك إضافة أو حذف أو تعديل جميع العناصر كما تريد.</string>
|
\n\"قابل للتعديل\" يتيح لك إضافة أو حذف أو تعديل جميع العناصر كما تريد.</string>
|
||||||
<string name="education_read_only_title">احمي قاعدة البيانات من التعديل</string>
|
<string name="education_read_only_title">احمي قاعدة البيانات من التعديل</string>
|
||||||
<string name="education_unlock_title">افتح قاعدة البيانات</string>
|
<string name="education_unlock_title">افتح قاعدة البيانات</string>
|
||||||
<string name="education_add_attachment_summary">أضف مرفقا للمدخلة لحفظ بيانات اضافية.</string>
|
<string name="education_add_attachment_summary">ارفع مرفقًا إلى مدخلك لحفظ البيانات الخارجية الهامة.</string>
|
||||||
<string name="education_add_attachment_title">أضف مرفقا</string>
|
<string name="education_add_attachment_title">أضف مرفقا</string>
|
||||||
<string name="autofill_block">احظر الملء التلقائي</string>
|
<string name="autofill_block">احظر الملء التلقائي</string>
|
||||||
<string name="keyboard_previous_database_credentials_title">شاشة بيانات اعتماد قاعدة البيانات</string>
|
<string name="keyboard_previous_database_credentials_title">شاشة بيانات اعتماد قاعدة البيانات</string>
|
||||||
@@ -402,7 +401,7 @@
|
|||||||
<string name="education_generate_password_title">أنشئ كلمة سر قوية</string>
|
<string name="education_generate_password_title">أنشئ كلمة سر قوية</string>
|
||||||
<string name="save_mode">وضع الحفظ</string>
|
<string name="save_mode">وضع الحفظ</string>
|
||||||
<string name="search_mode">وضع البحث</string>
|
<string name="search_mode">وضع البحث</string>
|
||||||
<string name="version">النسخة</string>
|
<string name="version">النُسخة</string>
|
||||||
<string name="template_group_name">النماذج</string>
|
<string name="template_group_name">النماذج</string>
|
||||||
<string name="holder">الحامل</string>
|
<string name="holder">الحامل</string>
|
||||||
<string name="number">الرقم</string>
|
<string name="number">الرقم</string>
|
||||||
@@ -410,7 +409,7 @@
|
|||||||
<string name="personal_identification_number">PIN</string>
|
<string name="personal_identification_number">PIN</string>
|
||||||
<string name="id_card">بطاقة الهوية</string>
|
<string name="id_card">بطاقة الهوية</string>
|
||||||
<string name="type">النوع</string>
|
<string name="type">النوع</string>
|
||||||
<string name="cryptocurrency">محفظة عملات مشفرة</string>
|
<string name="cryptocurrency">محفظة عملات التعموية</string>
|
||||||
<string name="public_key">المفتاح العمومي</string>
|
<string name="public_key">المفتاح العمومي</string>
|
||||||
<string name="private_key">المفتاح الخاص</string>
|
<string name="private_key">المفتاح الخاص</string>
|
||||||
<string name="account">الحساب</string>
|
<string name="account">الحساب</string>
|
||||||
@@ -418,20 +417,20 @@
|
|||||||
<string name="bank_name">اسم المصرف</string>
|
<string name="bank_name">اسم المصرف</string>
|
||||||
<string name="secure_note">ملاحظة آمنة</string>
|
<string name="secure_note">ملاحظة آمنة</string>
|
||||||
<string name="error_word_reserved">هذه الكلمة محجوزة ولا يمكن استخدامها.</string>
|
<string name="error_word_reserved">هذه الكلمة محجوزة ولا يمكن استخدامها.</string>
|
||||||
<string name="error_field_name_already_exists">اسم الحقل موجود سلفًا.</string>
|
<string name="error_field_name_already_exists">اسم الحقل موجود بالفعل.</string>
|
||||||
<string name="error_file_to_big">الملف الذي ترفعه كبير.</string>
|
<string name="error_file_to_big">الملف الذي تحاول رفعه كبير جدًا.</string>
|
||||||
<string name="error_upload_file">حدث خطأ أثناء رفع الملف.</string>
|
<string name="error_upload_file">حدث خطأ أثناء رفع الملف.</string>
|
||||||
<string name="error_duplicate_file">بيانات الملف موجودة سلفًا.</string>
|
<string name="error_duplicate_file">بيانات الملف موجودة بالفعل.</string>
|
||||||
<string name="error_remove_file">حدث خطأ أثناء إزالة بيانات الملف.</string>
|
<string name="error_remove_file">حدث خطأ أثناء إزالة بيانات الملف.</string>
|
||||||
<string name="error_start_database_action">حدث خطأ أثناء تنفيذ إجراء على قاعدة البيانات.</string>
|
<string name="error_start_database_action">حدث خطأ أثناء تنفيذ إجراء على قاعدة البيانات.</string>
|
||||||
<string name="content_description_otp_information">معلومات كلمة المرور لمرة واحدة</string>
|
<string name="content_description_otp_information">معلومات كلمة السر لمرة واحدة</string>
|
||||||
<string name="membership">العضوية</string>
|
<string name="membership">العضوية</string>
|
||||||
<string name="name">الاسم</string>
|
<string name="name">الاسم</string>
|
||||||
<string name="email">البريد الإلكتروني</string>
|
<string name="email">البريد الإلكتروني</string>
|
||||||
<string name="email_address">عنوان البريد الإلكتروني</string>
|
<string name="email_address">عنوان البريد الإلكتروني</string>
|
||||||
<string name="ssid">SSID</string>
|
<string name="ssid">SSID</string>
|
||||||
<string name="debit_credit_card">بطاقة السحب الفوري / الإئتمان</string>
|
<string name="debit_credit_card">بطاقة السحب الفوري / الإئتمان</string>
|
||||||
<string name="error_registration_read_only">لا يمكن حفظ عنصر في قاعدة بيانات مفتوحة للقراءة فقط</string>
|
<string name="error_registration_read_only">لا يمكن حفظ عنصر في قاعدة بيانات مفتوحة للقراءة فقط.</string>
|
||||||
<string name="otp_secret">الرمز السري</string>
|
<string name="otp_secret">الرمز السري</string>
|
||||||
<string name="place_of_issue">مكان المشكلة</string>
|
<string name="place_of_issue">مكان المشكلة</string>
|
||||||
<string name="date_of_issue">تاريخ المشكلة</string>
|
<string name="date_of_issue">تاريخ المشكلة</string>
|
||||||
@@ -452,8 +451,8 @@
|
|||||||
<string name="properties">الخصائص</string>
|
<string name="properties">الخصائص</string>
|
||||||
<string name="token">الرمز</string>
|
<string name="token">الرمز</string>
|
||||||
<string name="seed">البذرة</string>
|
<string name="seed">البذرة</string>
|
||||||
<string name="error_database_uri_null">يتعذر استرداد مسار قاعدة البيانات.</string>
|
<string name="error_database_uri_null">لا يمكن استرداد URI قاعدة البيانات.</string>
|
||||||
<string name="error_rebuild_list">يتعذر إعادة بناء القائمة بشكل صحيح.</string>
|
<string name="error_rebuild_list">تعذر إعادة بناء القائمة بشكل صحيح.</string>
|
||||||
<string name="menu_keystore_remove_key">احذف رمز فك القفل الجهاز</string>
|
<string name="menu_keystore_remove_key">احذف رمز فك القفل الجهاز</string>
|
||||||
<string name="menu_form_filling_settings">ملء النموذج</string>
|
<string name="menu_form_filling_settings">ملء النموذج</string>
|
||||||
<string name="menu_reload_database">أعد تحميل البيانات</string>
|
<string name="menu_reload_database">أعد تحميل البيانات</string>
|
||||||
@@ -463,8 +462,8 @@
|
|||||||
<string name="import_app_properties_summary">اختر ملفًا لاستيراد إعدادات التطبيق</string>
|
<string name="import_app_properties_summary">اختر ملفًا لاستيراد إعدادات التطبيق</string>
|
||||||
<string name="export_app_properties_title">صدّر إعدادات التطبيق</string>
|
<string name="export_app_properties_title">صدّر إعدادات التطبيق</string>
|
||||||
<string name="export_app_properties_summary">أنشئ ملفًا لتصدير إعدادات التطبيق</string>
|
<string name="export_app_properties_summary">أنشئ ملفًا لتصدير إعدادات التطبيق</string>
|
||||||
<string name="error_import_app_properties">خطأ أثناء استيراد إعدادات التطبيق</string>
|
<string name="error_import_app_properties">خطأ أثناء استيراد إعدادات التطبيق.</string>
|
||||||
<string name="error_export_app_properties">خطأ أثناء تصدير إعدادات التطبيق</string>
|
<string name="error_export_app_properties">خطأ أثناء تصدير إعدادات التطبيق.</string>
|
||||||
<string name="warning_database_info_changed">غُيِّرت معلومات قاعدة البيانات من خارج هذا التطبيق.</string>
|
<string name="warning_database_info_changed">غُيِّرت معلومات قاعدة البيانات من خارج هذا التطبيق.</string>
|
||||||
<string name="warning_database_info_changed_options">ادمج البيانات أو استبدل التعديلات الخارجية بحفظ قاعدة البيانات أو أعد تحميلها لجلب آخر التغييرات.</string>
|
<string name="warning_database_info_changed_options">ادمج البيانات أو استبدل التعديلات الخارجية بحفظ قاعدة البيانات أو أعد تحميلها لجلب آخر التغييرات.</string>
|
||||||
<string name="credential_before_click_advanced_unlock_button">اكتب كلمة السر، وأنقر هذا الزر.</string>
|
<string name="credential_before_click_advanced_unlock_button">اكتب كلمة السر، وأنقر هذا الزر.</string>
|
||||||
@@ -482,7 +481,7 @@
|
|||||||
<string name="autofill_ask_to_save_data_title">اسأل لحفظ البيانات</string>
|
<string name="autofill_ask_to_save_data_title">اسأل لحفظ البيانات</string>
|
||||||
<string name="content_description_database_color">لون قاعدة البيانات</string>
|
<string name="content_description_database_color">لون قاعدة البيانات</string>
|
||||||
<string name="menu_merge_from">ادمج من…</string>
|
<string name="menu_merge_from">ادمج من…</string>
|
||||||
<string name="show_uuid_summary">يعرض \"المعرف العام\" المرتبط بمُدخل او بمجموعة</string>
|
<string name="show_uuid_summary">يعرض UUID المرتبط بمدخل أو بمجموعة</string>
|
||||||
<string name="expired">انتهت</string>
|
<string name="expired">انتهت</string>
|
||||||
<string name="tags">الوسوم</string>
|
<string name="tags">الوسوم</string>
|
||||||
<string name="menu_merge_database">ادمج البيانات</string>
|
<string name="menu_merge_database">ادمج البيانات</string>
|
||||||
@@ -491,10 +490,10 @@
|
|||||||
<string name="warning_file_too_big">يفترض بقاعدة البيانات أن تحوي ملفات صغيرة الحجم ( كمفاتيح PGP).
|
<string name="warning_file_too_big">يفترض بقاعدة البيانات أن تحوي ملفات صغيرة الحجم ( كمفاتيح PGP).
|
||||||
\n
|
\n
|
||||||
\nبرفع هذا الملف قد يزداد حجم قاعدة البيانات ويضعف أداءها.</string>
|
\nبرفع هذا الملف قد يزداد حجم قاعدة البيانات ويضعف أداءها.</string>
|
||||||
<string name="error_move_group_here">يتعذر نقل المجموعة إلى هنا.</string>
|
<string name="error_move_group_here">لا يمكنك نقل مجموعة هنا.</string>
|
||||||
<string name="menu_save_copy_to">احفظ نسخة إلى…</string>
|
<string name="menu_save_copy_to">احفظ نسخة إلى…</string>
|
||||||
<string name="searchable">يمكن البحث عنه</string>
|
<string name="searchable">يمكن البحث عنه</string>
|
||||||
<string name="custom_data">بيانات مخصصة</string>
|
<string name="custom_data">بيانات مخصّصة</string>
|
||||||
<string name="case_sensitive">حساسة لحالة الأحرف</string>
|
<string name="case_sensitive">حساسة لحالة الأحرف</string>
|
||||||
<string name="regex">تعابير نمطية</string>
|
<string name="regex">تعابير نمطية</string>
|
||||||
<string name="enable_keep_screen_on_title">أبقِ الشاشة شغّالة</string>
|
<string name="enable_keep_screen_on_title">أبقِ الشاشة شغّالة</string>
|
||||||
@@ -512,18 +511,18 @@
|
|||||||
<string name="templates_group_uuid_title">مجموعة القوالب</string>
|
<string name="templates_group_uuid_title">مجموعة القوالب</string>
|
||||||
<string name="advanced_unlock_timeout">انتهت مهلة فتح الجهاز</string>
|
<string name="advanced_unlock_timeout">انتهت مهلة فتح الجهاز</string>
|
||||||
<string name="temp_advanced_unlock_timeout_summary">مهلة استخدام فتح الجهاز قبل حذف محتواها</string>
|
<string name="temp_advanced_unlock_timeout_summary">مهلة استخدام فتح الجهاز قبل حذف محتواها</string>
|
||||||
<string name="advanced_unlock_delete_all_key_warning">أتريد حذف كل مفاتيح التشفير المرتبطة بفتح الجهاز؟</string>
|
<string name="advanced_unlock_delete_all_key_warning">أتريد حذف كل مفاتيح التعمية المرتبطة بفتح الجهاز؟</string>
|
||||||
<string name="templates">القوالب</string>
|
<string name="templates">القوالب</string>
|
||||||
<string name="templates_group_enable_title">استخدام القوالب</string>
|
<string name="templates_group_enable_title">استخدام القوالب</string>
|
||||||
<string name="notification">الإشعارات</string>
|
<string name="notification">الإشعارات</string>
|
||||||
<string name="temp_advanced_unlock_enable_summary">لا تقم بتخزين أي محتوى مشفر لاستخدام إلغاء قفل الجهاز</string>
|
<string name="temp_advanced_unlock_enable_summary">لا تقم بتخزين أي محتوى مشفر لاستخدام إلغاء قفل الجهاز</string>
|
||||||
<string name="temp_advanced_unlock_timeout_title">انتهاء صلاحية فتح الحهاز</string>
|
<string name="temp_advanced_unlock_timeout_title">انتهاء صلاحية فتح الحهاز</string>
|
||||||
<string name="hide_expired_entries_title">إخفاء الإدخالات منتهية الصلاحية</string>
|
<string name="hide_expired_entries_title">أخفِ المدخلات منتهية الصلاحية</string>
|
||||||
<string name="content_description_hardware_key_checkbox">خانة إختيار مفتاح الجهاز</string>
|
<string name="content_description_hardware_key_checkbox">خانة إختيار مفتاح العتاد</string>
|
||||||
<string name="content_description_passphrase_word_count">عدد عبارات المرور</string>
|
<string name="content_description_passphrase_word_count">عدد عبارات المرور</string>
|
||||||
<string name="content_description_entry_background_color">لون خلفية المدخل</string>
|
<string name="content_description_entry_background_color">لون خلفية المدخل</string>
|
||||||
<string name="passphrase">عبارة المرور</string>
|
<string name="passphrase">عبارة السر</string>
|
||||||
<string name="colorize_password_title">تلوين كلمات المرور</string>
|
<string name="colorize_password_title">لوّن كلمات السر</string>
|
||||||
<string name="permission">الإذن</string>
|
<string name="permission">الإذن</string>
|
||||||
<string name="advanced_unlock_prompt_not_initialized">تعذر تهيئة موجه إلغاء قفل الجهاز.</string>
|
<string name="advanced_unlock_prompt_not_initialized">تعذر تهيئة موجه إلغاء قفل الجهاز.</string>
|
||||||
<string name="biometric_security_update_required">مطلوب تحديث أمان المقاييس الحيوية.</string>
|
<string name="biometric_security_update_required">مطلوب تحديث أمان المقاييس الحيوية.</string>
|
||||||
@@ -536,12 +535,12 @@
|
|||||||
\nاستخدم طريقة ملء النموذج التي تفضلها.</string>
|
\nاستخدم طريقة ملء النموذج التي تفضلها.</string>
|
||||||
<string name="html_text_dev_feature_work_hard">نحن نعمل بجد لإصدار هذه الميزة بسرعة.</string>
|
<string name="html_text_dev_feature_work_hard">نحن نعمل بجد لإصدار هذه الميزة بسرعة.</string>
|
||||||
<string name="autofill_inline_suggestions_summary">حاول عرض اقتراحات الملء التلقائي مباشرة من لوحة مفاتيح متوافقة</string>
|
<string name="autofill_inline_suggestions_summary">حاول عرض اقتراحات الملء التلقائي مباشرة من لوحة مفاتيح متوافقة</string>
|
||||||
<string name="delete_entered_password_summary">يحذف كلمة المرور التي تم إدخالها بعد محاولة الاتصال بقاعدة البيانات</string>
|
<string name="delete_entered_password_summary">يحذف كلمة السر التي أُدخلت بعد محاولة الاتصال بقاعدة البيانات</string>
|
||||||
<string name="education_lock_summary">اقفل قاعدةبياناتك بسرعة، يمكنك إعداد التطبيق لقفلها بعد فترة، وعند إيقاف تشغيل الشاشة.</string>
|
<string name="education_lock_summary">اقفل قاعدةبياناتك بسرعة، يمكنك إعداد التطبيق لقفلها بعد فترة، وعند إيقاف تشغيل الشاشة.</string>
|
||||||
<string name="education_sort_title">فرز العنصر</string>
|
<string name="education_sort_title">فرز العنصر</string>
|
||||||
<string name="contribute">ساهِم</string>
|
<string name="contribute">ساهِم</string>
|
||||||
<string name="upload_attachment">رفع %1$s</string>
|
<string name="upload_attachment">ارفع %1$s</string>
|
||||||
<string name="download_canceled">ألغيت!</string>
|
<string name="download_canceled">أُلغِيَ!</string>
|
||||||
<string name="unit_kibibyte">كيلو بايت</string>
|
<string name="unit_kibibyte">كيلو بايت</string>
|
||||||
<string name="unit_mebibyte">ميغا بايت</string>
|
<string name="unit_mebibyte">ميغا بايت</string>
|
||||||
<string name="unit_gibibyte">جيجابت</string>
|
<string name="unit_gibibyte">جيجابت</string>
|
||||||
@@ -557,7 +556,7 @@
|
|||||||
<string name="title_case">حالة العنوان</string>
|
<string name="title_case">حالة العنوان</string>
|
||||||
<string name="character_count">عدد الأحرف: %1$d</string>
|
<string name="character_count">عدد الأحرف: %1$d</string>
|
||||||
<string name="style_choose_summary">السمة المستخدمة في التطبيق</string>
|
<string name="style_choose_summary">السمة المستخدمة في التطبيق</string>
|
||||||
<string name="show_entry_colors_summary">يعرض ألوان المقدمة والخلفية لإدخال</string>
|
<string name="show_entry_colors_summary">يعرض ألوان المقدمة والخلفية للمدخل</string>
|
||||||
<string name="icon_pack_choose_summary">حزمة الأيقونات المستخدمة في التطبيق</string>
|
<string name="icon_pack_choose_summary">حزمة الأيقونات المستخدمة في التطبيق</string>
|
||||||
<string name="show_entry_colors_title">ألوان الدخول</string>
|
<string name="show_entry_colors_title">ألوان الدخول</string>
|
||||||
<string name="device_credential_unlock_enable_title">فتح بيانات اعتماد الجهاز</string>
|
<string name="device_credential_unlock_enable_title">فتح بيانات اعتماد الجهاز</string>
|
||||||
@@ -574,15 +573,15 @@
|
|||||||
<string name="keyboard_previous_fill_in_summary">العودة تلقائيًا إلى لوحة المفاتيح السابقة بعد تنفيذ \"إجراء المفتاح التلقائي\"</string>
|
<string name="keyboard_previous_fill_in_summary">العودة تلقائيًا إلى لوحة المفاتيح السابقة بعد تنفيذ \"إجراء المفتاح التلقائي\"</string>
|
||||||
<string name="download_attachment">تثبيت %1$s</string>
|
<string name="download_attachment">تثبيت %1$s</string>
|
||||||
<string name="html_about_privacy"><strong> لا يتم استرداد أي بيانات مستخدم</strong>، هذا التطبيق لا يتصل بأي خادم، ويعمل محليًا فقط ويحترم خصوصية المستخدمين تمامًا.</string>
|
<string name="html_about_privacy"><strong> لا يتم استرداد أي بيانات مستخدم</strong>، هذا التطبيق لا يتصل بأي خادم، ويعمل محليًا فقط ويحترم خصوصية المستخدمين تمامًا.</string>
|
||||||
<string name="error_cancel_by_user">ألغى المستخدم.</string>
|
<string name="error_cancel_by_user">أُلغِيَ بواسطة المستخدم.</string>
|
||||||
<string name="show_otp_token_title">إظهار رمز \"الاقتران لمرة واحدة\" OTP</string>
|
<string name="show_otp_token_title">أظهر رمز OTP</string>
|
||||||
<string name="show_otp_token_summary">إظهار رموز\"الاقتران لمرة واحدة\" في قائمة المدخلات</string>
|
<string name="show_otp_token_summary">يعرض رموز OTP في قائمة المدخلات</string>
|
||||||
<string name="warning_database_already_opened">قاعدة البيانات مفتوحة بالفعل، أغلقها أولاً لفتح قاعدة البيانات الجديدة</string>
|
<string name="warning_database_already_opened">قاعدة البيانات مفتوحة بالفعل، أغلقها أولاً لفتح قاعدة البيانات الجديدة</string>
|
||||||
<string name="warning_database_info_reloaded">ستؤدي إعادة تحميل قاعدة البيانات إلى حذف البيانات المعدلة محليًا.</string>
|
<string name="warning_database_info_reloaded">ستؤدي إعادة تحميل قاعدة البيانات إلى حذف البيانات المعدلة محليًا.</string>
|
||||||
<string name="templates_group_enable_summary">استخدم القوالب الديناميكية لملء حقول الإدخال</string>
|
<string name="templates_group_enable_summary">استخدم القوالب الديناميكية لملء حقول المدخل</string>
|
||||||
<string name="keyboard_auto_go_action_summary">إجراء مفتاح \"Go\" بعد الضغط على مفتاح \"Field\"</string>
|
<string name="keyboard_auto_go_action_summary">إجراء مفتاح \"Go\" بعد الضغط على مفتاح \"Field\"</string>
|
||||||
<string name="allow_no_password_summary">يسمح بالنقر فوق الزر \"فتح\" إذا لم يتم تحديد بيانات اعتماد</string>
|
<string name="allow_no_password_summary">يسمح بالنقر فوق الزر \"فتح\" إذا لم يتم تحديد بيانات اعتماد</string>
|
||||||
<string name="education_generate_password_summary">أنشئ كلمة مرور قوية لربطها بإدخالك، وحددها بسهولة وفقًا لمعايير النموذج ولا تنس كلمة المرور الآمنة.</string>
|
<string name="education_generate_password_summary">أنشئ كلمة سر قوية لربطها بإدخالك، وحددها بسهولة وفقًا لمعايير النموذج ولا تنسَ كلمة السر الآمنة.</string>
|
||||||
<string name="education_setup_OTP_title">قم بإعداد OTP</string>
|
<string name="education_setup_OTP_title">قم بإعداد OTP</string>
|
||||||
<string name="style_brightness_title">سطوع السمة</string>
|
<string name="style_brightness_title">سطوع السمة</string>
|
||||||
<string name="word_separator">الفاصل</string>
|
<string name="word_separator">الفاصل</string>
|
||||||
@@ -597,43 +596,43 @@
|
|||||||
<string name="kdf_explanation">لإنشاء مفتاح خوارزمية التشفير، يتحول المفتاح الرئيسي باستخدام وظيفة اشتقاق مفتاح مملح عشوائيًا.</string>
|
<string name="kdf_explanation">لإنشاء مفتاح خوارزمية التشفير، يتحول المفتاح الرئيسي باستخدام وظيفة اشتقاق مفتاح مملح عشوائيًا.</string>
|
||||||
<string name="html_text_dev_feature_buy_pro">بشراء الإصدار <strong> pro </strong>،</string>
|
<string name="html_text_dev_feature_buy_pro">بشراء الإصدار <strong> pro </strong>،</string>
|
||||||
<string name="auto_type">كتابة تلقائيًا</string>
|
<string name="auto_type">كتابة تلقائيًا</string>
|
||||||
<string name="hardware_key">مفتاح الجهاز</string>
|
<string name="hardware_key">مفتاح العتاد</string>
|
||||||
<string name="advanced_unlock_prompt_store_credential_title">رابط لفتح الجهاز</string>
|
<string name="advanced_unlock_prompt_store_credential_title">رابط لفتح الجهاز</string>
|
||||||
<string name="backspace">فراغ للخلف</string>
|
<string name="backspace">فراغ للخلف</string>
|
||||||
<string name="enter">دخول</string>
|
<string name="enter">دخول</string>
|
||||||
<string name="education_sort_summary">اختر كيفية فرز الإدخالات والمجموعات.</string>
|
<string name="education_sort_summary">اختر كيفية فرز المدخلات والمجموعات.</string>
|
||||||
<string name="html_text_feature_generosity">هذا <strong> النمط المرئي</strong> متاح بفضل كرمك.</string>
|
<string name="html_text_feature_generosity">هذا <strong> النمط المرئي</strong> متاح بفضل كرمك.</string>
|
||||||
<string name="info">المعلومات</string>
|
<string name="info">المعلومات</string>
|
||||||
<string name="waiting_challenge_response">في انتظار استجابة التحدي…</string>
|
<string name="waiting_challenge_response">في انتظار استجابة التحدي…</string>
|
||||||
<string name="bank_identifier_code">SWIFT / BIC</string>
|
<string name="bank_identifier_code">SWIFT / BIC</string>
|
||||||
<string name="international_bank_account_number">IBAN</string>
|
<string name="international_bank_account_number">IBAN</string>
|
||||||
<string name="error_no_hardware_key">حدد مفتاح الجهاز.</string>
|
<string name="error_no_hardware_key">حدّد مفتاح العتاد.</string>
|
||||||
<string name="colorize_password_summary">تلوين أحرف كلمة المرور حسب النوع</string>
|
<string name="colorize_password_summary">لوّن أحرف كلمة السر حسب النوع</string>
|
||||||
<string name="enable_keep_screen_on_summary">استمر في تشغيل الشاشة عند مشاهدة إدخال أو تعديله</string>
|
<string name="enable_keep_screen_on_summary">استمر في تشغيل الشاشة عند مشاهدة مدخل أو تعديله</string>
|
||||||
<string name="enable_screenshot_mode_title">وضع لقطة الشاشة</string>
|
<string name="enable_screenshot_mode_title">وضع لقطة الشاشة</string>
|
||||||
<string name="navigation_drawer_open">درج التنقل مفتوح</string>
|
<string name="navigation_drawer_open">درج التنقل مفتوح</string>
|
||||||
<string name="waiting_challenge_request">في انتظار طلب التحدي…</string>
|
<string name="waiting_challenge_request">في انتظار طلب التحدي…</string>
|
||||||
<string name="navigation_drawer_close">درج التنقل مقفول</string>
|
<string name="navigation_drawer_close">درج التنقل مقفول</string>
|
||||||
<string name="error_XML_malformed">XML تالف.</string>
|
<string name="error_XML_malformed">XML تالف.</string>
|
||||||
<string name="error_otp_type">لم يتم التعرف على نوع OTP الحالي من خلال هذا النموذج، وقد لا يؤدي التحقق من صحته إلى إنشاء الرمز المميز بشكل صحيح.</string>
|
<string name="error_otp_type">لم يتم التعرف على نوع OTP الحالي من خلال هذا النموذج، وقد لا يؤدي التحقق من صحته إلى إنشاء الرمز المميز بشكل صحيح.</string>
|
||||||
<string name="error_challenge_already_requested">التحدي مطلوب بالفعل</string>
|
<string name="error_challenge_already_requested">التحدي طُلَب بالفعل.</string>
|
||||||
<string name="error_response_already_provided">تقدم الرد بالفعل.</string>
|
<string name="error_response_already_provided">تقدم الرد بالفعل.</string>
|
||||||
<string name="error_no_response_from_challenge">غير قادر على الحصول على رد من التحدي.</string>
|
<string name="error_no_response_from_challenge">غير قادر على الحصول على رد من التحدي.</string>
|
||||||
<string name="error_driver_required">مطلوب تعريف لـ%1$s.</string>
|
<string name="error_driver_required">مطلوب تعريف لـ%1$s.</string>
|
||||||
<string name="error_unable_merge_database_kdb">تعذر الدمج من قاعدة بيانات V1.</string>
|
<string name="error_unable_merge_database_kdb">غير قادر على الدمج مع ملف قاعدة بيانات kdb.</string>
|
||||||
<string name="error_location_unknown">موقع قاعدة البيانات غير معروف، لا يمكن تنفيذ إجراء قاعدة البيانات.</string>
|
<string name="error_location_unknown">موقع قاعدة البيانات غير معروف، لا يمكن تنفيذ إجراء قاعدة البيانات.</string>
|
||||||
<string name="menu_advanced_unlock_settings_summary">القياس الحيوي، بيانات اعتماد الجهاز</string>
|
<string name="menu_advanced_unlock_settings_summary">القياس الحيوي، بيانات اعتماد الجهاز</string>
|
||||||
<string name="menu_database_settings_summary">البيانات الوصفية، سلة المحذوفات، القوالب، التاريخ</string>
|
<string name="menu_database_settings_summary">البيانات الوصفية، سلة المحذوفات، القوالب، التاريخ</string>
|
||||||
<string name="menu_security_settings_summary">التشفير، وظيفة اشتقاق المفتاح</string>
|
<string name="menu_security_settings_summary">التشفير، وظيفة اشتقاق المفتاح</string>
|
||||||
<string name="error_hardware_key_unsupported">مفتاح الجهاز غير مدعوم.</string>
|
<string name="error_hardware_key_unsupported">مفتاح العتاد غير مدعوم.</string>
|
||||||
<string name="master_key_settings_summary">التغيير والتجديد</string>
|
<string name="master_key_settings_summary">التغيير والتجديد</string>
|
||||||
<string name="error_empty_key">لا يمكن أن يكون المفتاح فارغًا.</string>
|
<string name="error_empty_key">لا يمكن أن يكون المفتاح فارغًا.</string>
|
||||||
<string name="corrupted_file">ملف تالف.</string>
|
<string name="corrupted_file">ملف تالف.</string>
|
||||||
<string name="warning_keyfile_integrity">لا يتم ضمان تجزئة الملف لأن Android يمكنه تغيير بياناته بسرعة. قم بتغيير امتداد الملف إلى bin. من أجل التكامل الصحيح.</string>
|
<string name="warning_keyfile_integrity">لا يتم ضمان تجزئة الملف لأن Android يمكنه تغيير بياناته بسرعة. قم بتغيير امتداد الملف إلى bin. من أجل التكامل الصحيح.</string>
|
||||||
<string name="invalid_db_same_uuid">%1$s بنفس UUID %2$s موجود بالفعل.</string>
|
<string name="invalid_db_same_uuid">%1$s بنفس UUID %2$s موجود بالفعل.</string>
|
||||||
<string name="remember_hardware_key_title">تذكر مفاتيح الأجهزة</string>
|
<string name="remember_hardware_key_title">تذكر مفاتيح العتاد</string>
|
||||||
<string name="warning_exact_alarm">لم تسمح للتطبيق باستخدام منبه دقيق. نتيجة لذلك، لن يتم تنفيذ الميزات التي تتطلب مؤقتًا في وقت محدد.</string>
|
<string name="warning_exact_alarm">لم تسمح للتطبيق باستخدام منبه دقيق. نتيجة لذلك، لن يتم تنفيذ الميزات التي تتطلب مؤقتًا في وقت محدد.</string>
|
||||||
<string name="remember_hardware_key_summary">يتتبع مفاتيح الأجهزة المستخدمة</string>
|
<string name="remember_hardware_key_summary">يتتبع مفاتيح العتاد المستخدمة</string>
|
||||||
<string name="warning_database_notification_permission">يسمح لك إذن الإشعار بعرض حالة قاعدة البيانات وقفلها باستخدام زر يسهل الوصول إليه.
|
<string name="warning_database_notification_permission">يسمح لك إذن الإشعار بعرض حالة قاعدة البيانات وقفلها باستخدام زر يسهل الوصول إليه.
|
||||||
\n
|
\n
|
||||||
\nإذا لم تنشط هذا الإذن، فلن تكون قاعدة البيانات المفتوحة في الخلفية مرئية إذا كان هناك تطبيق آخر في المقدمة.</string>
|
\nإذا لم تنشط هذا الإذن، فلن تكون قاعدة البيانات المفتوحة في الخلفية مرئية إذا كان هناك تطبيق آخر في المقدمة.</string>
|
||||||
@@ -646,10 +645,9 @@
|
|||||||
<string name="advanced_unlock_prompt_extract_credential_message">استخراج بيانات اعتماد قاعدة البيانات مع بيانات فتح الجهاز</string>
|
<string name="advanced_unlock_prompt_extract_credential_message">استخراج بيانات اعتماد قاعدة البيانات مع بيانات فتح الجهاز</string>
|
||||||
<string name="ask">إسأل</string>
|
<string name="ask">إسأل</string>
|
||||||
<string name="configure_biometric">لم تسجل بيانات اعتماد المقاييس الحيوية أو الجهاز.</string>
|
<string name="configure_biometric">لم تسجل بيانات اعتماد المقاييس الحيوية أو الجهاز.</string>
|
||||||
<string name="show_uuid_title">إظهار \"المعرف العام المميز\" UUID</string>
|
<string name="show_uuid_title">أظهر \"المعرّف العام المميز\" UUID</string>
|
||||||
<string name="unlock_and_link_biometric">رابط فتح الجهاز</string>
|
<string name="unlock_and_link_biometric">رابط فتح الجهاز</string>
|
||||||
<string name="advanced_unlock_invalid_key">لا يمكن قراءة مفتاح فتح الجهاز. يرجى حذفه وتكرار إجراء التعرف على الفتح.</string>
|
<string name="advanced_unlock_invalid_key">لا يمكن قراءة مفتاح فتح الجهاز. يرجى حذفه وتكرار إجراء التعرف على الفتح.</string>
|
||||||
<string name="advanced_unlock_scanning_error">خطأ في فتح الجهاز: %1$s</string>
|
|
||||||
<string name="menu_appearance_settings_summary">المظاهر والألوان والسمات</string>
|
<string name="menu_appearance_settings_summary">المظاهر والألوان والسمات</string>
|
||||||
<string name="autofill_explanation_summary">تمكين الملء التلقائي لملء النماذج بسرعة في التطبيقات الأخرى</string>
|
<string name="autofill_explanation_summary">تمكين الملء التلقائي لملء النماذج بسرعة في التطبيقات الأخرى</string>
|
||||||
<string name="device_credential_unlock_enable_summary">يتيح لك استخدام بيانات اعتماد جهازك لفتح قاعدة البيانات</string>
|
<string name="device_credential_unlock_enable_summary">يتيح لك استخدام بيانات اعتماد جهازك لفتح قاعدة البيانات</string>
|
||||||
@@ -660,15 +658,14 @@
|
|||||||
<string name="advanced_unlock_keystore_warning">ستقوم هذه الميزة بتخزين بيانات الاعتماد المشفرة في KeyStore الآمن بجهازك.
|
<string name="advanced_unlock_keystore_warning">ستقوم هذه الميزة بتخزين بيانات الاعتماد المشفرة في KeyStore الآمن بجهازك.
|
||||||
\n
|
\n
|
||||||
\nاعتمادًا على تطبيق API الأصلي لنظام التشغيل، قد لا يعمل بكامل طاقته.
|
\nاعتمادًا على تطبيق API الأصلي لنظام التشغيل، قد لا يعمل بكامل طاقته.
|
||||||
\nتحقق من توافق وأمن KeyStore مع الشركة المصنعة لجهازك ومنشئ ROM الذي تستخدمه.</string>
|
|
||||||
<string name="keyboard_selection_entry_summary">عند عرض إدخال في KeePassDX، عبئ Magikeyboard بهذا الإدخال</string>
|
|
||||||
<string name="enable_screenshot_mode_summary">اسمح لتطبيقات الطرف الثالث بتسجيل أو التقاط لقطات شاشة للتطبيق</string>
|
|
||||||
<string name="keyboard_save_search_info_summary">حاول حفظ المعلومات المشتركة عند إجراء اختيار إدخال يدوي لاستخدامات مستقبلية أسهل</string>
|
|
||||||
<string name="education_entry_edit_summary">تحرير الإدخال الخاص بك مع الحقول المخصصة. يمكن الرجوع إلى بيانات التجمع بين حقول الإدخال المختلفة.</string>
|
|
||||||
<string name="education_validate_entry_title">تحقق من صحة الإدخال</string>
|
|
||||||
<string name="education_validate_entry_summary">تذكر التحقق من صحة الإدخال الخاص بك وحفظ قاعدة البيانات الخاصة بك.
|
|
||||||
\n
|
\n
|
||||||
\nإذا تم تنشيط القفل التلقائي ونسيت أنك تجري تعديلاً، فإنك تخاطر بفقدان بياناتك.</string>
|
\nتحقق من توافق وأمن KeyStore مع الشركة المصنعة لجهازك ومنشئ ROM الذي تستخدمه.</string>
|
||||||
|
<string name="keyboard_selection_entry_summary">عند عرض مدخل في KeePassDX، عبئ Magikeyboard بهذا المدخل</string>
|
||||||
|
<string name="enable_screenshot_mode_summary">اسمح لتطبيقات الطرف الثالث بتسجيل أو التقاط لقطات شاشة للتطبيق</string>
|
||||||
|
<string name="keyboard_save_search_info_summary">حاول حفظ المعلومات المشتركة عند إجراء اختيار مدخل يدوي لاستخدامات مستقبلية أسهل</string>
|
||||||
|
<string name="education_entry_edit_summary">عدّل إدخالك مع الحقول المخصّصة. يمكن الرجوع إلى بيانات التجمع بين حقول مدخل المختلفة.</string>
|
||||||
|
<string name="education_validate_entry_title">تحقق من صحة المدخل</string>
|
||||||
|
<string name="education_validate_entry_summary">تذكر التحقق من صحة إدخالك وحفظ قاعدة بياناتك. \n \nإذا القفل التلقائي مُنشّط ونسيت أنك تجري تعديلاً، فإنك تخاطر بفقدان بياناتك.</string>
|
||||||
<string name="education_entry_new_field_summary">قم بتسجيل حقل إضافي، أضف قيمة وقم بحمايته بشكل اختياري.</string>
|
<string name="education_entry_new_field_summary">قم بتسجيل حقل إضافي، أضف قيمة وقم بحمايته بشكل اختياري.</string>
|
||||||
<string name="education_unlock_summary">أدخل كلمة المرور و/أو ملف المفتاح لفتح قاعدة بياناتك.
|
<string name="education_unlock_summary">أدخل كلمة المرور و/أو ملف المفتاح لفتح قاعدة بياناتك.
|
||||||
\n
|
\n
|
||||||
@@ -681,6 +678,25 @@
|
|||||||
<string name="download_initialization">جارِ التهيئة…</string>
|
<string name="download_initialization">جارِ التهيئة…</string>
|
||||||
<string name="download_progression">قيد التقدم: %1$d%%</string>
|
<string name="download_progression">قيد التقدم: %1$d%%</string>
|
||||||
<string name="html_text_buy_pro">بشراء الإصدار الاحترافي، ستتمتع بإمكانية الوصول إلى هذا <strong> النمط المرئي</strong> وستساعد بشكل خاص في <strong> تنفيذ مشروعات المجتمع. </strong></string>
|
<string name="html_text_buy_pro">بشراء الإصدار الاحترافي، ستتمتع بإمكانية الوصول إلى هذا <strong> النمط المرئي</strong> وستساعد بشكل خاص في <strong> تنفيذ مشروعات المجتمع. </strong></string>
|
||||||
<string name="html_text_donation">من أجل الحفاظ على حريتنا ولكي نكون نشيطين دائمًا، فإننا نعتمد على <strong> مساهمتك.</strong></string>
|
<string name="html_text_donation">من خلال <strong>المساهمة</strong> في المشروع <i>(مالياً أو برمجياً أو ترجمة)</i>، ستساعده على الاستمرار في الحياة والازدهار، وستكون مؤهلاً أيضاً لإجراء فتح <strong>السمة</strong>.</string>
|
||||||
<string name="html_text_dev_feature_encourage">أنت تشجع المطورين على إنشاء <strong> ميزات جديدة</strong> و <strong> إصلاح الخلل</strong> وفقًا لملاحظاتك.</string>
|
<string name="html_text_dev_feature_encourage">أنت تشجع المطورين على إنشاء <strong> ميزات جديدة</strong> و <strong> إصلاح الخلل</strong> وفقًا لملاحظاتك.</string>
|
||||||
</resources>
|
<string name="style_name_forest">غابة</string>
|
||||||
|
<string name="style_name_simple">بسيط</string>
|
||||||
|
<string name="style_name_moon">قمر</string>
|
||||||
|
<string name="style_name_divine">إلهي</string>
|
||||||
|
<string name="style_name_classic">كلاسيكي</string>
|
||||||
|
<string name="style_name_dark">داكن</string>
|
||||||
|
<string name="style_name_reply">رد</string>
|
||||||
|
<string name="warning_database_info_changed_options_read_only">أعِد تحميل قاعدة البيانات بأحدث التغييرات.</string>
|
||||||
|
<string name="style_name_sun">شمس</string>
|
||||||
|
<string name="style_name_kunzite">الكونزيت</string>
|
||||||
|
<string name="style_name_follow_system">اتبع النظام</string>
|
||||||
|
<string name="style_name_light">فاتح</string>
|
||||||
|
<string name="hide_templates_summary">لا يتم عرض القوالب</string>
|
||||||
|
<string name="generate_keyfile">ولّد ملف مفتاح</string>
|
||||||
|
<string name="nodes">العُقد</string>
|
||||||
|
<string name="recursive_number_entries_title">عدد متكرر من المدخلات</string>
|
||||||
|
<string name="recursive_number_entries_summary">يحسب بشكل متكرر عدد المدخلات في المجموعة</string>
|
||||||
|
<string name="warning_large_keyfile">لا يُنصح بإضافة ملف مفتاحي كبير، فقد يؤدي هذا إلى منع فتح قاعدة البيانات.</string>
|
||||||
|
<string name="hide_templates_title">أخفِ القوالب</string>
|
||||||
|
</resources>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user